diff --git a/xr/src/main/java/com/example/xr/projected/PhoneMainActivity.kt b/xr/src/main/java/com/example/xr/projected/PhoneMainActivity.kt index 3a0ff5a6a..bfb0b961e 100644 --- a/xr/src/main/java/com/example/xr/projected/PhoneMainActivity.kt +++ b/xr/src/main/java/com/example/xr/projected/PhoneMainActivity.kt @@ -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)) diff --git a/xr/src/main/java/com/example/xr/projected/ProjectedHardware.kt b/xr/src/main/java/com/example/xr/projected/ProjectedHardware.kt index 3b656237d..499ace1ee 100644 --- a/xr/src/main/java/com/example/xr/projected/ProjectedHardware.kt +++ b/xr/src/main/java/com/example/xr/projected/ProjectedHardware.kt @@ -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 @@ -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" @@ -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) @@ -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] diff --git a/xr/src/main/java/com/example/xr/projected/ProjectedMainActivity.kt b/xr/src/main/java/com/example/xr/projected/ProjectedMainActivity.kt index 0bb54e273..543fc53c2 100644 --- a/xr/src/main/java/com/example/xr/projected/ProjectedMainActivity.kt +++ b/xr/src/main/java/com/example/xr/projected/ProjectedMainActivity.kt @@ -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) { @@ -177,7 +177,7 @@ fun HomeScreen( } ) { if (areVisualsOn) { - Text("Hello, AI Glasses!") + Text("Hello, Display Glasses!") } else { Text("Display is off. Audio guidance active.") }