• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2025 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 android.content.pm;
18 
19 import android.annotation.MainThread;
20 import android.annotation.NonNull;
21 import android.util.ArrayMap;
22 
23 import com.android.internal.annotations.VisibleForTesting;
24 
25 import java.util.Arrays;
26 import java.util.Collection;
27 
28 /**
29  * A simple cache for SDK-defined system feature versions.
30  *
31  * The dense representation minimizes any per-process memory impact (<1KB). The tradeoff is that
32  * custom, non-SDK defined features are not captured by the cache, for which we can rely on the
33  * usual IPC cache for related queries.
34  *
35  * @hide
36  */
37 public final class SystemFeaturesCache {
38 
39     // Sentinel value used for SDK-declared features that are unavailable on the current device.
40     private static final int UNAVAILABLE_FEATURE_VERSION = Integer.MIN_VALUE;
41 
42     // This will be initialized just once, from the process main thread, but ready from any thread.
43     private static volatile SystemFeaturesCache sInstance;
44 
45     // An array of versions for SDK-defined features, from [0, PackageManager.SDK_FEATURE_COUNT).
46     @NonNull
47     private final int[] mSdkFeatureVersions;
48 
49     /**
50      * Installs the process-global cache instance.
51      *
52      * <p>Note: Usage should be gated on android.content.pm.Flags.cacheSdkSystemFeature(). In
53      * practice, this should only be called from 1) SystemServer init, or 2) bindApplication.
54      */
55     @MainThread
setInstance(SystemFeaturesCache instance)56     public static void setInstance(SystemFeaturesCache instance) {
57         if (sInstance != null) {
58             throw new IllegalStateException("SystemFeaturesCache instance already initialized.");
59         }
60         sInstance = instance;
61     }
62 
63     /**
64      * Gets the process-global cache instance.
65      *
66      * Note: Usage should be gated on android.content.pm.Flags.cacheSdkSystemFeature(), and should
67      * always occur after the instance has been installed early in the process lifecycle.
68      */
getInstance()69     public static @NonNull SystemFeaturesCache getInstance() {
70         SystemFeaturesCache instance = sInstance;
71         if (instance == null) {
72             throw new IllegalStateException("SystemFeaturesCache not initialized");
73         }
74         return instance;
75     }
76 
77     /** Checks for existence of the process-global instance. */
hasInstance()78     public static boolean hasInstance() {
79         return sInstance != null;
80     }
81 
82     /** Clears the process-global cache instance for testing. */
83     @VisibleForTesting
clearInstance()84     public static void clearInstance() {
85         sInstance = null;
86     }
87 
88     /**
89      * Populates the cache from the set of all available {@link FeatureInfo} definitions.
90      *
91      * System features declared in {@link PackageManager} will be entered into the cache based on
92      * availability in this feature set. Other custom system features will be ignored.
93      */
SystemFeaturesCache(@onNull ArrayMap<String, FeatureInfo> availableFeatures)94     public SystemFeaturesCache(@NonNull ArrayMap<String, FeatureInfo> availableFeatures) {
95         this(availableFeatures.values());
96     }
97 
98     @VisibleForTesting
SystemFeaturesCache(@onNull Collection<FeatureInfo> availableFeatures)99     public SystemFeaturesCache(@NonNull Collection<FeatureInfo> availableFeatures) {
100         // First set all SDK-defined features as unavailable.
101         mSdkFeatureVersions = new int[PackageManager.SDK_FEATURE_COUNT];
102         Arrays.fill(mSdkFeatureVersions, UNAVAILABLE_FEATURE_VERSION);
103 
104         // Then populate SDK-defined feature versions from the full set of runtime features.
105         for (FeatureInfo fi : availableFeatures) {
106             int sdkFeatureIndex = PackageManager.maybeGetSdkFeatureIndex(fi.name);
107             if (sdkFeatureIndex >= 0) {
108                 mSdkFeatureVersions[sdkFeatureIndex] = fi.version;
109             }
110         }
111     }
112 
113     /**
114      * Populates the cache from an array of SDK feature versions originally obtained via {@link
115      * #getSdkFeatureVersions()} from another instance.
116      */
SystemFeaturesCache(@onNull int[] sdkFeatureVersions)117     public SystemFeaturesCache(@NonNull int[] sdkFeatureVersions) {
118         if (sdkFeatureVersions.length != PackageManager.SDK_FEATURE_COUNT) {
119             throw new IllegalArgumentException(
120                     String.format(
121                             "Unexpected cached SDK feature count: %d (expected %d)",
122                             sdkFeatureVersions.length, PackageManager.SDK_FEATURE_COUNT));
123         }
124         mSdkFeatureVersions = sdkFeatureVersions;
125     }
126 
127     /**
128      * Gets the raw cached feature versions.
129      *
130      * <p>Note: This should generally only be neded for (de)serialization purposes.
131      */
132     // TODO(b/375000483): Consider reusing the ApplicationSharedMemory mapping for version lookup.
getSdkFeatureVersions()133     public int[] getSdkFeatureVersions() {
134         return mSdkFeatureVersions;
135     }
136 
137     /**
138      * @return Whether the given feature is available (for SDK-defined features), otherwise null.
139      */
maybeHasFeature(@onNull String featureName, int version)140     public Boolean maybeHasFeature(@NonNull String featureName, int version) {
141         // Features defined outside of the SDK aren't cached.
142         int sdkFeatureIndex = PackageManager.maybeGetSdkFeatureIndex(featureName);
143         if (sdkFeatureIndex < 0) {
144             return null;
145         }
146 
147         // As feature versions can in theory collide with our sentinel value, in the (extremely)
148         // unlikely event that the queried version matches the sentinel value, we can't distinguish
149         // between an unavailable feature and a feature with the defined sentinel value.
150         if (version == UNAVAILABLE_FEATURE_VERSION
151                 && mSdkFeatureVersions[sdkFeatureIndex] == UNAVAILABLE_FEATURE_VERSION) {
152             return null;
153         }
154 
155         return mSdkFeatureVersions[sdkFeatureIndex] >= version;
156     }
157 }
158