skill: add Android skills
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
Glasses have multiple sources and types of interaction inputs, including
|
||||
hardware and touch, physical gesture, and voice.
|
||||
|
||||
## Hardware controls
|
||||
|
||||
Glasses controls can vary depending on device model, but most will include a
|
||||
camera button, touchpad, display button, and power switch or button. Hardware
|
||||
controls have default interactions, and some can be remapped for your app.
|
||||
Consider that inputs for glasses are more 1-dimensional, where users can make
|
||||
one control input at a time, compared to touchscreen inputs. The glasses
|
||||
hardware controls have default interaction mapping, some of which are handled by
|
||||
the app, and others that are handled by the system.
|
||||
|
||||
**The system will handle the following inputs:**
|
||||
|
||||
- Camera button single press to take photos and camera button hold to take videos
|
||||
- Swiping with two fingers on the touchpad for volume
|
||||
- Touch \& hold on the touchpad and the wake word on the microphone will launch Gemini
|
||||
- System interprets swipe down on display glasses as system back.
|
||||
|
||||
**Your app has access to the glasses:**
|
||||
|
||||
- Microphone
|
||||
- Point-of-View (POV) camera
|
||||
- Inertial Measurement Unit (IMU)
|
||||
- Camera button double-press
|
||||
|
||||

|
||||
|
||||
### AI Glasses
|
||||
|
||||
|
||||
| Input | Gesture | Interaction affect |
|
||||
|---|---|---|
|
||||
| Touchpad (1) | Tap | Play / Pause / Confirm |
|
||||
| | Swipe | Next/Previous/Dismiss |
|
||||
| | Swipe down | N/A |
|
||||
| | Touch \& hold | Invoke AI |
|
||||
| | 2-finger swipe | Volume |
|
||||
| Display button (2) | Press | N/A |
|
||||
| Camera button (3) | Press | Photo / Video end |
|
||||
| | Touch \& hold | Video start |
|
||||
| | Double press | Camera |
|
||||
|
||||
<br />
|
||||
|
||||
### AI Glasses with Display
|
||||
|
||||
|
||||
| Input | Gesture | Interaction affect |
|
||||
|---|---|---|
|
||||
| Touchpad (1) | Tap | Play / Pause / Confirm |
|
||||
| | Swipe | UI Navigation |
|
||||
| | Swipe down | Back |
|
||||
| | Touch \& hold | Invoke AI |
|
||||
| | 2-finger swipe | Volume |
|
||||
| Display button (2) | Press | Wake/Sleep |
|
||||
| Camera button (3) | Press | Photo / Video end |
|
||||
| | Touch \& hold | Video start |
|
||||
| | Double press | Camera |
|
||||
|
||||
<br />
|
||||
|
||||
 On
|
||||
display focus states have a visual affordances.
|
||||
@@ -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)  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.
|
||||
@@ -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)  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
|
||||
|
||||
 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
|
||||
|
||||
 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 />
|
||||
@@ -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)  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.
|
||||
 **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.
|
||||
@@ -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)  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.
|
||||
 **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.
|
||||
@@ -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)  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.
|
||||
@@ -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)  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 />
|
||||
@@ -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)  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.
|
||||
 **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.
|
||||
@@ -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)  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.
|
||||
Reference in New Issue
Block a user