Skip to content

Commit e44fb10

Browse files
committed
add in projected snippets for AI glasses: glasses activity, launching a glasses activity
1 parent 2421773 commit e44fb10

File tree

7 files changed

+334
-2
lines changed

7 files changed

+334
-2
lines changed

gradle/libs.versions.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ wearToolingPreview = "1.0.0"
9898
webkit = "1.14.0"
9999
wearPhoneInteractions = "1.1.0"
100100
wearRemoteInteractions = "1.1.0"
101+
xrGlimmer = "1.0.0-alpha03"
102+
xrProjected = "1.0.0-alpha03"
101103

102104
[libraries]
103105
accompanist-adaptive = "com.google.accompanist:accompanist-adaptive:0.37.3"
@@ -241,6 +243,8 @@ wear-compose-material = { module = "androidx.wear.compose:compose-material", ver
241243
wear-compose-material3 = { module = "androidx.wear.compose:compose-material3", version.ref = "wearComposeMaterial3" }
242244
androidx-wear-phone-interactions = { group = "androidx.wear", name = "wear-phone-interactions", version.ref = "wearPhoneInteractions" }
243245
androidx-wear-remote-interactions = { group = "androidx.wear", name = "wear-remote-interactions", version.ref = "wearRemoteInteractions" }
246+
androidx-glimmer = { group = "androidx.xr.glimmer", name = "glimmer", version.ref = "xrGlimmer" }
247+
androidx-projected = { group = "androidx.xr.projected", name = "projected", version.ref = "xrProjected" }
244248

245249
[plugins]
246250
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }

xr/build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ dependencies {
4040
implementation(libs.androidx.activity.ktx)
4141

4242
implementation(libs.androidx.media3.exoplayer)
43+
implementation(libs.androidx.glimmer)
44+
implementation(libs.androidx.projected)
4345

4446
val composeBom = platform(libs.androidx.compose.bom)
4547
implementation(composeBom)
@@ -68,4 +70,4 @@ dependencies {
6870

6971
implementation(libs.androidx.activity.compose)
7072
implementation(libs.androidx.appcompat)
71-
}
73+
}

xr/src/main/AndroidManifest.xml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,23 @@
1919

2020
<application
2121
android:label="XR"
22-
tools:ignore="MissingApplicationIcon" />
22+
tools:ignore="MissingApplicationIcon">
23+
<activity
24+
android:name="com.example.xr.projected.GlassesMainActivity"
25+
android:exported="true"
26+
android:requiredDisplayCategory="@string/display_category_xr_projected">
27+
<intent-filter>
28+
<action android:name="android.intent.action.MAIN" />
29+
</intent-filter>
30+
</activity>
31+
<activity
32+
android:name="com.example.xr.projected.PhoneMainActivity"
33+
android:exported="true">
34+
<intent-filter>
35+
<action android:name="android.intent.action.MAIN" />
36+
<category android:name="android.intent.category.LAUNCHER" />
37+
</intent-filter>
38+
</activity>
39+
</application>
2340

2441
</manifest>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.xr.projected
18+
19+
import android.content.Context
20+
import androidx.core.content.ContextCompat
21+
import androidx.lifecycle.DefaultLifecycleObserver
22+
import androidx.lifecycle.LifecycleOwner
23+
import androidx.xr.projected.ProjectedDisplayController
24+
import androidx.xr.projected.ProjectedDisplayController.PresentationMode
25+
import androidx.xr.projected.experimental.ExperimentalProjectedApi
26+
import java.util.function.Consumer
27+
28+
@OptIn(ExperimentalProjectedApi::class)
29+
class GlassesLifecycleObserver(
30+
context: Context,
31+
private val controller: ProjectedDisplayController,
32+
private val onVisualsChanged: (Boolean) -> Unit
33+
) : DefaultLifecycleObserver {
34+
35+
private val executor = ContextCompat.getMainExecutor(context)
36+
37+
private val visualStateListener = Consumer<ProjectedDisplayController.PresentationModeFlags> { flags ->
38+
val visualsOn = flags.hasPresentationMode(PresentationMode.VISUALS_ON)
39+
onVisualsChanged(visualsOn)
40+
}
41+
42+
override fun onStart(owner: LifecycleOwner) {
43+
// Register when the Activity is visible
44+
controller.addPresentationModeChangedListener(executor, visualStateListener)
45+
}
46+
47+
override fun onStop(owner: LifecycleOwner) {
48+
// Unregister when the Activity is hidden to save battery and prevent leaks
49+
controller.removePresentationModeChangedListener(visualStateListener)
50+
}
51+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.xr.projected
18+
19+
import android.os.Bundle
20+
import androidx.activity.ComponentActivity
21+
import androidx.activity.compose.setContent
22+
import androidx.activity.viewModels
23+
import androidx.compose.foundation.layout.Box
24+
import androidx.compose.foundation.layout.fillMaxSize
25+
import androidx.compose.runtime.Composable
26+
import androidx.compose.runtime.getValue
27+
import androidx.compose.ui.Alignment
28+
import androidx.compose.ui.Modifier
29+
import androidx.lifecycle.LifecycleOwner
30+
import androidx.lifecycle.DefaultLifecycleObserver
31+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
32+
import androidx.lifecycle.lifecycleScope
33+
import androidx.xr.glimmer.Button
34+
import androidx.xr.glimmer.Card
35+
import androidx.xr.glimmer.GlimmerTheme
36+
import androidx.xr.glimmer.Text
37+
import androidx.xr.glimmer.surface
38+
import androidx.xr.projected.ProjectedDisplayController
39+
import androidx.xr.projected.experimental.ExperimentalProjectedApi
40+
import kotlinx.coroutines.launch
41+
42+
// [START androidxr_projected_ai_glasses_activity]
43+
@OptIn(ExperimentalProjectedApi::class)
44+
class GlassesMainActivity : ComponentActivity() {
45+
46+
override fun onCreate(savedInstanceState: Bundle?) {
47+
super.onCreate(savedInstanceState)
48+
val viewModel: GlassesViewModel by viewModels()
49+
50+
lifecycleScope.launch {
51+
val controller = ProjectedDisplayController.create(this@GlassesMainActivity)
52+
53+
val observer = GlassesLifecycleObserver(
54+
context = this@GlassesMainActivity,
55+
controller = controller,
56+
onVisualsChanged = viewModel::updateVisuals
57+
)
58+
lifecycle.addObserver(observer)
59+
60+
// Cleanup observer to close the controller
61+
lifecycle.addObserver(object : DefaultLifecycleObserver {
62+
override fun onDestroy(owner: LifecycleOwner) {
63+
controller.close()
64+
}
65+
})
66+
}
67+
68+
setContent {
69+
// [required] Use collectAsStateWithLifecycle for safe collection
70+
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
71+
72+
GlimmerTheme {
73+
HomeScreen(
74+
visualsOn = uiState.areVisualsOn,
75+
onClose = { finish() }
76+
)
77+
}
78+
}
79+
}
80+
}
81+
// [END androidxr_projected_ai_glasses_activity]
82+
83+
// [START androidxr_projected_ai_glasses_activity_homescreen]
84+
@Composable
85+
fun HomeScreen(
86+
visualsOn: Boolean,
87+
onClose: () -> Unit,
88+
modifier: Modifier = Modifier
89+
) {
90+
Box(
91+
modifier = modifier
92+
.surface(focusable = false)
93+
.fillMaxSize(),
94+
contentAlignment = Alignment.Center
95+
) {
96+
Card(
97+
title = { Text("Android XR") },
98+
action = {
99+
Button(onClick = onClose) {
100+
Text("Close")
101+
}
102+
}
103+
) {
104+
if (visualsOn) {
105+
Text("Hello, AI Glasses!")
106+
} else {
107+
Text("Display is off. Audio guidance active.")
108+
}
109+
}
110+
}
111+
}
112+
// [END androidxr_projected_ai_glasses_activity_homescreen]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.xr.projected
18+
19+
import androidx.lifecycle.ViewModel
20+
import kotlinx.coroutines.flow.MutableStateFlow
21+
import kotlinx.coroutines.flow.StateFlow
22+
import kotlinx.coroutines.flow.asStateFlow
23+
import kotlinx.coroutines.flow.update
24+
25+
data class GlassesUiState(
26+
val areVisualsOn: Boolean = true
27+
)
28+
class GlassesViewModel : ViewModel() {
29+
private val _uiState = MutableStateFlow(GlassesUiState())
30+
val uiState: StateFlow<GlassesUiState> = _uiState.asStateFlow()
31+
32+
fun updateVisuals(visualsOn: Boolean) {
33+
_uiState.update { it.copy(areVisualsOn = visualsOn) }
34+
}
35+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.xr.projected
18+
19+
import android.content.Intent
20+
import android.os.Build
21+
import android.os.Bundle
22+
import androidx.activity.ComponentActivity
23+
import androidx.activity.compose.setContent
24+
import androidx.activity.enableEdgeToEdge
25+
import androidx.annotation.RequiresApi
26+
import androidx.compose.foundation.layout.Arrangement
27+
import androidx.compose.foundation.layout.Column
28+
import androidx.compose.foundation.layout.Spacer
29+
import androidx.compose.foundation.layout.fillMaxSize
30+
import androidx.compose.foundation.layout.height
31+
import androidx.compose.foundation.layout.padding
32+
import androidx.compose.material3.Button
33+
import androidx.compose.material3.ButtonDefaults
34+
import androidx.compose.material3.MaterialTheme
35+
import androidx.compose.material3.Scaffold
36+
import androidx.compose.material3.Text
37+
import androidx.compose.runtime.Composable
38+
import androidx.compose.runtime.getValue
39+
import androidx.compose.runtime.rememberCoroutineScope
40+
import androidx.compose.ui.Alignment
41+
import androidx.compose.ui.Modifier
42+
import androidx.compose.ui.platform.LocalContext
43+
import androidx.compose.ui.unit.dp
44+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
45+
import androidx.xr.projected.ProjectedContext
46+
import androidx.xr.projected.experimental.ExperimentalProjectedApi
47+
48+
class PhoneMainActivity : ComponentActivity() {
49+
@RequiresApi(Build.VERSION_CODES.BAKLAVA)
50+
override fun onCreate(savedInstanceState: Bundle?) {
51+
super.onCreate(savedInstanceState)
52+
enableEdgeToEdge()
53+
setContent {
54+
MaterialTheme {
55+
ConnectionScreen()
56+
}
57+
}
58+
}
59+
}
60+
61+
@RequiresApi(Build.VERSION_CODES.BAKLAVA)
62+
@OptIn(ExperimentalProjectedApi::class)
63+
@Composable
64+
fun ConnectionScreen() {
65+
val context = LocalContext.current
66+
Scaffold { paddingValues ->
67+
Column(
68+
modifier = Modifier
69+
.fillMaxSize()
70+
.padding(paddingValues),
71+
horizontalAlignment = Alignment.CenterHorizontally,
72+
verticalArrangement = Arrangement.Center
73+
) {
74+
Text(
75+
text = "Hello AI Glasses",
76+
style = MaterialTheme.typography.titleLarge
77+
)
78+
Spacer(modifier = Modifier.height(32.dp))
79+
val scope = rememberCoroutineScope()
80+
val isGlassesConnected by ProjectedContext.isProjectedDeviceConnected(
81+
context,
82+
scope.coroutineContext
83+
).collectAsStateWithLifecycle(initialValue = false)
84+
Button(
85+
onClick = {
86+
// [START androidxr_projected_start_glasses_activity]
87+
88+
val options = ProjectedContext.createProjectedActivityOptions(context)
89+
val intent = Intent(context, GlassesMainActivity::class.java)
90+
context.startActivity(intent, options.toBundle())
91+
92+
// [END androidxr_projected_start_glasses_activity]
93+
},
94+
colors = ButtonDefaults.buttonColors(
95+
containerColor = if (isGlassesConnected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
96+
),
97+
enabled = isGlassesConnected
98+
) {
99+
Text(
100+
text = "Launch",
101+
style = MaterialTheme.typography.headlineMedium
102+
)
103+
}
104+
Spacer(modifier = Modifier.height(32.dp))
105+
Text(
106+
text = "Status: " + if (isGlassesConnected) "Connected" else "Disconnected",
107+
style = MaterialTheme.typography.titleMedium
108+
)
109+
}
110+
}
111+
}

0 commit comments

Comments
 (0)