Skip to content
Open
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
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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" }
Expand Down
2 changes: 2 additions & 0 deletions xr/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
66 changes: 56 additions & 10 deletions xr/src/main/java/com/example/xr/projected/GlassesMainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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]
Expand All @@ -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 ->
Copy link
Contributor

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

Copy link
Contributor Author

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.

if (results[Manifest.permission.CAMERA] == true) {
// Permission granted, initialize the session/features.
initializeGlassesFeatures()
} else {
// Handle permission denial.
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The permission denial case is not handled. It's important to provide feedback to the user when a required permission is denied. You should consider showing a message explaining why the camera permission is necessary for the app's functionality and guide them on how to grant it in the app settings.

}
}
// [END androidxr_projected_permissions_launcher]

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

Expand All @@ -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
Expand All @@ -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]

Expand Down Expand Up @@ -123,4 +169,4 @@ fun HomeScreen(
}
}
}
// [END androidxr_projected_ai_glasses_activity_homescreen]
// [END androidxr_projected_ai_glasses_activity_homescreen]
147 changes: 147 additions & 0 deletions xr/src/main/java/com/example/xr/projected/ProjectedHardware.kt
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
}
}
// [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)
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The cameraCharacteristics variable is assigned but never used. If it's not needed for any logic, it should be removed to avoid confusion and improve code clarity.


// 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]