Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions xr/src/main/java/com/example/xr/input/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
}

android {
namespace = "com.example.aiglasses.camera"
compileSdk = 36

defaultConfig {
applicationId = "com.example.aiglasses.camera"
minSdk = 36
targetSdk = 36
versionCode = 1
versionName = "1.0"
}

buildFeatures {
compose = true
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = "17"
freeCompilerArgs += listOf(
"-opt-in=androidx.xr.projected.experimental.ExperimentalProjectedApi"
)
}

packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}

dependencies {
// ProGuard minification extension library required for Jetpack XR alpha05+
compileOnly("com.android.extensions.xr:extensions-xr:1.1.0")

// Android XR & AI Glasses libraries
implementation("androidx.xr.runtime:runtime:1.0.0-alpha10")
implementation("androidx.xr.glimmer:glimmer:1.0.0-alpha07")
implementation("androidx.xr.projected:projected:1.0.0-alpha04")
implementation("androidx.xr.arcore:arcore:1.0.0-alpha10")

// Jetpack Compose
val composeBom = platform("androidx.compose:compose-bom:2024.12.01")
implementation(composeBom)
implementation("androidx.activity:activity-compose:1.9.3")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")

// Core utilities
implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.HEAD_TRACKING" />
<uses-permission android:name="android.permission.HAND_TRACKING" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

<application
android:allowBackup="true"
android:label="Input Samples"
android:supportsRtl="true"
android:theme="@android:style/Theme.DeviceDefault">

<!-- Phone Companion Entry Activity (Runs on Phone Display) -->
<activity
android:name=".MainActivity"
android:exported="true"
android:label="Input Samples">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<!-- Projected Activity for AI Glasses (Runs inside Projected Glasses Context) -->
<!-- requiredDisplayCategory="xr_projected" tells Android XR to launch this activity inside the projected AI Glasses context -->
<activity
android:name=".HelloCameraActionActivity"
android:exported="true"
android:launchMode="singleTask"
android:taskAffinity="com.example.aiglasses.camera.cameraaction"
android:requiredDisplayCategory="android.hardware.display.category.XR_PROJECTED"
android:label="AI Glasses Camera Action">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.XR_PROJECTED_LAUNCHER" />
</intent-filter>
<!-- Explicit intent filters for ALL camera capture & button actions so the system knows this app supports the camera! -->
<intent-filter>
<action android:name="android.media.action.IMAGE_CAPTURE" />
<action android:name="android.media.action.IMAGE_CAPTURE_SECURE" />
<action android:name="android.media.action.STILL_IMAGE_CAMERA" />
<action android:name="android.media.action.STILL_IMAGE_CAMERA_SECURE" />
<action android:name="android.intent.action.CAMERA_BUTTON" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>

<!-- Projected Activity for AI Glasses Motion Gesture Sample -->
<activity
android:name=".HelloMotionGestureActivity"
android:exported="true"
android:launchMode="singleTask"
android:taskAffinity="com.example.aiglasses.camera.motiongesture"
android:requiredDisplayCategory="android.hardware.display.category.XR_PROJECTED"
android:label="Hello Motion Gesture">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.XR_PROJECTED_LAUNCHER" />
</intent-filter>
</activity>

<!-- Projected Activity for AI Glasses Back Gesture Sample -->
<activity
android:name=".HelloBackGestureActivity"
android:exported="true"
android:launchMode="singleTask"
android:taskAffinity="com.example.aiglasses.camera.backgesture"
android:requiredDisplayCategory="android.hardware.display.category.XR_PROJECTED"
android:label="Hello Back Gesture">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.XR_PROJECTED_LAUNCHER" />
</intent-filter>
</activity>

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package com.example.aiglasses.camera

import android.media.AudioAttributes
import android.media.AudioManager
import android.os.Bundle
import android.speech.tts.TextToSpeech
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.lifecycleScope
import androidx.xr.projected.ProjectedContext
import androidx.xr.projected.ProjectedDisplayController
import androidx.xr.projected.ProjectedDisplayController.PresentationMode.Companion.VISUALS_ON
import kotlinx.coroutines.launch
import java.util.Locale

/**
* Base activity for AI Glasses projected samples (`CameraAction`, `MotionGesture`, `BackGesture`).
*
* Encapsulates non-sample-specific hardware boilerplate and state management:
* - Initializes [TextToSpeech] using [ProjectedDeviceContext] (`USAGE_ASSISTANT`) for glasses speaker routing.
* - Observes [ProjectedDisplayController] presentation modes (`VISUALS_ON`) into [isDisplayOn].
* - Holds common UI state ([notificationText], [gestureSourceText]) so samples don't need to declare them.
* - Provides [triggerFeedback] for unified visual chips, [Toast], and audio [TextToSpeech] notifications.
*/
abstract class BaseProjectedActivity(val sampleTitle: String) : ComponentActivity() {

companion object {
private const val BASE_TAG = "BaseProjectedActivity"
}

/** Observable Compose state indicating whether the projected display currently has visuals active. */
val isDisplayOn = mutableStateOf(true)

/** Observable notification message string for Glimmer UI rendering. */
val notificationText = mutableStateOf<String?>(null)

/** Observable source description (e.g., "Touchpad Tap", "ACTION_DOWN") for Glimmer UI rendering. */
val gestureSourceText = mutableStateOf<String?>(null)

/** Counter incremented on every triggerFeedback invocation so UI reacts even for repeated identical messages. */
val feedbackEventCount = mutableStateOf(0)

private var tts: TextToSpeech? = null
private var displayController: ProjectedDisplayController? = null
private var lastSpokenTimestamp = 0L

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initializeTextToSpeech()
observeDisplayState()
}

private fun initializeTextToSpeech() {
val ttsContext = try {
ProjectedContext.createProjectedDeviceContext(this)
} catch (e: Exception) {
Log.w(BASE_TAG, "Could not create ProjectedDeviceContext for TTS, falling back to activity context: ${e.message}")
this
}
tts = TextToSpeech(ttsContext) { status ->
if (status == TextToSpeech.SUCCESS) {
tts?.language = Locale.getDefault()
val attributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build()
tts?.setAudioAttributes(attributes)
onTextToSpeechReady()
} else {
Log.e(BASE_TAG, "TextToSpeech initialization failed.")
}
}
}

protected open fun onTextToSpeechReady() {}

private fun observeDisplayState() {
lifecycleScope.launch {
try {
val controller = ProjectedDisplayController.create(this@BaseProjectedActivity)
displayController = controller
controller.addPresentationModeChangedListener { flags ->
val visualsOn = flags.hasPresentationMode(VISUALS_ON)
isDisplayOn.value = visualsOn
Log.d(BASE_TAG, "Display state changed. Visuals On: $visualsOn")
}
} catch (e: Exception) {
Log.w(BASE_TAG, "ProjectedDisplayController not available: ${e.message}")
}
}
}

/**
* Updates observable UI states ([notificationText], [gestureSourceText]) and triggers
* a spoken and visual [Toast] notification on the AI Glasses.
*
* @param message The text string to speak and display.
* @param source Description of the trigger source (e.g., "System Back", "Touchpad Swipe").
* @param utteranceId Identifier for the TTS utterance.
* @param throttleMs Minimum milliseconds between consecutive speeches.
*/
fun triggerFeedback(
message: String,
source: String,
utteranceId: String = "projected_utterance",
throttleMs: Long = 0L,
speakAudio: Boolean = true
) {
val now = System.currentTimeMillis()
if (throttleMs > 0L && now - lastSpokenTimestamp < throttleMs) {
return
}
lastSpokenTimestamp = now

notificationText.value = message
gestureSourceText.value = source
feedbackEventCount.value = feedbackEventCount.value + 1

Toast.makeText(this, "$message ($source)", Toast.LENGTH_SHORT).show()
if (speakAudio) {
tts?.speak(message, TextToSpeech.QUEUE_FLUSH, null, utteranceId)
}
}

override fun onDestroy() {
super.onDestroy()
displayController?.close()
tts?.stop()
tts?.shutdown()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.example.aiglasses.camera

import android.os.Bundle
import android.util.Log
import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent
import androidx.xr.glimmer.GlimmerTheme

/**
* Demonstrates how to intercept and handle system back gestures
* from a Projected Activity running on AI Glasses.
*/
class HelloBackGestureActivity : BaseProjectedActivity("Back Gestures") {

// [START androidxr_projected_back_gesture_callback]
private val backCallback =
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
showExitConfirmation()
}
}
// [END androidxr_projected_back_gesture_callback]

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// [START androidxr_projected_back_gesture_register]
onBackPressedDispatcher.addCallback(this, backCallback)
// [END androidxr_projected_back_gesture_register]

setContent {
GlimmerTheme {
ProjectedSampleScreen(
activity = this@HelloBackGestureActivity,
subtitle = "System Back Intercept Enabled",
consumeSwipeBackward = false,
onGestureAction = {}
)
}
}
}

private fun showExitConfirmation() {
Log.i("HelloBackGesture", "System Back gesture received")
triggerFeedback(
"Back gesture detected.",
"System Back (handleOnBackPressed)",
"back_gesture",
throttleMs = 1000L
)
}

private fun setExitConfirmationVisible(visible: Boolean) {
backCallback.isEnabled = visible
}
}



Loading