diff options
4 files changed, 364 insertions, 0 deletions
diff --git a/apct-tests/perftests/core/src/android/content/pm/SystemFeaturesPerfTest.java b/apct-tests/perftests/core/src/android/content/pm/SystemFeaturesPerfTest.java new file mode 100644 index 000000000000..43f545318124 --- /dev/null +++ b/apct-tests/perftests/core/src/android/content/pm/SystemFeaturesPerfTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.pm; + +import android.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; + +import androidx.test.filters.LargeTest; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.pm.RoSystemFeatures; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; + + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class SystemFeaturesPerfTest { + // As each query is relatively cheap, add an inner iteration loop to reduce execution noise. + private static final int NUM_ITERATIONS = 10; + + @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + + @Test + public void hasSystemFeature_PackageManager() { + final PackageManager pm = + InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageManager(); + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + for (int i = 0; i < NUM_ITERATIONS; ++i) { + pm.hasSystemFeature(PackageManager.FEATURE_WATCH); + pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK); + pm.hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS); + pm.hasSystemFeature(PackageManager.FEATURE_AUTOFILL); + pm.hasSystemFeature("com.android.custom.feature.1"); + pm.hasSystemFeature("foo"); + pm.hasSystemFeature(""); + } + } + } + + @Test + public void hasSystemFeature_SystemFeaturesCache() { + final PackageManager pm = + InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageManager(); + final SystemFeaturesCache cache = + new SystemFeaturesCache(Arrays.asList(pm.getSystemAvailableFeatures())); + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + for (int i = 0; i < NUM_ITERATIONS; ++i) { + cache.maybeHasFeature(PackageManager.FEATURE_WATCH, 0); + cache.maybeHasFeature(PackageManager.FEATURE_LEANBACK, 0); + cache.maybeHasFeature(PackageManager.FEATURE_IPSEC_TUNNELS, 0); + cache.maybeHasFeature(PackageManager.FEATURE_AUTOFILL, 0); + cache.maybeHasFeature("com.android.custom.feature.1", 0); + cache.maybeHasFeature("foo", 0); + cache.maybeHasFeature("", 0); + } + } + } + + @Test + public void hasSystemFeature_RoSystemFeatures() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + for (int i = 0; i < NUM_ITERATIONS; ++i) { + RoSystemFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0); + RoSystemFeatures.maybeHasFeature(PackageManager.FEATURE_LEANBACK, 0); + RoSystemFeatures.maybeHasFeature(PackageManager.FEATURE_IPSEC_TUNNELS, 0); + RoSystemFeatures.maybeHasFeature(PackageManager.FEATURE_AUTOFILL, 0); + RoSystemFeatures.maybeHasFeature("com.android.custom.feature.1", 0); + RoSystemFeatures.maybeHasFeature("foo", 0); + RoSystemFeatures.maybeHasFeature("", 0); + } + } + } +} diff --git a/core/java/android/content/pm/SystemFeaturesCache.aidl b/core/java/android/content/pm/SystemFeaturesCache.aidl new file mode 100644 index 000000000000..18c1830a1859 --- /dev/null +++ b/core/java/android/content/pm/SystemFeaturesCache.aidl @@ -0,0 +1,19 @@ +/* +** Copyright 2025, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.content.pm; + +parcelable SystemFeaturesCache; diff --git a/core/java/android/content/pm/SystemFeaturesCache.java b/core/java/android/content/pm/SystemFeaturesCache.java new file mode 100644 index 000000000000..c41a7abbbc35 --- /dev/null +++ b/core/java/android/content/pm/SystemFeaturesCache.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.pm; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArrayMap; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.Arrays; +import java.util.Collection; + +/** + * A simple cache for SDK-defined system feature versions. + * + * The dense representation minimizes any per-process memory impact (<1KB). The tradeoff is that + * custom, non-SDK defined features are not captured by the cache, for which we can rely on the + * usual IPC cache for related queries. + * + * @hide + */ +public final class SystemFeaturesCache implements Parcelable { + + // Sentinel value used for SDK-declared features that are unavailable on the current device. + private static final int UNAVAILABLE_FEATURE_VERSION = Integer.MIN_VALUE; + + // An array of versions for SDK-defined features, from [0, PackageManager.SDK_FEATURE_COUNT). + @NonNull + private final int[] mSdkFeatureVersions; + + /** + * Populates the cache from the set of all available {@link FeatureInfo} definitions. + * + * System features declared in {@link PackageManager} will be entered into the cache based on + * availability in this feature set. Other custom system features will be ignored. + */ + public SystemFeaturesCache(@NonNull ArrayMap<String, FeatureInfo> availableFeatures) { + this(availableFeatures.values()); + } + + @VisibleForTesting + public SystemFeaturesCache(@NonNull Collection<FeatureInfo> availableFeatures) { + // First set all SDK-defined features as unavailable. + mSdkFeatureVersions = new int[PackageManager.SDK_FEATURE_COUNT]; + Arrays.fill(mSdkFeatureVersions, UNAVAILABLE_FEATURE_VERSION); + + // Then populate SDK-defined feature versions from the full set of runtime features. + for (FeatureInfo fi : availableFeatures) { + int sdkFeatureIndex = PackageManager.maybeGetSdkFeatureIndex(fi.name); + if (sdkFeatureIndex >= 0) { + mSdkFeatureVersions[sdkFeatureIndex] = fi.version; + } + } + } + + /** Only used by @{code CREATOR.createFromParcel(...)} */ + private SystemFeaturesCache(@NonNull Parcel parcel) { + final int[] featureVersions = parcel.createIntArray(); + if (featureVersions == null) { + throw new IllegalArgumentException( + "Parceled SDK feature versions should never be null"); + } + if (featureVersions.length != PackageManager.SDK_FEATURE_COUNT) { + throw new IllegalArgumentException( + String.format( + "Unexpected cached SDK feature count: %d (expected %d)", + featureVersions.length, PackageManager.SDK_FEATURE_COUNT)); + } + mSdkFeatureVersions = featureVersions; + } + + /** + * @return Whether the given feature is available (for SDK-defined features), otherwise null. + */ + public Boolean maybeHasFeature(@NonNull String featureName, int version) { + // Features defined outside of the SDK aren't cached. + int sdkFeatureIndex = PackageManager.maybeGetSdkFeatureIndex(featureName); + if (sdkFeatureIndex < 0) { + return null; + } + + // As feature versions can in theory collide with our sentinel value, in the (extremely) + // unlikely event that the queried version matches the sentinel value, we can't distinguish + // between an unavailable feature and a feature with the defined sentinel value. + if (version == UNAVAILABLE_FEATURE_VERSION + && mSdkFeatureVersions[sdkFeatureIndex] == UNAVAILABLE_FEATURE_VERSION) { + return null; + } + + return mSdkFeatureVersions[sdkFeatureIndex] >= version; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int flags) { + parcel.writeIntArray(mSdkFeatureVersions); + } + + @NonNull + public static final Parcelable.Creator<SystemFeaturesCache> CREATOR = + new Parcelable.Creator<SystemFeaturesCache>() { + + @Override + public SystemFeaturesCache createFromParcel(Parcel parcel) { + return new SystemFeaturesCache(parcel); + } + + @Override + public SystemFeaturesCache[] newArray(int size) { + return new SystemFeaturesCache[size]; + } + }; +} diff --git a/core/tests/coretests/src/android/content/pm/SystemFeaturesCacheTest.java b/core/tests/coretests/src/android/content/pm/SystemFeaturesCacheTest.java new file mode 100644 index 000000000000..ce4aa42f39b6 --- /dev/null +++ b/core/tests/coretests/src/android/content/pm/SystemFeaturesCacheTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.pm; + +import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; +import static android.content.pm.PackageManager.FEATURE_WATCH; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Parcel; +import android.util.ArrayMap; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class SystemFeaturesCacheTest { + + private SystemFeaturesCache mCache; + + @Test + public void testNoFeatures() throws Exception { + SystemFeaturesCache cache = new SystemFeaturesCache(new ArrayMap<String, FeatureInfo>()); + assertThat(cache.maybeHasFeature("", 0)).isNull(); + assertThat(cache.maybeHasFeature(FEATURE_WATCH, 0)).isFalse(); + assertThat(cache.maybeHasFeature(FEATURE_PICTURE_IN_PICTURE, 0)).isFalse(); + assertThat(cache.maybeHasFeature("com.missing.feature", 0)).isNull(); + } + + @Test + public void testNonSdkFeature() throws Exception { + ArrayMap<String, FeatureInfo> features = new ArrayMap<>(); + features.put("custom.feature", createFeature("custom.feature", 0)); + SystemFeaturesCache cache = new SystemFeaturesCache(features); + + assertThat(cache.maybeHasFeature("custom.feature", 0)).isNull(); + } + + @Test + public void testSdkFeature() throws Exception { + ArrayMap<String, FeatureInfo> features = new ArrayMap<>(); + features.put(FEATURE_WATCH, createFeature(FEATURE_WATCH, 0)); + SystemFeaturesCache cache = new SystemFeaturesCache(features); + + assertThat(cache.maybeHasFeature(FEATURE_WATCH, 0)).isTrue(); + assertThat(cache.maybeHasFeature(FEATURE_WATCH, -1)).isTrue(); + assertThat(cache.maybeHasFeature(FEATURE_WATCH, 1)).isFalse(); + assertThat(cache.maybeHasFeature(FEATURE_WATCH, Integer.MIN_VALUE)).isTrue(); + assertThat(cache.maybeHasFeature(FEATURE_WATCH, Integer.MAX_VALUE)).isFalse(); + + // Other SDK-declared features should be reported as unavailable. + assertThat(cache.maybeHasFeature(FEATURE_PICTURE_IN_PICTURE, 0)).isFalse(); + } + + @Test + public void testSdkFeatureHasMinVersion() throws Exception { + ArrayMap<String, FeatureInfo> features = new ArrayMap<>(); + features.put(FEATURE_WATCH, createFeature(FEATURE_WATCH, Integer.MIN_VALUE)); + SystemFeaturesCache cache = new SystemFeaturesCache(features); + + assertThat(cache.maybeHasFeature(FEATURE_WATCH, 0)).isFalse(); + + // If both the query and the feature version itself happen to use MIN_VALUE, we can't + // reliably indicate availability, so it should report an indeterminate result. + assertThat(cache.maybeHasFeature(FEATURE_WATCH, Integer.MIN_VALUE)).isNull(); + } + + @Test + public void testParcel() throws Exception { + ArrayMap<String, FeatureInfo> features = new ArrayMap<>(); + features.put(FEATURE_WATCH, createFeature(FEATURE_WATCH, 0)); + SystemFeaturesCache cache = new SystemFeaturesCache(features); + + Parcel parcel = Parcel.obtain(); + SystemFeaturesCache parceledCache; + try { + parcel.writeParcelable(cache, 0); + parcel.setDataPosition(0); + parceledCache = parcel.readParcelable(getClass().getClassLoader()); + } finally { + parcel.recycle(); + } + + assertThat(parceledCache.maybeHasFeature(FEATURE_WATCH, 0)) + .isEqualTo(cache.maybeHasFeature(FEATURE_WATCH, 0)); + assertThat(parceledCache.maybeHasFeature(FEATURE_PICTURE_IN_PICTURE, 0)) + .isEqualTo(cache.maybeHasFeature(FEATURE_PICTURE_IN_PICTURE, 0)); + assertThat(parceledCache.maybeHasFeature("custom.feature", 0)) + .isEqualTo(cache.maybeHasFeature("custom.feature", 0)); + } + + private static FeatureInfo createFeature(String name, int version) { + FeatureInfo fi = new FeatureInfo(); + fi.name = name; + fi.version = version; + return fi; + } +} |