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
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ fun ConnectionScreen() {
verticalArrangement = Arrangement.Center
) {
Text(
text = "Hello AI Glasses",
text = "Hello Display Glasses",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(32.dp))
Expand Down
184 changes: 109 additions & 75 deletions xr/src/main/java/com/example/xr/projected/ProjectedHardware.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ import android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioRecord
import android.media.MediaRecorder
import android.os.Build
import android.util.Log
import android.util.Range
import android.util.Size
import androidx.activity.ComponentActivity
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.camera2.interop.CaptureRequestOptions
Expand All @@ -39,8 +41,12 @@ import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.core.resolutionselector.ResolutionStrategy
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.xr.projected.ProjectedContext
import androidx.xr.projected.experimental.ExperimentalProjectedApi
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch

private const val TAG = "ProjectedHardware"

Expand All @@ -61,31 +67,50 @@ private val bufferSize = AudioRecord.getMinBufferSize(
).coerceAtLeast(1024)

/**
* Demonstrates how to obtain a context for the projected device (AI glasses)
* from the host device (phone).
* Demonstrates how to obtain a context for the projected device (audio and display glasses)
* from the host device (phone) and how to monitor the projected device's
* connectivity to manage resources.
*/
// [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
@RequiresApi(Build.VERSION_CODES.BAKLAVA)
@OptIn(ExperimentalProjectedApi::class, ExperimentalCoroutinesApi::class)
private fun monitorProjectedConnectivity(activity: ComponentActivity) {
activity.lifecycleScope.launch {
// Before creating a projected context, check to see if the projected device is connected.
// While this method returns true, the projected context remains valid.
ProjectedContext.isProjectedDeviceConnected(activity, coroutineContext)
.collectLatest { isConnected ->
if (isConnected) {
// From a phone Activity or Service, get a context for the audio and display glasses.
// Re-initialize on reconnect: Obtain another context instance.
val projectedContext = try {
ProjectedContext.createProjectedDeviceContext(activity)
} catch (e: IllegalStateException) {
Log.e(TAG, "Failed to create projected context", e)
return@collectLatest
}

// Use the projectedContext to initialize system services (e.g., CameraManager).
Log.i(TAG, "Projected device connected. Initializing hardware...")
} else {
// The projected context is destroyed when the device disconnects.
// Clean up on disconnect: Listen for 'false' and release resources.
Log.i(TAG, "Projected device disconnected. Cleaning up hardware resources...")
}
}
}
}
// [END androidxr_projected_context_get_projected]

/**
* Demonstrates how to obtain a context for the host device (phone)
* from the projected device (AI glasses).
* from the projected device (audio and display 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.
// From a projected Activity, get a context for the phone.
ProjectedContext.createHostDeviceContext(activity)
} catch (e: IllegalStateException) {
Log.e(TAG, "Failed to create host device context", e)
Expand All @@ -95,77 +120,86 @@ private fun getPhoneContext(activity: ComponentActivity): Context? {
// [END androidxr_projected_context_get_host]

/**
* Demonstrates how to capture an image using the AI glasses' camera.
* Demonstrates how to capture an image using the audio and display glasses' camera.
*/
@RequiresApi(Build.VERSION_CODES.BAKLAVA)
@androidx.annotation.OptIn(ExperimentalCamera2Interop::class)
@OptIn(ExperimentalProjectedApi::class)
@OptIn(ExperimentalProjectedApi::class, ExperimentalCoroutinesApi::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
}
activity.lifecycleScope.launch {
// Before creating a projected context, check to see if the projected device is connected.
ProjectedContext.isProjectedDeviceConnected(activity, coroutineContext)
.collectLatest { isConnected ->
if (isConnected) {
// 1. Get the CameraProvider using the projected context.
// When using the projected context, DEFAULT_BACK_CAMERA maps to the audio and display glasses' camera.
val projectedContext = try {
ProjectedContext.createProjectedDeviceContext(activity)
} catch (e: IllegalStateException) {
Log.e(TAG, "Projected context could not be created", e)
return@collectLatest
}

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()
val cameraProviderFuture = ProcessCameraProvider.getInstance(projectedContext)

// 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()
cameraProviderFuture.addListener({
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

// 6. Initialize the ImageCapture use case with options.
val imageCapture = ImageCapture.Builder()
// Optional: Configure resolution, format, etc.
.setResolutionSelector(resolutionSelector)
.build()
// 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()

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ fun HomeScreen(
title = { Text("Permission Required") },
action = { Button(onClick = onClose) { Text("Exit") } }
) {
Text("Camera access is needed to use AI glasses features.")
Text("Camera access is needed to use display glasses features.")
Button(onClick = onRetryPermission) { Text("Retry") }
}
} else if (isVisualUiSupported) {
Expand All @@ -177,7 +177,7 @@ fun HomeScreen(
}
) {
if (areVisualsOn) {
Text("Hello, AI Glasses!")
Text("Hello, Display Glasses!")
} else {
Text("Display is off. Audio guidance active.")
}
Expand Down
Loading