skill: add Android skills

This commit is contained in:
2026-07-14 10:34:54 +08:00
parent e260c02629
commit ab82fb7cf0
182 changed files with 62992 additions and 0 deletions

View File

@@ -0,0 +1,360 @@
Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) Audio \&
Display Glasses [](https://developer.android.com/develop/xr/devices#audio-display) [Learn about XR device types →](https://developer.android.com/develop/xr/devices)
<br />
After you've [requested and been granted the necessary permissions](https://developer.android.com/develop/xr/jetpack-xr-sdk/request-hardware-permissions), your app
can access the hardware on the audio glasses or display glasses. The key to
accessing the glasses' hardware (instead of the phone's hardware), is to use a
[projected context](https://developer.android.com/develop/xr/jetpack-xr-sdk/glasses/support-different-types#projected-context).
There are two primary ways to get a projected context, depending on where your
code is executing:
## Get a projected context if your code is running in an projected activity
If your app's code is running from within your [projected activity](https://developer.android.com/develop/xr/jetpack-xr-sdk/glasses/support-different-types#activity-lifecycle), its own
activity context is already a [projected context](https://developer.android.com/develop/xr/jetpack-xr-sdk/glasses/support-different-types#projected-context). In this scenario, calls
made within that activity can already access the glasses' hardware.
## Get a projected context for code running in a phone app component
If a part of your app outside of your projected activity (such as a phone
activity or a service) needs to access the glasses' hardware, it must explicitly
obtain a projected context. To do this, use the
[`createProjectedDeviceContext`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createProjectedDeviceContext(android.content.Context)) method:
```kotlin
@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
}
}
```
<br />
### Check for validity
Wrap the [`createProjectedDeviceContext`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createProjectedDeviceContext(android.content.Context)) call within the
[`ProjectedContext.isProjectedDeviceConnected`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#isProjectedDeviceConnected(android.content.Context,kotlin.coroutines.CoroutineContext)). While this method returns
`true`, the projected context remains valid to the connected device, and your
phone app activity or service (such as a `CameraManager`) can access the AI
glasses hardware.
### Clean up on disconnect
The projected context is tied to the lifecycle of the connected device, so it is
destroyed when the device disconnects. When the device disconnects,
[`ProjectedContext.isProjectedDeviceConnected`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#isProjectedDeviceConnected(android.content.Context,kotlin.coroutines.CoroutineContext)) returns `false`. Your app
should listen for this change and clean up any system services (such as a
`CameraManager`) or resources that your app created using that projected
context.
### Re-initialize on reconnect
When the glasses reconnect, your app can obtain another projected
context instance using [`createProjectedDeviceContext`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createProjectedDeviceContext(android.content.Context)), and then
re-initialize any system services or resources using the new projected context.
## Record audio with the glasses' microphone
You can record audio from the glasses using two distinct methods:
- Use a [projected context](https://developer.android.com/develop/xr/jetpack-xr-sdk/glasses/support-different-types#projected-context).
- Use Bluetooth Hands-Free Profile (HFP).
### Choose a recording methods
The method you choose depends on whether you need high-fidelity, XR-specific
audio processing, or standard Bluetooth audio input.
| Recording method | Microphone access | Common use case |
|---|---|---|
| Projected Context | Multiple microphones | Recording using a projected context lets your app access multiple microphones from the glasses and its specialized hardware features, such as: - XR-specific spatialization. - Advanced denoising. - Voice separation that distinguishes between wearer's and bystander's voices. - Maintain recording access in multidevice environments even when the glasses are not the active Bluetooth device. |
| Bluetooth HFP | Single microphone | Relies on the Bluetooth Hands-Free Profile (HFP) for immediate, out-of-the-box compatibility. In this mode, the glasses connect to the phone using standard Headset and Advanced Audio Distribution Profile (A2DP) [profiles](https://developer.android.com/develop/connectivity/bluetooth/profiles), functioning like a typical Bluetooth peripheral. If your app is already designed for standard Bluetooth recording, you can use this method to record audio from the glasses without integrating any XR-specific capabilities. |
### Record audio using a projected context
To record audio using a projected context, first request the required runtime
permissions, and then record the audio using the [`AudioRecord`](https://developer.android.com/reference/kotlin/android/media/AudioRecord) API, as
described in the following sections.
#### Request runtime permissions
To access multiple microphones on the glasses, you must request audio
permissions specifically for the projected device. The standard, phone-scoped
`RECORD_AUDIO` permission that a user has granted for your app on their mobile
device is insufficient.
Follow these steps to request the permissions:
1. [Declare the `RECORD_AUDIO` permission](https://developer.android.com/develop/xr/jetpack-xr-sdk/request-hardware-permissions#declare-permissions) in your app's manifest file.
2. Request the projected-device-scoped permissions in one of the following
ways, depending on where your code is executing:
- **Code executing from a projected activity** : Use the [`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) with the [`ProjectedPermissionsResultContract`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsResultContract#ProjectedPermissionsResultContract()). For more information on using this method, see the [register the permissions launcher](https://developer.android.com/develop/xr/jetpack-xr-sdk/request-hardware-permissions#register) section and subsequent sections in the guide for requesting hardware permissions.
- **Code executing from a host phone activity** : Use [`Activity#requestPermissions(permissions, requestCode, deviceId)`](https://developer.android.com/reference/kotlin/android/app/Activity#requestpermissions_1) and provide the device ID obtained from your `projectedDeviceContext`, as described in the [understand the permission request user flow](https://developer.android.com/develop/xr/jetpack-xr-sdk/request-hardware-permissions#permissions-user-flow) section of the guide for requesting hardware permissions.
#### Initialize AudioRecord with a projected context
To ensure that audio is recorded from the glasses rather than the host phone,
you must associate the `AudioRecord` object with the projected device context.
The following code uses the [`AudioRecord.Builder`](https://developer.android.com/reference/kotlin/android/media/AudioRecord.Builder) and passes the
`projectedDeviceContext` to the [`setContext`](https://developer.android.com/reference/kotlin/android/media/AudioRecord.Builder#setcontext) method:
```kotlin
// Initialize AudioRecord with projected device context
val audioRecord = AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
.setAudioFormat(audioFormat)
.setBufferSizeInBytes(bufferSize)
// pass in the projected device context
.setContext(projectedDeviceContext)
.build()
audioRecord.startRecording()
```
<br />
##### Key points about the code
- You can set the audio source to [`CAMCORDER`](https://developer.android.com/reference/kotlin/android/media/MediaRecorder.AudioSource#camcorder),
[`VOICE_RECOGNITION`](https://developer.android.com/reference/kotlin/android/media/MediaRecorder.AudioSource#voice_recognition), [`VOICE_COMMUNICATION`](https://developer.android.com/reference/kotlin/android/media/MediaRecorder.AudioSource#voice_communication), or
[`UNPROCESSED`](https://developer.android.com/reference/kotlin/android/media/MediaRecorder.AudioSource#unprocessed) to tailor the audio processing to your specific use
case.
For example, use `VOICE_COMMUNICATION` if your use case needs automatic
noise reduction. `VOICE_RECOGNITION` is processed with acoustic echo
cancellation (AEC). And if you need raw, unaltered audio, select
`UNPROCESSED` or `CAMCORDER`.
- To ensure compatibility with the glasses, the `audioFormat` object must
define a sample rate of 16kHz and a channel configuration of either mono or
stereo (using [`CHANNEL_IN_MONO`](https://developer.android.com/reference/kotlin/android/media/AudioFormat#channel_in_mono) or [`CHANNEL_IN_STEREO`](https://developer.android.com/reference/kotlin/android/media/AudioFormat#channel_in_stereo)).
- Use [`AudioRecord.getMinBufferSize()`](https://developer.android.com/reference/kotlin/android/media/AudioRecord#getminbuffersize) to determine the minimum buffer
size to create the `AudioRecord` object. However, to prevent audio drops
from the glasses, you should read from this buffer in short, frequent chunks
(ideally 20ms slices) rather than waiting for the entire buffer to fill.
> [!WARNING]
> **Preview:** Support for using the [`MediaRecorder`](https://developer.android.com/reference/kotlin/android/media/MediaRecorder) API with a projected context will be available in an upcoming Android Beta release.
##### Clean up after use
When your app no longer needs the microphone, or when the activity is stopped,
call [`stop`](https://developer.android.com/reference/kotlin/android/media/AudioRecord#stop) and [`release`](https://developer.android.com/reference/kotlin/android/media/AudioRecord#release) on the `AudioRecord` object.
##### Check for runtime permissions before recording
Before calling [`startRecording`](https://developer.android.com/reference/kotlin/android/media/AudioRecord#startrecording), [verify that the user has granted the
microphone permission for the glasses](https://developer.android.com/develop/xr/jetpack-xr-sdk/request-hardware-permissions#check-function) using the projected context.
### Record audio using Bluetooth HFP
To record audio using Bluetooth HFP, first request the required runtime
permissions, and then record the audio using the [`AudioManager`](https://developer.android.com/reference/kotlin/android/media/AudioManager) API, as
described in the following sections.
#### Request permissions
As with any standard Bluetooth audio device, the [`RECORD_AUDIO`](https://developer.android.com/reference/kotlin/android/Manifest.permission#record_audio),
[`BLUETOOTH_CONNECT`](https://developer.android.com/reference/kotlin/android/Manifest.permission#bluetooth_connect), and other related permissions are controlled by the
phone and not the connected device (such as audio glasses or display glasses).
Follow these steps to request the permissions:
1. [Declare following permissions](https://developer.android.com/develop/xr/jetpack-xr-sdk/request-hardware-permissions#declare-permissions) in your app's manifest file:
- [`RECORD_AUDIO`](https://developer.android.com/reference/kotlin/android/Manifest.permission#record_audio)
- [`BLUETOOTH_CONNECT`](https://developer.android.com/reference/kotlin/android/Manifest.permission#bluetooth_connect)
- [`MODIFY_AUDIO_SETTINGS`](https://developer.android.com/reference/kotlin/android/Manifest.permission#modify_audio_settings)
2. Request both the `RECORD_AUDIO` and `BLUETOOTH_CONNECT` permissions at
runtime using the [standard Android permission flow](https://developer.android.com/training/permissions/requesting).
#### Use AudioManager to route audio
After the user has granted your app the necessary runtime permissions, use the
[`AudioManager`](https://developer.android.com/reference/kotlin/android/media/AudioManager) API to set the communication device to
[`TYPE_BLUETOOTH_SCO`](https://developer.android.com/reference/kotlin/android/media/AudioDeviceInfo#type_bluetooth_sco) to route the audio through Bluetooth HFP. This
directs the system to retrieve audio from the Bluetooth peripheral.
```kotlin
val audioManager = context.getSystemService(AudioManager::class.java) ?: return
val devices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
val hfpDevice = devices.find { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }
hfpDevice?.let { device ->
val audioRecord = AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
.setAudioFormat(audioFormat)
.setBufferSizeInBytes(bufferSize)
.build()
// Route recording to the Bluetooth device
audioRecord.setPreferredDevice(device)
audioManager.setCommunicationDevice(device)
audioRecord.startRecording()
```
<br />
> [!NOTE]
> **Note:** To learn more about Bluetooth connectivity, refer to our [documentation](https://developer.android.com/develop/connectivity/bluetooth).
## Capture an image with the glasses' camera
To capture an image with the glasses' camera, set up and bind the CameraX's
[`ImageCapture`](https://developer.android.com/reference/kotlin/androidx/camera/core/ImageCapture) [use case](https://developer.android.com/media/camera/camerax#ease-of-use) to the glasses' camera using the correct
context for your app:
```kotlin
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)
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()
// 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))
}
```
<br />
### Key points about the code
- Obtains an instance of the [`ProcessCameraProvider`](https://developer.android.com/reference/kotlin/androidx/camera/lifecycle/ProcessCameraProvider) using the [**projected device context**](https://developer.android.com/develop/xr/jetpack-xr-sdk/access-hardware-projected-context#phone-activity-service).
- Within the projected context's scope, the glasses' primary, outward-pointing camera maps to the `DEFAULT_BACK_CAMERA` when selecting a camera.
- A pre-binding check uses [`cameraProvider.hasCamera(cameraSelector)`](https://developer.android.com/reference/kotlin/androidx/camera/lifecycle/ProcessCameraProvider#hasCamera(androidx.camera.core.CameraSelector)) to verify that the selected camera is available on the device before proceeding.
- Uses **Camera2 Interop** with [`Camera2CameraInfo`](https://developer.android.com/reference/kotlin/androidx/camera/camera2/interop/Camera2CameraInfo) to read the underlying [`CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP`](https://developer.android.com/reference/kotlin/android/hardware/camera2/CameraCharacteristics#scaler_stream_configuration_map), which can be useful for advanced checks on supported resolutions.
- A custom [`ResolutionSelector`](https://developer.android.com/reference/kotlin/androidx/camera/core/resolutionselector/ResolutionSelector) is built to precisely control the output image resolution for [`ImageCapture`](https://developer.android.com/reference/kotlin/androidx/camera/core/ImageCapture).
- Creates an `ImageCapture` use case that is configured with a custom `ResolutionSelector`.
- Binds the `ImageCapture` use case to the activity's lifecycle. This automatically manages the opening and closing of the camera based on the activity's state (for example, stopping the camera when the activity is paused).
After the glasses' camera is set up, you can capture an image with the
CameraX's `ImageCapture` class. Refer to the CameraX's documentation to learn
about using [`takePicture`](https://developer.android.com/media/camera/camerax/take-photo#take_a_picture) to [capture an image](https://developer.android.com/media/camera/camerax/take-photo#take_a_picture).
### Capture a video with the glasses' camera
To capture a video instead of an image with the glasses' camera, replace the
`ImageCapture` components with the corresponding [`VideoCapture`](https://developer.android.com/reference/kotlin/androidx/camera/video/VideoCapture) components
and modify the capture execution logic.
The main changes involve using a different use case, creating a different output
file, and initiating the capture using the appropriate video recording method.
For more information about the `VideoCapture` API and how to use it, see the
[CameraX's video capture documentation](https://developer.android.com/media/camera/camerax/video-capture#using-videocapture-api).
> [!IMPORTANT]
> **Important:** Battery power and thermal dissipation on glasses devices is often limited. When developing for glasses, pay close attention to the requested **output format** and **resolution**, as these can severely impact system power and temperature.
The following table shows the recommended resolution and frame rate depending on
your app's use case:
| Use case | Resolution | Frame rate |
|---|---|---|
| Video Communication | 1280 x 720 | 15 FPS |
| Computer Vision | 640 x 480 | 10 FPS |
| AI Video Streaming | 640 x 480 | 1 FPS |
## Access a phone's hardware from a projected activity
An [projected activity](https://developer.android.com/develop/xr/jetpack-xr-sdk/glasses/support-different-types#activity-lifecycle) can also access the phone's hardware (such as the
camera or microphone) by using [`createHostDeviceContext(context)`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createHostDeviceContext(android.content.Context)) to get
the host device's (phone) context:
```kotlin
@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
}
}
```
<br />
When accessing hardware or resources that are specific to the host device
(phone) in a hybrid app (an app containing both mobile and glasses
experiences), you must explicitly select the correct context to make sure your
app can access the correct hardware:
- Use the [`Activity`](https://developer.android.com/reference/kotlin/android/app/Activity) context from the phone `Activity` or the [`ProjectedContext.createHostDeviceContext`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createHostDeviceContext(android.content.Context)) to get the phone's context.
- Don't use [`getApplicationContext`](https://developer.android.com/reference/kotlin/android/content/Context#getapplicationcontext) because the application context can incorrectly return the glasses' context if a projected activity was the most-recently-launched component.

View File

@@ -0,0 +1,100 @@
<br />
Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) Display Glasses [](https://developer.android.com/develop/xr/devices#audio-display) [Learn about XR device types →](https://developer.android.com/develop/xr/devices)
<br />
In Jetpack Compose Glimmer, a [`Button`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Button.composable) is an interactive component that's
optimized for display glasses input, offering clear visual feedback through its
states to guide user actions.
Buttons are built on the Jetpack Compose Glimmer [surface system](https://developer.android.com/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/surface), which
automatically handles physical properties like borders and depth.
The standard button contains a text label and optional icons. You can use it for
primary or secondary actions. There are also specialized buttons, such as [icon
buttons](https://developer.android.com/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/icon-buttons) and [toggle buttons](https://developer.android.com/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/toggle-buttons), which are defined as separate components in
Jetpack Compose Glimmer.
### Default
![](https://developer.android.com/static/images/design/ui/glasses/guides/glasses_components_buttons_anatomy_default.png) An example of some different styles of buttons in Jetpack Compose Glimmer. These examples show default, medium-sized buttons with three button states: Enabled (1), Focused (2), and Pressed (3).
### Large
![](https://developer.android.com/static/images/design/ui/glasses/guides/glasses_components_buttons_large.png) An example of some different styles of buttons in Jetpack Compose Glimmer. These examples show large-sized buttons with three button states: Enabled (1), Focused (2), and Pressed (3).
## Anatomy
A button consists of a container and a label, with optional leading and trailing
icons.
| Part | Description |
|---|---|
| Container | The background surface of the button. |
| Label | The text describing the action. |
| Icon (optional) | Leading or trailing visual indicators. |
## Sizes
Jetpack Compose Glimmer buttons support two size variants. These affect the
minimum height and internal padding.
| Size | Minimum height | Default usage |
|---|---|---|
| Medium | 48.dp | Standard actions and lists (default). |
| Large | 72.dp | High-emphasis actions or primary screen entry points. |
## States
Buttons in Jetpack Compose Glimmer change their appearance to communicate their
state.
- **Enabled**: The default state for an interactive button.
- **Focused** : When focused, the button applies a [`GlimmerTheme.depthEffectLevels.level1`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/DepthEffectLevels#level1()) and a focused border highlight.
- **Pressed**: When activated, a semi-transparent white overlay is applied to the surface.
- **Disabled**: The button doesn't respond to input and its visual appearance is adjusted.
## Button defaults
The following defaults apply to standard buttons:
- By default, buttons use [`GlimmerTheme.typography.bodySmall`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Typography#bodySmall()). Make sure that text within buttons is concise and clearly describes the action.
- The default shape for a button is [`GlimmerTheme.shapes.large`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Shapes#large()). This consistent rounding helps users identify buttons across the display glasses interface.
> [!NOTE]
> **Note:** Any modifier passed to the `Button` composable is applied to the outer layout. While [`ButtonSize`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/ButtonSize) sets the default minimum height, you can also apply custom size modifiers to control the button's final layout, such as [`Modifier.fillMaxWidth`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/fillMaxWidth.modifier).
## Example: Button with text
The following code creates a standard button with text:
```kotlin
@Composable
fun ButtonSample() {
Button(onClick = {}) { Text("Send") }
}
```
<br />
## Example: Buttons with leading and trailing icons
You can also add icons to the start (using `leadingIcon`) or end (using
`trailingIcon`) of the text to provide additional context.
The following code creates a button with a leading icon:
```kotlin
@Composable
fun ButtonWithLeadingIconSample() {
Button(onClick = {}, leadingIcon = { Icon(FavoriteIcon, "Localized description") }) {
Text("Send")
}
}
```
<br />

View File

@@ -0,0 +1,97 @@
<br />
Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) Display Glasses [](https://developer.android.com/develop/xr/devices#audio-display) [Learn about XR device types →](https://developer.android.com/develop/xr/devices)
<br />
In Jetpack Compose Glimmer, the [`Card`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable) component serves as the primary
container for related content, creating a clear visual boundary for digestible
units of information. Cards are highly adaptable, supporting combinations of
main content, optional titles, subtitles, and leading or trailing icons. Cards
fill the maximum available width by default, are focusable, and you can also
make them clickable. Cards support a vertical arrangement where the header image
is top-most, followed by the title, subtitle, and body content.
Cards are built on the Jetpack Compose Glimmer [surface system](https://developer.android.com/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/surface), so they
inherit physical properties like depth effects, clipping, and consistent border
highlights.
![](https://developer.android.com/static/images/design/ui/glasses/guides/glasses_components_cards.png) **Figure 1.** An example of some different styles of cards in Jetpack Compose Glimmer.
## Anatomy and slots
A Jetpack Compose Glimmer Card is built from several specialized elements that
let you customize its content and layout.
| Slot | Description |
|---|---|
| Header | The top section of the card, designed to hold an image. |
| Title and subtitle | These text fields provide the main and secondary labels for the card. The title is placed above the subtitle. |
| Leading icon | An optional icon (typically an [`Icon`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Icon.composable)) that appears at the beginning of the card's content area. |
| Trailing icon | An optional icon that appears at the end of the card's content area. |
| Action | A slot for a primary interactive element, such as a [`Button`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Button.composable). |
| Main content | The core body of the card for primary [`Text`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Text.composable) or content. |
## Card defaults
The following defaults apply to cards:
- **Width**: Cards fill the maximum available width of the display to optimize legibility on display glasses.
- **Minimum height** : `80.dp`
- **Text vertical spacing** : `3.dp`
- **Header shape** : Uses [`RoundedCornerShape`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/shape/package-summary#RoundedCornerShape(androidx.compose.ui.unit.Dp)) with `24.dp` corners
- **Content padding** : Defaults to [`GlimmerTheme.componentSpacingValues.medium`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/ComponentSpacingValues#medium()). This affects the outermost padding around header images and the content container.
- **Shape** : Defaults to [`GlimmerTheme.shapes.medium`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Shapes#medium()).
- **Text rendering** : Uses the default values from
[`GlimmerTheme.typography`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/GlimmerTheme#typography()) for the following slots:
- Title: [`bodyMedium`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Typography#bodyMedium())
- Subtitle: [`caption`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Typography#caption())
- Main content: [`bodySmall`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Typography#bodySmall())
## Example: Basic card
The following code creates a basic card:
```kotlin
@Composable
fun CardSample() {
Card { Text("This is a card") }
}
```
<br />
## Example: Complex card with multiple slots
The following code shows how to use multiple slots together to build a card.
```kotlin
@Composable
fun CardWithTitleAndLeadingIconAndHeaderAndAction() {
Card(
action = {
Button(onClick = {}, trailingIcon = { Icon(FavoriteIcon, "Localized description") }) {
Text("Send")
}
},
title = { Text("Title") },
leadingIcon = { Icon(FavoriteIcon, "Localized description") },
header = {
Image(MyHeaderImage, "Localized description", contentScale = ContentScale.FillWidth)
},
) {
Text("This is a card with a title, leading icon, header image, and action")
}
}
```
<br />
### Key points about the code
- Shows how to utilize various card elements, such as [`header`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable), [`title`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable), [`leadingIcon`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable), and [`action`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable), to customize the card's content and structure.
- Uses the standard [`Card`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function0)) overload with an action because only the card (or its internal action) needs to be focusable. If you need to make the entire card surface interactive, use the [`Card`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function0)) overload with `onClick` instead.
- This card uses an action slot, so the card surface isn't focusable, and focus is directed to the action button instead.

View File

@@ -0,0 +1,107 @@
<br />
Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) Display Glasses [](https://developer.android.com/develop/xr/devices#audio-display) [Learn about XR device types →](https://developer.android.com/develop/xr/devices)
<br />
All Jetpack Compose Glimmer components are designed to work with standard input
methods, such as a tap or swipe on the glasses' touchpad, while also being
receptive to lower-level input commands that are specific to the hardware on
display glasses. Jetpack Compose Glimmer components automatically handle the
necessary input events.
For standard actions like scroll and drag, use the Jetpack Compose Glimmer
components to promote a consistent experience. However, for custom components or
bespoke interaction behaviors, you can use existing Compose APIs like
[`Modifier.draggable`](https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier#(androidx.compose.ui.Modifier).draggable(androidx.compose.foundation.gestures.DraggableState,androidx.compose.foundation.gestures.Orientation,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Boolean,kotlin.coroutines.SuspendFunction2,kotlin.coroutines.SuspendFunction2,kotlin.Boolean)) or [`Modifier.scrollable`](https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier#(androidx.compose.ui.Modifier).scrollable(androidx.compose.foundation.gestures.ScrollableState,androidx.compose.foundation.gestures.Orientation,kotlin.Boolean,kotlin.Boolean,androidx.compose.foundation.gestures.FlingBehavior,androidx.compose.foundation.interaction.MutableInteractionSource)).
## Pointer input and focus
On display glasses, pointer input can affect focus:
- **Tap**: Direct interaction for activating element. Focus moves to an element when a user interacts with it.
- **Swipe**: Used for navigation and scrolling. Unhandled swipe gestures automatically translate into focus movements, enabling seamless UI navigation without direct pointer input.
<br />
> [!WARNING]
> **(Temporary requirement) Enable required input flag** :
>
> <br />
>
> Using a feature of Jetpack Compose, the system can automatically set the initial
> focus to the very-first focusable element when the screen loads, which is often
> the top-left item on the screen.
>
> This feature is still in development and isn't enabled by default. To activate
> this feature, set the `isInitialFocusOnFocusableAvailable` flag to `true` in
> your activity's [`onCreate()`](https://developer.android.com/reference/kotlin/android/app/Activity#onCreate(android.os.Bundle)) method.
>
> import androidx.compose.ui.ExperimentalComposeUiApi
> import androidx.compose.ui.ComposeUiFlags
>
> class GlassesActivityExample : ComponentActivity() {
> override fun onCreate(savedInstanceState: Bundle?) {
> @OptIn(ExperimentalComposeUiApi::class)
> ComposeUiFlags.isInitialFocusOnFocusableAvailable = true
> super.onCreate(savedInstanceState)
> }
> }
>
> <br />
>
<br />
## Navigation behavior and order
Focus movement and order change as a user navigates your app.
### Focus movement
On a scrollable container, focus moves continuously with a swipe on the
touchpad. For discrete elements like a row of buttons, each swipe moves the
focus one element at a time.
### Focus order
Just like in Jetpack Compose, Jetpack Compose Glimmer uses one-dimensional focus
search. To learn more about the order of focus traversal, see [Change focus
traversal order](https://developer.android.com/develop/ui/compose/touch-input/focus/change-focus-traversal-order#override-one-dimensional).
To change the initially-focused item, you can add a top-level
[`Modifier.focusGroup()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier#(androidx.compose.ui.Modifier).focusGroup()) and specify a custom `onEnter`
[`focusProperty`](https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier#(androidx.compose.ui.Modifier).focusProperties(kotlin.Function1)):
Modifier.focusProperties {
onEnter = {
initialFocus.requestFocus()
// Prevent focus from exiting the group
cancelFocusChange()
}
}
.focusGroup()
### Scrolling containers
For an optimal user experience, scrolling containers like lists should be the
only major component on a screen. Avoid placing a scrollable list directly above
or below other interactive elements, such as buttons, to prevent navigation
confusion and promote smooth, predictable focus movement.
## Default focus states
Jetpack Compose Glimmer implements default focus states across its interactable
components, including surfaces, cards, and list items, promoting consistent and
clear visual feedback during user interaction.
![](https://developer.android.com/static/images/design/ui/glasses/guides/glasses_ixd_inputs_focus.png) **Figure 1.** Three focus states in Jetpack Compose Glimmer, which are differentiated using outline-based visual feedback.
- **Default** : The button's background color is derived from
[`GlimmerTheme.colors.surface`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Colors#surface()), its main content calculates the content
color of that surface.
- **Focused**: The border width is increased to communicate focus.
- **Focused + Pressed** : The background is set to
`GlimmerTheme.colors.surface` at increased opacity to communicate its
selected state.

View File

@@ -0,0 +1,95 @@
<br />
Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) Display Glasses [](https://developer.android.com/develop/xr/devices#audio-display) [Learn about XR device types →](https://developer.android.com/develop/xr/devices)
<br />
In Jetpack Compose Glimmer, the [`Icon`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Icon.composable) component is a UI element for
rendering single-color icons. Icons intelligently handle tinting and scaling so
that they remain legible and visually consistent with the [`GlimmerTheme`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/GlimmerTheme.composable).
## Sizes
While icons default to the size provided by [`LocalIconSize`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/package-summary#LocalIconSize()), you can also
use the three icon sizes provided to set an specific size. These sizes are also
used by default for the following contexts:
| Size token | Default usage |
|---|---|
| `small` | For standard list items or small chips. |
| `medium` | For standalone icons and title chips. |
| `large` | For high-emphasis components like cards. |
## Icon sources
Icons can accept [`ImageVector`](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/vector/ImageVector), [`ImageBitmap`](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/ImageBitmap), or [`Painter`](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/painter/Painter) as
their source. When defining your own icons, use `ImageVector` where possible to
promote sharp rendering at any scale on display glasses.
## Color and Tinting
- **Automatic tint** : The icon resolves its color based on the `LocalContentColor` provided by the parent surface `LocalContentColor` provided by the parent surface, such as a [`surface`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/surface.composable) or [`Button`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Button.composable).
- **Manual Tinting** : Use the `tint` parameter to apply a specific color.
- **Multicolored Assets** : For icons that should not be tinted (like multicolored brand logos), set `tint = Color.Unspecified`.
- **Generic Images** : For photographs or generic images that don't follow icon sizing and tinting rules, use the standard [`androidx.compose.foundation.Image`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/Image.composable) instead.
## Example: Basic icon within a surface
The following code creates an icon placed inside a circular surface, utilizing
the theme's primary color:
```kotlin
@Composable
fun IconSampleUsage() {
GlimmerLazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
item { IconSizesSample() }
item {
Icon(
FavoriteIcon,
"Localized description",
Modifier.surface(
shape = CircleShape,
color = GlimmerTheme.colors.primary,
border = null,
)
.padding(12.dp),
)
}
}
}
```
<br />
## Example: Icons with different sizes
The following code demonstrates the different icon sizes:
```kotlin
@Composable
fun IconSizesSample() {
val iconSizes = GlimmerTheme.iconSizes
Column(
verticalArrangement = Arrangement.spacedBy(20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(FavoriteIcon, "Localized description", Modifier.size(iconSizes.small))
// medium is also the default size, defining explicitly for clarity
Icon(FavoriteIcon, "Localized description", Modifier.size(iconSizes.medium))
Icon(FavoriteIcon, "Localized description", Modifier.size(iconSizes.large))
}
}
```
<br />
### Key points about the code
- Each icon's size is customized using [`GlimmerTheme.iconSizes`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/IconSizes) with a modifier. For icons, the default value is [`GlimmerTheme.iconSizes.medium`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/IconSizes#medium()). Use these sizes instead of hard-coding values to maintain consistency across your UI.
- Provides a localized `contentDescription` for each icon. Always provide these descriptions unless the icon is purely decorative.

View File

@@ -0,0 +1,68 @@
<br />
Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) Display Glasses [](https://developer.android.com/develop/xr/devices#audio-display) [Learn about XR device types →](https://developer.android.com/develop/xr/devices)
<br />
In Jetpack Compose Glimmer, the [`Text`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Text.composable) component lets you set various text
properties like color, font size, font style, font weight, font family, letter
spacing, and text alignment.
Jetpack Compose Glimmer Text is unique because it intelligently manages color
matching. For example, if no color override is specified, the text defaults to
the content color provided by the nearest surface that it's sitting on.
## Example: Create a text heading in a box
@Composable
fun TextSample() {
Text(
text = "This is a sample heading",
style = GlimmerTheme.typography.titleLarge )
}
### Key points about the code
- Because no color is specified, this text looks at the nearest surface to pick the best readable color (usually white).
## Sizing
Typography scale in Jetpack Compose Glimmer is significantly larger than
standard mobile Material Design. Styles like `TitleLarge` and `BodyLarge` are
both `30.sp`, and the Caption is `18.sp`:
| Style | Size (sp) | Weight | Line Height |
|---|---|---|---|
| titleLarge | 30 | 750 | 36.sp |
| titleMedium | 24 | 750 | 28.sp |
| titleSmall | 20 | 750 | 28.sp |
| bodyLarge | 30 | 520 | 36.sp |
| bodyMedium | 24 | 520 | 36.sp |
| bodySmall | 20 | 520 | 28.sp |
| caption | 18 | 650 | 24.sp |
## Use Google Sans Flex
[Google Sans Flex](https://fonts.google.com/specimen/Google+Sans+Flex?query=google+sans&preview.script=Latn) is a variable font specifically chosen for AI
glasses that is provided as part of the Jetpack Compose Glimmer. The font's
rounded corners and adjustable axes allow for ideal optical sizing, ensuring
that text remains glanceable and legible. If possible, to improve consistency
for users between your app and the system, use Google Sans Flex for all text
displayed on display glasses.
To use Google Sans Flex, [add the `glimmer-google-fonts` library to your app's
dependencies](https://developer.android.com/develop/xr/jetpack-xr-sdk/set-up-sdk#augmented), then apply the font globally to the `GlimmerTheme`:
```kotlin
@Composable
fun GoogleSansFlexTypographySample() {
val typography = createGoogleSansFlexTypography()
GlimmerTheme(typography = typography) {
Text("Hello World", style = GlimmerTheme.typography.titleLarge)
}
}
```
<br />

View File

@@ -0,0 +1,63 @@
<br />
Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) Display Glasses [](https://developer.android.com/develop/xr/devices#audio-display) [Learn about XR device types →](https://developer.android.com/develop/xr/devices)
<br />
In Jetpack Compose Glimmer, the [`TitleChip`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/TitleChip.composable) component is a non-interactive
component that provides brief context or labeling for associated content, such
as a [`Card`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable) or a [`VerticalList`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/list/VerticalList.composable).
Use title chips for concise information like a short title, a category name, or
a status indicator. Normally, you should place title chips above the content
they describe to establish a clear structural relationship.
![](https://developer.android.com/static/images/design/ui/glasses/guides/glasses_components_titlechip_anatomy.png) **Figure 1.** An example of a default style title chip and sticky title chip in Jetpack Compose Glimmer. Each title chip has a label (1) and an optional leading icon or entity (2).
## Basic example: Create a short title chip
The following code creates a basic title chip:
```kotlin
@Composable
fun TitleChipSample() {
TitleChip { Text("Messages") }
}
```
<br />
## Example: Create a title chip with a card
To use a title chip with another component, place the title chip
[`TitleChipDefaults.associatedContentSpacing`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/TitleChipDefaults#associatedContentSpacing()) above the other component in
the composable. The following code creates a title chip with a card:
```kotlin
@Composable
fun TitleChipWithCardSample() {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
TitleChip { Text("Title Chip") }
Spacer(Modifier.height(TitleChipDefaults.associatedContentSpacing))
Card(
title = { Text("Title") },
subtitle = { Text("Subtitle") },
leadingIcon = { Icon(FavoriteIcon, "Localized description") },
) {
Text("Card Content")
}
}
}
```
<br />
### Key points about the code
- The title chip is centered horizontally, which is the usual alignment for a title chip placed above a card.
- The [`Spacer`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Spacer.composable#Spacer(androidx.compose.ui.Modifier)) has a fixed height to provide the right amount of vertical spacing between the two components. This is defined using [`TitleChipDefaults.associatedContentSpacing`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/TitleChipDefaults#associatedContentSpacing()).
- Uses an optional `leadingIcon` to add additional visual context before the text content.
- The title chip automatically uses the [`caption`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Typography#caption()) style for its text.
- The title chip isn't interactive. If you need a clickable label, use a [`Button`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Button.composable) or another interactive component.

View File

@@ -0,0 +1,178 @@
<br />
Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) Audio \&
Display Glasses [](https://developer.android.com/develop/xr/devices#audio-display) [Learn about XR device types →](https://developer.android.com/develop/xr/devices)
<br />
Just like on a phone, accessing sensitive hardware like the camera and
microphone on audio glasses and display glasses requires explicit user consent.
These are considered
**glasses-specific permissions**, and your app must request them at runtime,
even if it already has the corresponding permissions on the phone.
## Declare the permissions in your app's manifest
Before requesting permissions, you must [declare them in your app's manifest](https://developer.android.com/training/permissions/declaring)
file using the [`<uses-permission>`](https://developer.android.com/guide/topics/manifest/uses-permission-element) element. This declaration remains the
same whether the permission is for a phone or a glasses-specific feature, but
you must still explicitly request it for glasses-specific hardware or
functionality.
<manifest ...>
<!-- Only declare permissions that your app actually needs. In this example,
we declare permissions for the camera. -->
<uses-permission android:name="android.permission.CAMERA"/>
<application ...>
...
</application>
</manifest>
## Register the permissions launcher
To request permissions for audio glasses and display glasses, first you use the
[`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) with the [`ProjectedPermissionsResultContract`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsResultContract#ProjectedPermissionsResultContract())
method to register the permissions launcher.
```kotlin
// Register the permissions launcher using the ProjectedPermissionsResultContract.
private val requestPermissionLauncher: ActivityResultLauncher<List<ProjectedPermissionsRequestParams>> =
registerForActivityResult(ProjectedPermissionsResultContract()) { results ->
if (results[Manifest.permission.CAMERA] == true) {
isPermissionDenied = false
initializeGlassesFeatures()
} else {
// Handle permission denial.
isPermissionDenied = true
}
}
```
<br />
### Key points about the code
- The code creates an [`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) using the [`ProjectedPermissionsResultContract`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsResultContract#ProjectedPermissionsResultContract()) method. The callback receives a map of permission names to their granted status.
- You need to specify which permissions your app requires, such as [`Manifest.permission.CAMERA`](https://developer.android.com/reference/kotlin/android/Manifest.permission#camera) or [`Manifest.permission.RECORD_AUDIO`](https://developer.android.com/reference/kotlin/android/Manifest.permission#record_audio).
## Create the request function
Next, you'll create a function that uses your app's permissions launcher to
request the permissions from the user at runtime.
```kotlin
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))
}
```
<br />
### Key points about the code
- The `requestHardwarePermissions` function builds a [`ProjectedPermissionsRequestParams`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsRequestParams) object. This object bundles the list of permissions your app needs and the user-facing rationale. Make the rationale clear and concise to explain why your app needs these permissions.
- Calling `launch` on the launcher triggers the [permission request user
flow](https://developer.android.com/develop/xr/jetpack-xr-sdk/request-hardware-permissions#permissions-user-flow).
- Your app should handle both granted and denied results gracefully in the launcher's callback.
## Create the permissions check function
Next, you'll create a function that can check whether the user has granted
permissions to your app.
```kotlin
private fun hasCameraPermission(): Boolean {
return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) ==
PackageManager.PERMISSION_GRANTED
}
```
<br />
## Add the permission request logic
And lastly, create the logic that uses these functions to check for and request
the permissions at runtime.
```kotlin
if (hasCameraPermission()) {
initializeGlassesFeatures()
} else {
requestHardwarePermissions()
}
```
<br />
### Key points about the code
- If the user has already granted your app the required permissions, the `initializeGlassesFeatures` function is called to initialize your app's experience. This function is defined as [part of your app's activity for AI
glasses](https://developer.android.com/develop/xr/jetpack-xr-sdk/glasses/first-activity#create-activity).
## Understand the permission request user flow
When you launch a permission request using the
[`ProjectedPermissionsResultContract`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsResultContract#ProjectedPermissionsResultContract()) method, the system initiates a
coordinated user flow across both the glasses and the phone.
<br />
> [!IMPORTANT]
> **Important:** You should call the `ProjectedPermissionsResultContract` method from an [`Activity`](https://developer.android.com/reference/kotlin/android/app/Activity) displayed on the glasses. Don't use the standard Android permission APIs (such as [`requestPermissions`](https://developer.android.com/reference/kotlin/androidx/core/app/ActivityCompat#requestPermissions(android.app.Activity,%20java.lang.String%5B%5D,%20int)) with [`ActivityResultLauncher<String>`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher)) in code running on the glasses. Doing so attempts to launch a non-interactable permission dialog on the glasses, breaking the user flow.
>
> <br />
>
> If your app already has an `Activity` displayed on the phone, you should use
> [`Activity#requestPermissions(permissions, requestCode, deviceId)`](https://developer.android.com/reference/kotlin/android/app/Activity#requestpermissions_1), where
> the `deviceId` comes from calling the [`getDeviceId`](https://developer.android.com/reference/kotlin/android/content/Context#getdeviceid) method on the
> [`Context`](https://developer.android.com/reference/kotlin/android/content/Context) returned by calling
> [`ProjectedContext.createProjectedDeviceContext`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createProjectedDeviceContext(android.content.Context)).
>
> <br />
>
<br />
During the permissions user flow, here is what your app and the user can expect:
1. **On the glasses** : An activity appears on the **projected device
(glasses)**, instructing the user to look at their phone to continue.
<br />
> [!WARNING]
> **Preview:** Currently, the instructions and rationale provided by the system are not audible to the user. To provide an audible rationale to the user, we recommend using [Text to Speech
> (TTS)](https://developer.android.com/develop/xr/jetpack-xr-sdk/tts). For example:
>
> <br />
>
> tts?.speak("Please review the permission request on your host device",
> TextToSpeech.QUEUE_ADD,
> null,
> "permission_request")
>
> <br />
>
<br />
2. **On the phone** : Concurrently, an activity launches on the **host device
(phone)**. This screen displays the rationale string you provided and gives
the user the option to proceed or cancel.
3. **On the phone** : If the user accepts the rationale, a modified Android
system permission dialog appears on the phone telling the user that they are
granting the permission **for the glasses** (not the phone), and the user
can formally grant or deny the permission.
4. **Receiving the result** : After the user makes their final choice, the
activities on both the phone and glasses are dismissed. Your
[`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) callback is then invoked with a map containing
the granted status for each requested permission.