diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index da07354ea..c536e9ec0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,6 +5,7 @@ android-googleid = "1.2.0" androidGradlePlugin = "9.0.0" androidx-activity-compose = "1.12.3" androidx-appcompat = "1.7.0" +androidx-cameraX = "1.5.3" androidx-compose-bom = "2026.01.01" androidx-compose-ui-test = "1.7.0-alpha08" androidx-compose-ui-test-junit4-accessibility = "1.11.0-alpha04" @@ -115,6 +116,8 @@ android-identity-googleid = { module = "com.google.android.libraries.identity.go androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" } androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } +androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "androidx-cameraX" } +androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "androidx-cameraX" } androidx-compose-animation-graphics = { module = "androidx.compose.animation:animation-graphics", version.ref = "compose-latest" } androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" } androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose-latest" } diff --git a/xr/build.gradle.kts b/xr/build.gradle.kts index 75a65f603..ace08d38a 100644 --- a/xr/build.gradle.kts +++ b/xr/build.gradle.kts @@ -40,6 +40,8 @@ dependencies { implementation(libs.androidx.activity.ktx) implementation(libs.androidx.media3.exoplayer) + implementation(libs.androidx.camera.lifecycle) + implementation(libs.androidx.camera.camera2) val composeBom = platform(libs.androidx.compose.bom) implementation(composeBom) diff --git a/xr/src/main/java/com/example/xr/projected/GlassesMainActivity.kt b/xr/src/main/java/com/example/xr/projected/GlassesMainActivity.kt index 66cd13cb0..484d9be54 100644 --- a/xr/src/main/java/com/example/xr/projected/GlassesMainActivity.kt +++ b/xr/src/main/java/com/example/xr/projected/GlassesMainActivity.kt @@ -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> = + registerForActivityResult(ProjectedPermissionsResultContract()) { results -> + if (results[Manifest.permission.CAMERA] == true) { + // Permission granted, initialize the session/features. + initializeGlassesFeatures() + } else { + // Handle permission denial. + } + } + // [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] \ No newline at end of file diff --git a/xr/src/main/java/com/example/xr/projected/ProjectedHardware.kt b/xr/src/main/java/com/example/xr/projected/ProjectedHardware.kt new file mode 100644 index 000000000..ac0bd8484 --- /dev/null +++ b/xr/src/main/java/com/example/xr/projected/ProjectedHardware.kt @@ -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 + } +} +// [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) + + 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 + ) + + // 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]