From a5c2cf99d8dd127b77aa6c0bec08a8d6dbba2573 Mon Sep 17 00:00:00 2001 From: ethan zhou <231755529+ethanzhoucool@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:03:06 -0700 Subject: [PATCH] fix(playscan): match Android XR by namespace instead of invented names featureFormFactors mapped XR to android.software.xr.immersive and android.hardware.xr.head_tracking. Neither is a real feature constant. I made them up, so no XR app ever matched and XR packages kept getting the phone track's blocking target API finding, which is the exact false CRITICAL the form-factor work was meant to remove. The regression test used the same invented names, so it could not catch it. No XR constant appears in PackageManager's constant pool in android.jar for API 34, 35 or 36, so an exact name cannot be verified the way the other four were. Rather than swap one unverifiable guess for another, XR is now matched on its namespace: a required feature under android.software.xr. or android.hardware.xr. marks the app as XR. That covers api.spatial, api.openxr, the hardware.xr.* inputs, and anything added later. The other four names are unchanged and were confirmed by reading PackageManager's constant pool out of android.jar on all three platform versions: type.watch, type.television, software.leanback, type.automotive. Tests now use real XR feature names, and cover the namespace boundary (android.software.xrated and com.example.xr.custom stay phone) and required="false". Verified against an aapt2-linked XR APK: targetSdk 33 with a required api.spatial feature reports WARN naming Android XR, not CRITICAL. Reported by Cursor Bugbot on #22. --- internal/playscan/manifest.go | 33 ++++++++++++++++++----- internal/playscan/rules_test.go | 46 ++++++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/internal/playscan/manifest.go b/internal/playscan/manifest.go index 2b4ec09..741bd95 100644 --- a/internal/playscan/manifest.go +++ b/internal/playscan/manifest.go @@ -63,13 +63,29 @@ const ( // featureFormFactors maps the declaration that puts an app on a // non-phone Play track to that form factor. +// +// Every name here was read back from PackageManager's constant pool in +// android.jar on android-34, -35 and -36, which all agree. var featureFormFactors = map[string]FormFactor{ - "android.hardware.type.watch": FormFactorWear, - "android.hardware.type.television": FormFactorTV, - "android.software.leanback": FormFactorTV, - "android.hardware.type.automotive": FormFactorAutomotive, - "android.software.xr.immersive": FormFactorXR, - "android.hardware.xr.head_tracking": FormFactorXR, + "android.hardware.type.watch": FormFactorWear, + "android.hardware.type.television": FormFactorTV, + "android.software.leanback": FormFactorTV, + "android.hardware.type.automotive": FormFactorAutomotive, +} + +// xrFeaturePrefixes cover Android XR, which is matched by namespace rather than +// by exact name. +// +// No XR feature constant appears in any android.jar shipped for API 34 to 36, +// so an exact name could not be verified the way the ones above were. Guessing +// one is how this rule broke before: two invented names meant no XR app ever +// matched, and XR packages kept getting the phone track's blocking finding. +// Matching the namespace is correct for every current XR feature +// (android.software.xr.api.spatial, android.software.xr.api.openxr, the +// android.hardware.xr.* inputs) and for ones added later. +var xrFeaturePrefixes = []string{ + "android.software.xr.", + "android.hardware.xr.", } // FormFactor reports the non-phone form factor this manifest declares, or @@ -86,6 +102,11 @@ func (m *Manifest) FormFactor() FormFactor { if ff, ok := featureFormFactors[f.Name]; ok { return ff } + for _, prefix := range xrFeaturePrefixes { + if strings.HasPrefix(f.Name, prefix) { + return FormFactorXR + } + } } return FormFactorPhone } diff --git a/internal/playscan/rules_test.go b/internal/playscan/rules_test.go index 07c60c8..e12aa01 100644 --- a/internal/playscan/rules_test.go +++ b/internal/playscan/rules_test.go @@ -15,7 +15,9 @@ func TestTargetAPILevelRespectsFormFactor(t *testing.T) { {"wear", "android.hardware.type.watch", FormFactorWear}, {"tv", "android.software.leanback", FormFactorTV}, {"automotive", "android.hardware.type.automotive", FormFactorAutomotive}, - {"xr", "android.software.xr.immersive", FormFactorXR}, + {"xr spatial", "android.software.xr.api.spatial", FormFactorXR}, + {"xr openxr", "android.software.xr.api.openxr", FormFactorXR}, + {"xr hardware input", "android.hardware.xr.input.controller", FormFactorXR}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -71,3 +73,45 @@ func TestTargetAPILevelFormFactorCleanWhenCurrent(t *testing.T) { t.Errorf("want no findings, got %d: %s", len(findings), findings[0].Title) } } + +// XR is matched by namespace, not by an exact constant name. The rule shipped +// with two invented names (android.software.xr.immersive, +// android.hardware.xr.head_tracking) that no real XR app declares, so no XR +// package ever matched and they kept getting the phone track's blocking +// finding. No XR constant appears in android.jar for API 34-36, so the +// namespace is what can actually be relied on. +func TestFormFactorMatchesXRByNamespace(t *testing.T) { + xr := []string{ + "android.software.xr.api.spatial", + "android.software.xr.api.openxr", + "android.hardware.xr.input.controller", + "android.software.xr.something.added.later", + } + for _, name := range xr { + m := &Manifest{UsesFeatures: []UsesFeature{{Name: name}}} + if got := m.FormFactor(); got != FormFactorXR { + t.Errorf("%s: FormFactor = %q, want %q", name, got, FormFactorXR) + } + } + + // The namespace must not swallow unrelated features that merely contain + // "xr", nor an XR feature the app says it does not require. + notXR := []string{ + "android.hardware.camera", + "android.software.xrated", // not the xr namespace: no dot after xr + "com.example.xr.custom", + } + for _, name := range notXR { + m := &Manifest{UsesFeatures: []UsesFeature{{Name: name}}} + if got := m.FormFactor(); got != FormFactorPhone { + t.Errorf("%s: FormFactor = %q, want phone", name, got) + } + } + + optional := &Manifest{UsesFeatures: []UsesFeature{ + {Name: "android.software.xr.api.spatial", Required: "false"}, + }} + if got := optional.FormFactor(); got != FormFactorPhone { + t.Errorf("optional XR feature: FormFactor = %q, want phone", got) + } +}