Skip to content

Commit ee87a44

Browse files
Adds the XrServiceAvailability API to the XR Runtime library
Bug: 493558010 Test: XrServiceAvailabilityTest Relnote: Added the `XrServiceAvailability` API Change-Id: If379eefa6e058cd4c90c7efc5e27787e79350ecc
1 parent 32b3531 commit ee87a44

6 files changed

Lines changed: 241 additions & 1 deletion

File tree

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ core = "1.12.0"
8484
xmlApis = "1.4.01"
8585
yarn = "1.22.17"
8686
extensionsXr = "1.3.0"
87+
xrProjectedMinApiVersion = "260316000"
8788

8889
[libraries]
8990
agpTestingPlatformCoreProto = { module = "com.google.testing.platform:core-proto", version = "0.0.8-alpha08" }

xr/runtime/runtime/api/current.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,26 @@ package androidx.xr.runtime {
427427
property public androidx.xr.runtime.XrLog.Level WARN;
428428
}
429429

430+
@SuppressCompatibility @androidx.xr.runtime.ExperimentalXrServiceAvailabilityApi public final class XrServiceAvailability {
431+
method @SuppressCompatibility @androidx.xr.runtime.ExperimentalXrServiceAvailabilityApi public static androidx.xr.runtime.XrServiceAvailability.XrServiceAvailabilityStatus checkProjectedServiceAvailability(android.content.Context context);
432+
field public static final androidx.xr.runtime.XrServiceAvailability INSTANCE;
433+
}
434+
435+
@SuppressCompatibility @androidx.xr.runtime.ExperimentalXrServiceAvailabilityApi public static final class XrServiceAvailability.XrServiceAvailabilityStatus {
436+
field public static final androidx.xr.runtime.XrServiceAvailability.XrServiceAvailabilityStatus AVAILABLE;
437+
field public static final androidx.xr.runtime.XrServiceAvailability.XrServiceAvailabilityStatus.Companion Companion;
438+
field public static final androidx.xr.runtime.XrServiceAvailability.XrServiceAvailabilityStatus OUTDATED;
439+
field public static final androidx.xr.runtime.XrServiceAvailability.XrServiceAvailabilityStatus UNKNOWN;
440+
field public static final androidx.xr.runtime.XrServiceAvailability.XrServiceAvailabilityStatus UNSUPPORTED;
441+
}
442+
443+
@SuppressCompatibility @androidx.xr.runtime.ExperimentalXrServiceAvailabilityApi public static final class XrServiceAvailability.XrServiceAvailabilityStatus.Companion {
444+
property public androidx.xr.runtime.XrServiceAvailability.XrServiceAvailabilityStatus AVAILABLE;
445+
property public androidx.xr.runtime.XrServiceAvailability.XrServiceAvailabilityStatus OUTDATED;
446+
property public androidx.xr.runtime.XrServiceAvailability.XrServiceAvailabilityStatus UNKNOWN;
447+
property public androidx.xr.runtime.XrServiceAvailability.XrServiceAvailabilityStatus UNSUPPORTED;
448+
}
449+
430450
}
431451

432452
package androidx.xr.runtime.math {

xr/runtime/runtime/api/restricted_current.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,26 @@ package androidx.xr.runtime {
594594
property public androidx.xr.runtime.XrLog.Level WARN;
595595
}
596596

597+
@SuppressCompatibility @androidx.xr.runtime.ExperimentalXrServiceAvailabilityApi public final class XrServiceAvailability {
598+
method @SuppressCompatibility @androidx.xr.runtime.ExperimentalXrServiceAvailabilityApi public static androidx.xr.runtime.XrServiceAvailability.XrServiceAvailabilityStatus checkProjectedServiceAvailability(android.content.Context context);
599+
field public static final androidx.xr.runtime.XrServiceAvailability INSTANCE;
600+
}
601+
602+
@SuppressCompatibility @androidx.xr.runtime.ExperimentalXrServiceAvailabilityApi public static final class XrServiceAvailability.XrServiceAvailabilityStatus {
603+
field public static final androidx.xr.runtime.XrServiceAvailability.XrServiceAvailabilityStatus AVAILABLE;
604+
field public static final androidx.xr.runtime.XrServiceAvailability.XrServiceAvailabilityStatus.Companion Companion;
605+
field public static final androidx.xr.runtime.XrServiceAvailability.XrServiceAvailabilityStatus OUTDATED;
606+
field public static final androidx.xr.runtime.XrServiceAvailability.XrServiceAvailabilityStatus UNKNOWN;
607+
field public static final androidx.xr.runtime.XrServiceAvailability.XrServiceAvailabilityStatus UNSUPPORTED;
608+
}
609+
610+
@SuppressCompatibility @androidx.xr.runtime.ExperimentalXrServiceAvailabilityApi public static final class XrServiceAvailability.XrServiceAvailabilityStatus.Companion {
611+
property public androidx.xr.runtime.XrServiceAvailability.XrServiceAvailabilityStatus AVAILABLE;
612+
property public androidx.xr.runtime.XrServiceAvailability.XrServiceAvailabilityStatus OUTDATED;
613+
property public androidx.xr.runtime.XrServiceAvailability.XrServiceAvailabilityStatus UNKNOWN;
614+
property public androidx.xr.runtime.XrServiceAvailability.XrServiceAvailabilityStatus UNSUPPORTED;
615+
}
616+
597617
}
598618

599619
package androidx.xr.runtime.internal {

xr/runtime/runtime/build.gradle

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,15 @@ dependencies {
5252

5353
android {
5454
namespace = "androidx.xr.runtime"
55-
defaultConfig.consumerProguardFiles("proguard-rules.pro")
55+
buildFeatures {
56+
resValues true
57+
}
58+
defaultConfig {
59+
consumerProguardFiles("proguard-rules.pro")
60+
61+
def xrProjectedMinApiVersion = libs.versions.xrProjectedMinApiVersion.get()
62+
resValue "string", "xr_projected_min_api_version", xrProjectedMinApiVersion
63+
}
5664
}
5765

5866
androidx {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package androidx.xr.runtime
18+
19+
import android.content.Context
20+
import android.os.Build
21+
22+
/** Provides information about the XR API availability. */
23+
@ExperimentalXrServiceAvailabilityApi
24+
public object XrServiceAvailability {
25+
/** Includes all supported states for the XR service availability. */
26+
@ExperimentalXrServiceAvailabilityApi
27+
public class XrServiceAvailabilityStatus private constructor(private val code: Int) {
28+
public companion object {
29+
/** XR service availability is unknown. */
30+
@JvmField
31+
public val UNKNOWN: XrServiceAvailabilityStatus = XrServiceAvailabilityStatus(0)
32+
/** XR service is available, and it's in the most recent version. */
33+
@JvmField
34+
public val AVAILABLE: XrServiceAvailabilityStatus = XrServiceAvailabilityStatus(1)
35+
/** XR service is unsupported on the device. */
36+
@JvmField
37+
public val UNSUPPORTED: XrServiceAvailabilityStatus = XrServiceAvailabilityStatus(2)
38+
/**
39+
* XR service available on the device is older than the library API used by the client
40+
* application.
41+
*/
42+
@JvmField
43+
public val OUTDATED: XrServiceAvailabilityStatus = XrServiceAvailabilityStatus(3)
44+
}
45+
}
46+
47+
/**
48+
* Returns the [XrServiceAvailabilityStatus] of Projected XR features support.
49+
*
50+
* @param context any [Context] object.
51+
*/
52+
@JvmStatic
53+
@ExperimentalXrServiceAvailabilityApi
54+
public fun checkProjectedServiceAvailability(context: Context): XrServiceAvailabilityStatus {
55+
if (isProjectedServiceUnsupported(context)) {
56+
return XrServiceAvailabilityStatus.UNSUPPORTED
57+
}
58+
return try {
59+
if (isProjectedServiceOutdated(context)) {
60+
XrServiceAvailabilityStatus.OUTDATED
61+
} else {
62+
XrServiceAvailabilityStatus.AVAILABLE
63+
}
64+
} catch (_: IllegalStateException) {
65+
XrServiceAvailabilityStatus.UNSUPPORTED
66+
}
67+
}
68+
69+
private fun isProjectedServiceUnsupported(context: Context) =
70+
!PackageManagerUtils.hasXrProjectedSystemFeature(context)
71+
72+
private fun isProjectedServiceOutdated(context: Context): Boolean {
73+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
74+
throw IllegalStateException("Service unsupported on builds older than Android U")
75+
}
76+
val serviceApiVersion = PackageManagerUtils.getProjectedPlatformApiVersion(context)
77+
val clientApiVersion = context.getString(R.string.xr_projected_min_api_version).toInt()
78+
79+
return serviceApiVersion < clientApiVersion
80+
}
81+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package androidx.xr.runtime
18+
19+
import android.app.Application
20+
import android.content.ComponentName
21+
import android.content.IntentFilter
22+
import android.content.pm.ApplicationInfo
23+
import android.content.pm.PackageInfo
24+
import android.content.pm.ServiceInfo
25+
import android.os.Build
26+
import androidx.test.core.app.ApplicationProvider
27+
import androidx.test.ext.junit.runners.AndroidJUnit4
28+
import androidx.xr.runtime.PackageManagerUtils.XR_PROJECTED_SYSTEM_FEATURE
29+
import com.google.common.truth.Truth.assertThat
30+
import org.junit.Before
31+
import org.junit.Test
32+
import org.junit.runner.RunWith
33+
import org.robolectric.Shadows.shadowOf
34+
import org.robolectric.annotation.Config
35+
36+
@RunWith(AndroidJUnit4::class)
37+
@OptIn(ExperimentalXrServiceAvailabilityApi::class)
38+
class XrServiceAvailabilityTest {
39+
40+
private lateinit var context: Application
41+
42+
@Before
43+
fun setUp() {
44+
context = ApplicationProvider.getApplicationContext()
45+
}
46+
47+
@Test
48+
fun checkProjectedServiceAvailability_whenFeatureMissing_returnsUnsupported() {
49+
shadowOf(context.packageManager).setSystemFeature(XR_PROJECTED_SYSTEM_FEATURE, false)
50+
51+
assertThat(XrServiceAvailability.checkProjectedServiceAvailability(context))
52+
.isEqualTo(XrServiceAvailability.XrServiceAvailabilityStatus.UNSUPPORTED)
53+
}
54+
55+
@Test
56+
@Config(sdk = [Build.VERSION_CODES.TIRAMISU])
57+
fun checkProjectedServiceAvailability_whenSdkTooLow_returnsUnsupported() {
58+
shadowOf(context.packageManager).setSystemFeature(XR_PROJECTED_SYSTEM_FEATURE, true)
59+
60+
assertThat(XrServiceAvailability.checkProjectedServiceAvailability(context))
61+
.isEqualTo(XrServiceAvailability.XrServiceAvailabilityStatus.UNSUPPORTED)
62+
}
63+
64+
@Test
65+
@Config(minSdk = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
66+
fun checkProjectedServiceAvailability_whenServiceMissing_returnsUnsupported() {
67+
shadowOf(context.packageManager).setSystemFeature(XR_PROJECTED_SYSTEM_FEATURE, true)
68+
69+
// No service set up for ACTION_BIND, so PackageManagerUtils.getProjectedPlatformApiVersion
70+
// will throw.
71+
assertThat(XrServiceAvailability.checkProjectedServiceAvailability(context))
72+
.isEqualTo(XrServiceAvailability.XrServiceAvailabilityStatus.UNSUPPORTED)
73+
}
74+
75+
@Test
76+
@Config(minSdk = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
77+
fun checkProjectedServiceAvailability_whenServicePresentButPropertyMissing_returnsUnsupported() {
78+
shadowOf(context.packageManager).setSystemFeature(XR_PROJECTED_SYSTEM_FEATURE, true)
79+
shadowOf(context.packageManager).apply {
80+
addServiceIfNotPresent(PROJECTED_SERVICE_COMPONENT_NAME)
81+
addIntentFilterForService(
82+
PROJECTED_SERVICE_COMPONENT_NAME,
83+
IntentFilter(PackageManagerUtils.ACTION_BIND),
84+
)
85+
installPackage(PROJECTED_PACKAGE_INFO)
86+
}
87+
88+
assertThat(XrServiceAvailability.checkProjectedServiceAvailability(context))
89+
.isEqualTo(XrServiceAvailability.XrServiceAvailabilityStatus.UNSUPPORTED)
90+
}
91+
92+
companion object {
93+
private const val PROJECTED_SERVICE_PACKAGE_NAME = "com.system.service"
94+
private const val PROJECTED_SERVICE_CLASS_NAME = "com.system.service.ProjectedService"
95+
96+
private val PROJECTED_SERVICE_COMPONENT_NAME: ComponentName =
97+
ComponentName(PROJECTED_SERVICE_PACKAGE_NAME, PROJECTED_SERVICE_CLASS_NAME)
98+
private val PROJECTED_SERVICE_INFO =
99+
ServiceInfo().apply {
100+
packageName = PROJECTED_SERVICE_PACKAGE_NAME
101+
name = PROJECTED_SERVICE_CLASS_NAME
102+
}
103+
private val PROJECTED_PACKAGE_INFO =
104+
PackageInfo().apply {
105+
packageName = PROJECTED_SERVICE_PACKAGE_NAME
106+
services = arrayOf(PROJECTED_SERVICE_INFO)
107+
applicationInfo = ApplicationInfo().apply { flags = ApplicationInfo.FLAG_SYSTEM }
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)