From fa1b1395feb7f2353308bc5cbcf28c24868692b4 Mon Sep 17 00:00:00 2001 From: leaiss Date: Thu, 28 May 2026 09:48:04 -0700 Subject: [PATCH] android(test app): MainActivity wrapper requests CAMERA at onCreate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thin Kotlin NativeActivity subclass whose only job is to surface the runtime CAMERA permission dialog. CNSDK's face tracker opens the front camera during xrCreateSession; without a runtime grant on Android 6+ the open silently fails and head tracking permanently uses the default centered eye position. The wrapper hands off to NativeActivity via super.onCreate immediately, so native init / xrCreateInstance run in parallel with the permission request — by the time the face tracker needs the camera, the user has already decided. Eliminates the `adb pm grant` step from the bring-up checklist's Step 0 for interactive day-1 use; pre-grant via adb remains documented for non-interactive bring-up (CI, scripted soak tests). Validated on the Android-36 emulator: GrantPermissionsActivity launched at start, full chain xrCreateInstance / xrCreateVulkanInstance / xrCreateVulkanDevice -> XR_SUCCESS unchanged, same expected emulator- Vulkan wall at VK device-extension check. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../android-bringup-checklist.md | 9 ++--- test_apps/cube_handle_vk_android/build.gradle | 7 ++++ .../src/main/AndroidManifest.xml | 13 ++++-- .../cube_handle_vk_android/MainActivity.kt | 40 +++++++++++++++++++ 4 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 test_apps/cube_handle_vk_android/src/main/java/com/displayxr/cube_handle_vk_android/MainActivity.kt diff --git a/docs/getting-started/android-bringup-checklist.md b/docs/getting-started/android-bringup-checklist.md index 8a086c648..a34c83d32 100644 --- a/docs/getting-started/android-bringup-checklist.md +++ b/docs/getting-started/android-bringup-checklist.md @@ -39,15 +39,12 @@ If A passes you can stop. B confirms the CNSDK convention assumptions. C is only The test app's `AndroidManifest.xml` declares `android.permission.CAMERA` (CNSDK front-camera face tracking) and `android.permission.WAKE_LOCK` (keep screen on during XR sessions). On Android 6+ `CAMERA` is a "dangerous" permission that requires either a user dialog OR an `adb pm grant`. -The test app is a thin `NativeActivity` with no Java/Kotlin wrapper to surface the runtime permission dialog. Two options for day-1: +The test app's `MainActivity` is a thin Kotlin wrapper around `NativeActivity` whose only job is to call `requestPermissions(CAMERA)` from `onCreate`. Day-1 path on Lume Pad: launch the app, tap **Allow** when the system permission dialog appears. The wrapper hands off to `NativeActivity` immediately, so the native init / `xrCreateInstance` runs in parallel — by the time CNSDK's face tracker opens the camera at session start, the grant is in place. + +For non-interactive bring-up (CI, scripted soak tests, no human at the device), pre-grant via adb: ```bash -# Option 1: pre-grant via adb (works on emulator + device-with-developer-mode) adb shell pm grant com.displayxr.cube_handle_vk_android android.permission.CAMERA - -# Option 2: tap "Allow" on the system permission dialog the first time -# face tracking tries to open the camera (CNSDK throws the dialog itself -# on first leia_core_enable_face_tracking call — confirm at A1). ``` **Missing CAMERA on Lume Pad symptom:** session reaches FOCUSED, app renders, but `xrLocateViews` always returns the default centered eye position regardless of head motion. No crash, no log error from CNSDK — just silently no face tracking. Check `adb shell dumpsys package com.displayxr.cube_handle_vk_android | grep CAMERA` to confirm grant. diff --git a/test_apps/cube_handle_vk_android/build.gradle b/test_apps/cube_handle_vk_android/build.gradle index d6ed0382a..c083d4ac9 100644 --- a/test_apps/cube_handle_vk_android/build.gradle +++ b/test_apps/cube_handle_vk_android/build.gradle @@ -9,6 +9,7 @@ plugins { id 'com.android.application' + id 'org.jetbrains.kotlin.android' } android { @@ -76,6 +77,10 @@ android { targetCompatibility JavaVersion.VERSION_17 } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 + } + packagingOptions { jniLibs { // libopenxr_loader.so ships in the AAR; keep it as-is @@ -85,6 +90,8 @@ android { } dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + // Khronos OpenXR loader for Android — bundles libopenxr_loader.so + // openxr/*.h headers via prefab. The loader discovers our runtime // APK at runtime via the OpenXRRuntimeService intent declared in diff --git a/test_apps/cube_handle_vk_android/src/main/AndroidManifest.xml b/test_apps/cube_handle_vk_android/src/main/AndroidManifest.xml index d4bb09c52..70b3e8e36 100644 --- a/test_apps/cube_handle_vk_android/src/main/AndroidManifest.xml +++ b/test_apps/cube_handle_vk_android/src/main/AndroidManifest.xml @@ -58,11 +58,18 @@ + android:label="DisplayXR Cube VK"> + diff --git a/test_apps/cube_handle_vk_android/src/main/java/com/displayxr/cube_handle_vk_android/MainActivity.kt b/test_apps/cube_handle_vk_android/src/main/java/com/displayxr/cube_handle_vk_android/MainActivity.kt new file mode 100644 index 000000000..ea8d366c8 --- /dev/null +++ b/test_apps/cube_handle_vk_android/src/main/java/com/displayxr/cube_handle_vk_android/MainActivity.kt @@ -0,0 +1,40 @@ +// Copyright 2026, Leia Inc. +// SPDX-License-Identifier: BSL-1.0 +// +// Thin NativeActivity wrapper whose only job is to surface the runtime +// CAMERA permission dialog at first launch. CNSDK's face tracker +// (libleiaSDK-faceTrackingInApp.so, loaded into this process by the +// DisplayXR runtime broker) opens the front camera during +// xrCreateSession. On Android 6+ a manifest is not +// sufficient for "dangerous" perms — the user must grant at runtime. +// Without this prompt, the camera open silently fails and head +// tracking permanently uses a default centered eye position. +// +// We delegate everything else (window setup, ANativeActivity glue, +// android_main thread spawn) to NativeActivity by calling +// super.onCreate first; the permission request is async and happens +// in parallel with the native init. If the user grants in time, the +// first xrCreateSession picks up the permission. If they deny or +// haven't decided yet, CNSDK degrades to no-face state — same as +// pre-wrapper behavior, but at least the dialog appeared. + +package com.displayxr.cube_handle_vk_android + +import android.Manifest +import android.app.NativeActivity +import android.content.pm.PackageManager +import android.os.Bundle + +class MainActivity : NativeActivity() { + + companion object { + private const val REQUEST_CAMERA = 1 + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(arrayOf(Manifest.permission.CAMERA), REQUEST_CAMERA) + } + } +}