diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e4b6c63c..75ed8c7c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -98,6 +98,8 @@ wearToolingPreview = "1.0.0"
webkit = "1.14.0"
wearPhoneInteractions = "1.1.0"
wearRemoteInteractions = "1.1.0"
+xrGlimmer = "1.0.0-alpha03"
+xrProjected = "1.0.0-alpha03"
[libraries]
accompanist-adaptive = "com.google.accompanist:accompanist-adaptive:0.37.3"
@@ -241,6 +243,8 @@ wear-compose-material = { module = "androidx.wear.compose:compose-material", ver
wear-compose-material3 = { module = "androidx.wear.compose:compose-material3", version.ref = "wearComposeMaterial3" }
androidx-wear-phone-interactions = { group = "androidx.wear", name = "wear-phone-interactions", version.ref = "wearPhoneInteractions" }
androidx-wear-remote-interactions = { group = "androidx.wear", name = "wear-remote-interactions", version.ref = "wearRemoteInteractions" }
+androidx-glimmer = { group = "androidx.xr.glimmer", name = "glimmer", version.ref = "xrGlimmer" }
+androidx-projected = { group = "androidx.xr.projected", name = "projected", version.ref = "xrProjected" }
[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
diff --git a/xr/build.gradle.kts b/xr/build.gradle.kts
index 690fc956..65ec078a 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.glimmer)
+ implementation(libs.androidx.projected)
val composeBom = platform(libs.androidx.compose.bom)
implementation(composeBom)
@@ -68,4 +70,4 @@ dependencies {
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
-}
\ No newline at end of file
+}
diff --git a/xr/src/main/AndroidManifest.xml b/xr/src/main/AndroidManifest.xml
index bc726787..883cceae 100644
--- a/xr/src/main/AndroidManifest.xml
+++ b/xr/src/main/AndroidManifest.xml
@@ -19,6 +19,23 @@
+ tools:ignore="MissingApplicationIcon">
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/xr/src/main/java/com/example/xr/projected/GlassesLifecycleObserver.kt b/xr/src/main/java/com/example/xr/projected/GlassesLifecycleObserver.kt
new file mode 100644
index 00000000..4ea97fa2
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/projected/GlassesLifecycleObserver.kt
@@ -0,0 +1,50 @@
+/*
+ * 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 androidx.core.content.ContextCompat
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.xr.projected.ProjectedDisplayController
+import androidx.xr.projected.ProjectedDisplayController.PresentationMode
+import androidx.xr.projected.experimental.ExperimentalProjectedApi
+import java.util.function.Consumer
+
+@OptIn(ExperimentalProjectedApi::class)
+class GlassesLifecycleObserver(
+ private val context: Context,
+ private val controller: ProjectedDisplayController,
+ private val onVisualsChanged: (Boolean) -> Unit
+) : DefaultLifecycleObserver {
+
+ private val executor = ContextCompat.getMainExecutor(context)
+
+ private val visualStateListener = Consumer { flags ->
+ val visualsOn = flags.hasPresentationMode(PresentationMode.VISUALS_ON)
+ onVisualsChanged(visualsOn)
+ }
+
+ override fun onStart(owner: LifecycleOwner) {
+ controller.addPresentationModeChangedListener(executor, visualStateListener)
+ }
+
+ override fun onStop(owner: LifecycleOwner) {
+ // unregister to stop consuming values and prevent memory leaks.
+ controller.removePresentationModeChangedListener(visualStateListener)
+ }
+}
diff --git a/xr/src/main/java/com/example/xr/projected/GlassesMainActivity.kt b/xr/src/main/java/com/example/xr/projected/GlassesMainActivity.kt
new file mode 100644
index 00000000..954666e0
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/projected/GlassesMainActivity.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.lifecycle.lifecycleScope
+import androidx.xr.glimmer.Button
+import androidx.xr.glimmer.Card
+import androidx.xr.glimmer.GlimmerTheme
+import androidx.xr.glimmer.Text
+import androidx.xr.glimmer.surface
+import androidx.xr.projected.ProjectedDisplayController
+import androidx.xr.projected.experimental.ExperimentalProjectedApi
+import kotlinx.coroutines.launch
+
+// [START androidxr_projected_ai_glasses_activity]
+@OptIn(ExperimentalProjectedApi::class)
+class GlassesMainActivity : ComponentActivity() {
+
+ private var projectedDisplayController: ProjectedDisplayController? = null
+ private var areVisualsOn by mutableStateOf(true)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ lifecycleScope.launch {
+ // initialize the controller
+ val controller = ProjectedDisplayController.create(this@GlassesMainActivity)
+ projectedDisplayController = controller
+
+ // attach the observer to manage registration based on Activity visibility
+ val observer = GlassesLifecycleObserver(
+ context = this@GlassesMainActivity,
+ controller = controller,
+ onVisualsChanged = { visualsOn ->
+ areVisualsOn = visualsOn
+ }
+ )
+ lifecycle.addObserver(observer)
+ }
+
+ setContent {
+ GlimmerTheme {
+ HomeScreen(
+ visualsOn = areVisualsOn,
+ onClose = { finish() }
+ )
+ }
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ projectedDisplayController?.close()
+ }
+}
+// [END androidxr_projected_ai_glasses_activity]
+
+// [START androidxr_projected_ai_glasses_activity_homescreen]
+@Composable
+fun HomeScreen(
+ visualsOn: Boolean,
+ modifier: Modifier = Modifier,
+ onClose: () -> Unit
+) {
+ Box(
+ modifier = modifier
+ .surface(focusable = false)
+ .fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Card(
+ title = { Text("Android XR") },
+ action = {
+ Button(onClick = {
+ onClose()
+ }) {
+ Text("Close")
+ }
+ }
+ ) {
+ // UI dynamically updates based on the observer's state
+ if (visualsOn) {
+ Text("Hello, AI Glasses!")
+ } else {
+ Text("Display is off. Audio guidance active.")
+ }
+ }
+ }
+}
+// [END androidxr_projected_ai_glasses_activity_homescreen]
diff --git a/xr/src/main/java/com/example/xr/projected/PhoneMainActivity.kt b/xr/src/main/java/com/example/xr/projected/PhoneMainActivity.kt
new file mode 100644
index 00000000..4534f304
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/projected/PhoneMainActivity.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.Intent
+import android.os.Build
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.xr.projected.ProjectedContext
+import androidx.xr.projected.experimental.ExperimentalProjectedApi
+
+class PhoneMainActivity : ComponentActivity() {
+ @RequiresApi(Build.VERSION_CODES.BAKLAVA)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ MaterialTheme {
+ ConnectionScreen()
+ }
+ }
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.BAKLAVA)
+@OptIn(ExperimentalProjectedApi::class)
+@Composable
+fun ConnectionScreen() {
+ val context = LocalContext.current
+ Scaffold { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = "Hello AI Glasses",
+ style = MaterialTheme.typography.titleLarge
+ )
+ Spacer(modifier = Modifier.height(32.dp))
+ val scope = rememberCoroutineScope()
+ val isGlassesConnected by ProjectedContext.isProjectedDeviceConnected(
+ context,
+ scope.coroutineContext
+ ).collectAsStateWithLifecycle(initialValue = false)
+ Button(
+ onClick = {
+ // [START androidxr_projected_start_glasses_activity]
+
+ val options = ProjectedContext.createProjectedActivityOptions(context)
+ val intent = Intent(context, GlassesMainActivity::class.java)
+ context.startActivity(intent, options.toBundle())
+
+ // [END androidxr_projected_start_glasses_activity]
+ },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = if (isGlassesConnected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
+ ),
+ enabled = isGlassesConnected
+ ) {
+ Text(
+ text = "Launch",
+ style = MaterialTheme.typography.headlineMedium
+ )
+ }
+ Spacer(modifier = Modifier.height(32.dp))
+ Text(
+ text = "Status: " + if (isGlassesConnected) "Connected" else "Disconnected",
+ style = MaterialTheme.typography.titleMedium
+ )
+ }
+ }
+}