Merge "BinderProxyTest: more time to get proxy" into main
diff --git a/apct-tests/perftests/aconfig/Android.bp b/apct-tests/perftests/aconfig/Android.bp
new file mode 100644
index 0000000..715923d
--- /dev/null
+++ b/apct-tests/perftests/aconfig/Android.bp
@@ -0,0 +1,39 @@
+// 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 {
+    default_team: "trendy_team_android_core_experiments",
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "AconfigPerfTests",
+    srcs: ["src/**/*.java"],
+    static_libs: [
+        "aconfig_device_paths_java_util",
+        "androidx.test.rules",
+        "apct-perftests-utils",
+        "collector-device-lib",
+        "truth",
+    ],
+    platform_apis: true,
+    certificate: "platform",
+    test_suites: ["device-tests"],
+    data: [":perfetto_artifacts"],
+}
diff --git a/apct-tests/perftests/aconfig/AndroidManifest.xml b/apct-tests/perftests/aconfig/AndroidManifest.xml
new file mode 100644
index 0000000..e9d7c17
--- /dev/null
+++ b/apct-tests/perftests/aconfig/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.perftests.aconfig">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.perftests.aconfig"/>
+
+</manifest>
\ No newline at end of file
diff --git a/apct-tests/perftests/aconfig/AndroidTest.xml b/apct-tests/perftests/aconfig/AndroidTest.xml
new file mode 100644
index 0000000..036e031
--- /dev/null
+++ b/apct-tests/perftests/aconfig/AndroidTest.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+<configuration description="Runs AconfigPerfTests metric instrumentation.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-metric-instrumentation" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="AconfigPerfTests.apk" />
+    </target_preparer>
+
+    <!-- Needed for pushing the trace config file -->
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="push-file" key="trace_config_detailed.textproto" value="/data/misc/perfetto-traces/trace_config.textproto" />
+    </target_preparer>
+
+
+    <!-- Needed for pulling the collected trace config on to the host -->
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="pull-pattern-keys" value="perfetto_file_path" />
+    </metrics_collector>
+
+    <!-- Needed for storing the perfetto trace files in the sdcard/test_results-->
+    <option name="isolated-storage" value="false" />
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.perftests.aconfig" />
+        <option name="hidden-api-checks" value="false"/>
+
+        <!-- Listener related args for collecting the traces and waiting for the device to stabilize. -->
+        <option name="device-listeners" value="android.device.collectors.ProcLoadListener,android.device.collectors.PerfettoListener" />
+        <!-- Guarantee that user defined RunListeners will be running before any of the default listeners defined in this runner. -->
+        <option name="instrumentation-arg" key="newRunListenerMode" value="true" />
+
+        <!-- ProcLoadListener related arguments -->
+        <!-- Wait for device last minute threshold to reach 3 with 2 minute timeout before starting the test run -->
+        <option name="instrumentation-arg" key="procload-collector:per_run" value="true" />
+        <option name="instrumentation-arg" key="proc-loadavg-threshold" value="3" />
+        <option name="instrumentation-arg" key="proc-loadavg-timeout" value="120000" />
+        <option name="instrumentation-arg" key="proc-loadavg-interval" value="10000" />
+
+        <!-- PerfettoListener related arguments -->
+        <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true" />
+        <option name="instrumentation-arg" key="perfetto_config_file" value="trace_config.textproto" />
+
+        <option name="instrumentation-arg" key="newRunListenerMode" value="true" />
+
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/apct-tests/perftests/aconfig/OWNERS b/apct-tests/perftests/aconfig/OWNERS
new file mode 100644
index 0000000..2202076
--- /dev/null
+++ b/apct-tests/perftests/aconfig/OWNERS
@@ -0,0 +1 @@
+file:platform/packages/modules/ConfigInfrastructure:/OWNERS
\ No newline at end of file
diff --git a/apct-tests/perftests/aconfig/src/android/os/flagging/AconfigPackagePerfTest.java b/apct-tests/perftests/aconfig/src/android/os/flagging/AconfigPackagePerfTest.java
new file mode 100644
index 0000000..df6e3c8
--- /dev/null
+++ b/apct-tests/perftests/aconfig/src/android/os/flagging/AconfigPackagePerfTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 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.os.flagging;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.aconfig.DeviceProtosTestUtil;
+import android.aconfig.nano.Aconfig.parsed_flag;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@RunWith(Parameterized.class)
+public class AconfigPackagePerfTest {
+
+    @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    @Parameterized.Parameters(name = "isPlatform={0}")
+    public static Collection<Object[]> data() {
+        return Arrays.asList(new Object[][] {{false}, {true}});
+    }
+
+    private static final Set<String> PLATFORM_CONTAINERS = Set.of("system", "vendor", "product");
+    private static List<parsed_flag> sFlags;
+
+    @BeforeClass
+    public static void init() {
+        try {
+            sFlags = DeviceProtosTestUtil.loadAndParseFlagProtos();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Parameterized.Parameter(0)
+
+    // if this variable is true, then the test query flags from system/product/vendor
+    // if this variable is false, then the test query flags from updatable partitions
+    public boolean mIsPlatform;
+
+    @Test
+    public void timeAconfigPackageLoadOnePackage() {
+        String packageName = "";
+        for (parsed_flag flag : sFlags) {
+            if (mIsPlatform == PLATFORM_CONTAINERS.contains(flag.container)) {
+                packageName = flag.package_;
+                break;
+            }
+        }
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            AconfigPackage.load(packageName);
+        }
+    }
+
+    @Test
+    public void timeAconfigPackageLoadMultiplePackages() {
+        // load num packages
+        int packageNum = 25;
+        Set<String> packageSet = new HashSet<>();
+        for (parsed_flag flag : sFlags) {
+            if (mIsPlatform == PLATFORM_CONTAINERS.contains(flag.container)) {
+                packageSet.add(flag.package_);
+            }
+            if (packageSet.size() >= packageNum) {
+                break;
+            }
+        }
+        List<String> packageList = new ArrayList(packageSet);
+        assertThat(packageList.size()).isAtLeast(packageNum);
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        for (int i = 0; state.keepRunning(); i++) {
+            AconfigPackage.load(packageList.get(i % packageNum));
+        }
+    }
+
+    @Test
+    public void timeAconfigPackageGetBooleanFlagValue() {
+        // get one package contains num of flags
+        int flagNum = 20;
+        List<parsed_flag> l = findNumFlagsInSamePackage(flagNum, mIsPlatform);
+        List<String> flagName = new ArrayList<>();
+        String packageName = l.get(0).package_;
+        for (parsed_flag flag : l) {
+            flagName.add(flag.name);
+        }
+        assertThat(flagName.size()).isAtLeast(flagNum);
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        AconfigPackage ap = AconfigPackage.load(packageName);
+        for (int i = 0; state.keepRunning(); i++) {
+            ap.getBooleanFlagValue(flagName.get(i % flagNum), false);
+        }
+    }
+
+    private static List<parsed_flag> findNumFlagsInSamePackage(int num, boolean isPlatform) {
+        Map<String, List<parsed_flag>> packageToFlag = new HashMap<>();
+        List<parsed_flag> ret = new ArrayList<parsed_flag>();
+        for (parsed_flag flag : sFlags) {
+            if (isPlatform == PLATFORM_CONTAINERS.contains(flag.container)) {
+                ret =
+                        packageToFlag.computeIfAbsent(
+                                flag.package_, k -> new ArrayList<parsed_flag>());
+                ret.add(flag);
+                if (ret.size() >= num) {
+                    break;
+                }
+            }
+        }
+        return ret;
+    }
+}
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index 6e4c28f..7a811a1 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -28,6 +28,7 @@
 per-file Service* = file:/ACTIVITY_MANAGER_OWNERS
 per-file SystemServiceRegistry.java = file:/ACTIVITY_MANAGER_OWNERS
 per-file *UserSwitchObserver* = file:/ACTIVITY_MANAGER_OWNERS
+per-file UidObserver* = file:/ACTIVITY_MANAGER_OWNERS
 
 # UI Automation
 per-file *UiAutomation* = file:/services/accessibility/OWNERS
diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java
index e98397d..cc6ec08 100644
--- a/core/java/android/permission/PermissionManager.java
+++ b/core/java/android/permission/PermissionManager.java
@@ -1716,20 +1716,14 @@
 
     private static int checkPermissionUncached(@Nullable String permission, int pid, int uid,
             int deviceId) {
+        final int appId = UserHandle.getAppId(uid);
+        if (appId == Process.ROOT_UID || appId == Process.SYSTEM_UID) {
+            return PackageManager.PERMISSION_GRANTED;
+        }
         final IActivityManager am = ActivityManager.getService();
         if (am == null) {
-            // Well this is super awkward; we somehow don't have an active ActivityManager
-            // instance. If we're testing a root or system UID, then they totally have whatever
-            // permission this is.
-            final int appId = UserHandle.getAppId(uid);
-            if (appId == Process.ROOT_UID || appId == Process.SYSTEM_UID) {
-                if (sShouldWarnMissingActivityManager) {
-                    Slog.w(LOG_TAG, "Missing ActivityManager; assuming " + uid + " holds "
-                            + permission);
-                    sShouldWarnMissingActivityManager = false;
-                }
-                return PackageManager.PERMISSION_GRANTED;
-            }
+            // We don't have an active ActivityManager instance and the calling UID is not root or
+            // system, so we don't grant this permission.
             Slog.w(LOG_TAG, "Missing ActivityManager; assuming " + uid + " does not hold "
                     + permission);
             return PackageManager.PERMISSION_DENIED;
diff --git a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
index 7ad18b8..917d501 100644
--- a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
+++ b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
@@ -306,8 +306,9 @@
                                                when, uncompLen, crc);
             }
 
-            ALOGE("Library '%s' is not PAGE(%zu)-aligned - will not be able to open it directly "
-                  "from apk.\n",
+            ALOGE("extractNativeLibs=false library '%s' is not PAGE(%zu)-"
+                  "aligned within apk (APK alignment, not ELF alignment) -"
+                  "will not be able to open it directly from apk.\n",
                   fileName, kPageSize);
             return INSTALL_FAILED_INVALID_APK;
         }
diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp
index 1bc15d7..a13dd78 100644
--- a/libs/androidfw/Android.bp
+++ b/libs/androidfw/Android.bp
@@ -80,6 +80,7 @@
         "LoadedArsc.cpp",
         "Locale.cpp",
         "LocaleData.cpp",
+        "LocaleDataLookup.cpp",
         "misc.cpp",
         "NinePatch.cpp",
         "ObbFile.cpp",
@@ -224,6 +225,7 @@
         "tests/Idmap_test.cpp",
         "tests/LoadedArsc_test.cpp",
         "tests/Locale_test.cpp",
+        "tests/LocaleDataLookup_test.cpp",
         "tests/NinePatch_test.cpp",
         "tests/ResourceTimer_test.cpp",
         "tests/ResourceUtils_test.cpp",
diff --git a/libs/androidfw/LocaleData.cpp b/libs/androidfw/LocaleData.cpp
index 020cef6..1b23d90 100644
--- a/libs/androidfw/LocaleData.cpp
+++ b/libs/androidfw/LocaleData.cpp
@@ -23,39 +23,18 @@
 #include <unordered_set>
 
 #include <androidfw/LocaleData.h>
+#include <androidfw/LocaleDataLookup.h>
 
 namespace android {
 
-#include "LocaleDataTables.cpp"
-
-inline uint32_t packLocale(const char* language, const char* region) {
-    return (((uint8_t) language[0]) << 24u) | (((uint8_t) language[1]) << 16u) |
-           (((uint8_t) region[0]) << 8u) | ((uint8_t) region[1]);
-}
-
-inline uint32_t dropRegion(uint32_t packed_locale) {
-    return packed_locale & 0xFFFF0000LU;
-}
-
-inline bool hasRegion(uint32_t packed_locale) {
-    return (packed_locale & 0x0000FFFFLU) != 0;
-}
-
-const size_t SCRIPT_LENGTH = 4;
-const size_t SCRIPT_PARENTS_COUNT = sizeof(SCRIPT_PARENTS)/sizeof(SCRIPT_PARENTS[0]);
 const uint32_t PACKED_ROOT = 0; // to represent the root locale
+const uint32_t MAX_PARENT_DEPTH = getMaxAncestorTreeDepth();
 
 uint32_t findParent(uint32_t packed_locale, const char* script) {
     if (hasRegion(packed_locale)) {
-        for (size_t i = 0; i < SCRIPT_PARENTS_COUNT; i++) {
-            if (memcmp(script, SCRIPT_PARENTS[i].script, SCRIPT_LENGTH) == 0) {
-                auto map = SCRIPT_PARENTS[i].map;
-                auto lookup_result = map->find(packed_locale);
-                if (lookup_result != map->end()) {
-                    return lookup_result->second;
-                }
-                break;
-            }
+        auto parent_key = findParentLocalePackedKey(script, packed_locale);
+        if (parent_key != 0) {
+            return parent_key;
         }
         return dropRegion(packed_locale);
     }
@@ -111,17 +90,6 @@
     return supported_ancestor_count + request_ancestors_index - 1;
 }
 
-inline bool isRepresentative(uint32_t language_and_region, const char* script) {
-    const uint64_t packed_locale = (
-            (((uint64_t) language_and_region) << 32u) |
-            (((uint64_t) script[0]) << 24u) |
-            (((uint64_t) script[1]) << 16u) |
-            (((uint64_t) script[2]) <<  8u) |
-            ((uint64_t) script[3]));
-
-    return (REPRESENTATIVE_LOCALES.count(packed_locale) != 0);
-}
-
 const uint32_t US_SPANISH = 0x65735553LU; // es-US
 const uint32_t MEXICAN_SPANISH = 0x65734D58LU; // es-MX
 const uint32_t LATIN_AMERICAN_SPANISH = 0x6573A424LU; // es-419
@@ -185,8 +153,8 @@
 
     // If we are here, left and right are equidistant from the request. We will
     // try and see if any of them is a representative locale.
-    const bool left_is_representative = isRepresentative(left, requested_script);
-    const bool right_is_representative = isRepresentative(right, requested_script);
+    const bool left_is_representative = isLocaleRepresentative(left, requested_script);
+    const bool right_is_representative = isLocaleRepresentative(right, requested_script);
     if (left_is_representative != right_is_representative) {
         return (int) left_is_representative - (int) right_is_representative;
     }
@@ -204,14 +172,14 @@
         return;
     }
     uint32_t lookup_key = packLocale(language, region);
-    auto lookup_result = LIKELY_SCRIPTS.find(lookup_key);
-    if (lookup_result == LIKELY_SCRIPTS.end()) {
+    auto lookup_result = lookupLikelyScript(lookup_key);
+    if (lookup_result == nullptr) {
         // We couldn't find the locale. Let's try without the region
         if (region[0] != '\0') {
             lookup_key = dropRegion(lookup_key);
-            lookup_result = LIKELY_SCRIPTS.find(lookup_key);
-            if (lookup_result != LIKELY_SCRIPTS.end()) {
-                memcpy(out, SCRIPT_CODES[lookup_result->second], SCRIPT_LENGTH);
+            lookup_result = lookupLikelyScript(lookup_key);
+            if (lookup_result != nullptr) {
+                memcpy(out, lookup_result, SCRIPT_LENGTH);
                 return;
             }
         }
@@ -220,7 +188,7 @@
         return;
     } else {
         // We found the locale.
-        memcpy(out, SCRIPT_CODES[lookup_result->second], SCRIPT_LENGTH);
+        memcpy(out, lookup_result, SCRIPT_LENGTH);
     }
 }
 
diff --git a/libs/androidfw/LocaleDataLookup.cpp b/libs/androidfw/LocaleDataLookup.cpp
new file mode 100644
index 0000000..5441e22
--- /dev/null
+++ b/libs/androidfw/LocaleDataLookup.cpp
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+
+#include <unordered_map>
+#include <unordered_set>
+
+#include <androidfw/LocaleDataLookup.h>
+
+namespace android {
+
+#include "LocaleDataTables.cpp"
+
+const size_t SCRIPT_PARENTS_COUNT = sizeof(SCRIPT_PARENTS)/sizeof(SCRIPT_PARENTS[0]);
+
+const char* lookupLikelyScript(uint32_t packed_lang_region) {
+
+    auto lookup_result = LIKELY_SCRIPTS.find(packed_lang_region);
+    if (lookup_result == LIKELY_SCRIPTS.end()) {
+        return nullptr;
+    } else {
+        return SCRIPT_CODES[lookup_result->second];
+    }
+}
+
+uint32_t findParentLocalePackedKey(const char* script, uint32_t packed_lang_region) {
+    for (size_t i = 0; i < SCRIPT_PARENTS_COUNT; i++) {
+        if (memcmp(script, SCRIPT_PARENTS[i].script, SCRIPT_LENGTH) == 0) {
+            auto map = SCRIPT_PARENTS[i].map;
+            auto lookup_result = map->find(packed_lang_region);
+            if (lookup_result != map->end()) {
+                return lookup_result->second;
+            }
+            break;
+        }
+    }
+    return 0;
+}
+
+uint32_t getMaxAncestorTreeDepth() {
+    return MAX_PARENT_DEPTH;
+}
+
+namespace hidden {
+
+bool isRepresentative(uint64_t packed_locale) {
+    return (REPRESENTATIVE_LOCALES.count(packed_locale) != 0);
+}
+
+} // namespace hidden
+
+} // namespace android
diff --git a/libs/androidfw/include/androidfw/LocaleDataLookup.h b/libs/androidfw/include/androidfw/LocaleDataLookup.h
new file mode 100644
index 0000000..7fde712
--- /dev/null
+++ b/libs/androidfw/include/androidfw/LocaleDataLookup.h
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <stddef.h>
+#include <stdint.h>
+
+
+namespace android {
+
+namespace hidden {
+    bool isRepresentative(uint64_t packed_locale);
+}
+
+constexpr size_t SCRIPT_LENGTH = 4;
+
+constexpr inline uint32_t packLocale(const char* language, const char* region) {
+    const unsigned char* lang = reinterpret_cast<const unsigned char*>(language);
+    const unsigned char* reg = reinterpret_cast<const unsigned char*>(region);
+    return (static_cast<uint32_t>(lang[0]) << 24u) |
+            (static_cast<uint32_t>(lang[1]) << 16u) |
+            (static_cast<uint32_t>(reg[0]) << 8u) |
+            static_cast<uint32_t>(reg[1]);
+}
+
+constexpr inline uint32_t dropRegion(uint32_t packed_locale) {
+    return packed_locale & 0xFFFF0000LU;
+}
+
+constexpr inline bool hasRegion(uint32_t packed_locale) {
+    return (packed_locale & 0x0000FFFFLU) != 0;
+}
+
+/**
+ * Return nullptr if the key isn't found. The input packed_lang_region can be computed
+ * by android::packLocale.
+ * Note that the returned char* is either nullptr or 4-byte char seqeuence, but isn't
+ * a null-terminated string.
+ */
+const char* lookupLikelyScript(uint32_t packed_lang_region);
+/**
+ * Return false if the key isn't representative. The input lookup key can be computed
+ * by android::packLocale.
+ */
+bool inline isLocaleRepresentative(uint32_t language_and_region, const char* script) {
+    const unsigned char* s = reinterpret_cast<const unsigned char*>(script);
+    const uint64_t packed_locale = (
+            ((static_cast<uint64_t>(language_and_region)) << 32u) |
+            (static_cast<uint64_t>(s[0]) << 24u) |
+            (static_cast<uint64_t>(s[1]) << 16u) |
+            (static_cast<uint64_t>(s[2]) <<  8u) |
+            static_cast<uint64_t>(s[3]));
+
+    return hidden::isRepresentative(packed_locale);
+}
+
+/**
+ * Return a parent packed key for a given script and child packed key. Return 0 if
+ * no parent is found.
+ */
+uint32_t findParentLocalePackedKey(const char* script, uint32_t packed_lang_region);
+
+uint32_t getMaxAncestorTreeDepth();
+
+} // namespace android
diff --git a/libs/androidfw/tests/LocaleDataLookup_test.cpp b/libs/androidfw/tests/LocaleDataLookup_test.cpp
new file mode 100644
index 0000000..26b220d
--- /dev/null
+++ b/libs/androidfw/tests/LocaleDataLookup_test.cpp
@@ -0,0 +1,108 @@
+/*
+ * 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.
+ */
+
+#include "androidfw/LocaleDataLookup.h"
+
+#include <cstddef>
+#include <string>
+
+#include "gtest/gtest.h"
+#include "gmock/gmock.h"
+
+
+namespace android {
+
+constexpr const char NULL_SCRIPT[4] = {'\0', '\0', '\0','\0' };
+
+#define EXPECT_SCEIPT_EQ(ex, s) EXPECT_EQ(0, s == nullptr ? -1 : memcmp(ex, s, 4))
+
+// Similar to packLanguageOrRegion() in ResourceTypes.cpp
+static uint32_t encodeLanguageOrRegionLiteral(const char* in, const char base) {
+  size_t len = strlen(in);
+  if (len <= 1) {
+    return 0;
+  }
+
+  if (len == 2) {
+      return (((uint8_t) in[0]) << 8) | ((uint8_t) in[1]);
+  }
+  uint8_t first = (in[0] - base) & 0x007f;
+  uint8_t second = (in[1] - base) & 0x007f;
+  uint8_t third = (in[2] - base) & 0x007f;
+
+  return ((uint8_t) (0x80 | (third << 2) | (second >> 3)) << 8) | ((second << 5) | first);
+}
+
+static uint32_t encodeLocale(const char* language, const char* region) {
+    return (encodeLanguageOrRegionLiteral(language, 'a') << 16) |
+            encodeLanguageOrRegionLiteral(region, '0');
+}
+
+TEST(LocaleDataLookupTest, lookupLikelyScript) {
+  EXPECT_EQ(nullptr, lookupLikelyScript(encodeLocale("", "")));
+  EXPECT_SCEIPT_EQ("Latn", lookupLikelyScript(encodeLocale("en", "")));
+  EXPECT_EQ(nullptr, lookupLikelyScript(encodeLocale("en", "US")));
+  EXPECT_EQ(nullptr, lookupLikelyScript(encodeLocale("en", "GB")));
+  EXPECT_SCEIPT_EQ("Latn", lookupLikelyScript(encodeLocale("fr", "")));
+  EXPECT_EQ(nullptr, lookupLikelyScript(encodeLocale("fr", "FR")));
+
+
+  EXPECT_SCEIPT_EQ("~~~A", lookupLikelyScript(encodeLocale("en", "XA")));
+  EXPECT_SCEIPT_EQ("Latn", lookupLikelyScript(encodeLocale("ha", "")));
+  EXPECT_SCEIPT_EQ("Arab", lookupLikelyScript(encodeLocale("ha", "SD")));
+  EXPECT_EQ(nullptr, lookupLikelyScript(encodeLocale("ha", "Sd"))); // case sensitive
+  EXPECT_SCEIPT_EQ("Hans", lookupLikelyScript(encodeLocale("zh", "")));
+  EXPECT_EQ(nullptr, lookupLikelyScript(encodeLocale("zh", "CN")));
+  EXPECT_SCEIPT_EQ("Hant", lookupLikelyScript(encodeLocale("zh", "HK")));
+
+  EXPECT_SCEIPT_EQ("Nshu", lookupLikelyScript(encodeLocale("zhx", "")));
+  EXPECT_SCEIPT_EQ("Nshu", lookupLikelyScript(0xDCF90000u)); // encoded "zhx"
+}
+
+TEST(LocaleDataLookupTest, isLocaleRepresentative) {
+  EXPECT_TRUE(isLocaleRepresentative(encodeLocale("en", "US"), "Latn"));
+  EXPECT_TRUE(isLocaleRepresentative(encodeLocale("en", "GB"), "Latn"));
+  EXPECT_FALSE(isLocaleRepresentative(encodeLocale("en", "US"), NULL_SCRIPT));
+  EXPECT_FALSE(isLocaleRepresentative(encodeLocale("en", ""), "Latn"));
+  EXPECT_FALSE(isLocaleRepresentative(encodeLocale("en", ""), NULL_SCRIPT));
+  EXPECT_FALSE(isLocaleRepresentative(encodeLocale("en", "US"), "Arab"));
+
+  EXPECT_TRUE(isLocaleRepresentative(encodeLocale("fr", "FR"), "Latn"));
+
+  EXPECT_TRUE(isLocaleRepresentative(encodeLocale("zh", "CN"), "Hans"));
+  EXPECT_FALSE(isLocaleRepresentative(encodeLocale("zh", "TW"), "Hans"));
+  EXPECT_FALSE(isLocaleRepresentative(encodeLocale("zhx", "CN"), "Hans"));
+  EXPECT_FALSE(isLocaleRepresentative(0xDCF9434E, "Hans"));
+  EXPECT_TRUE(isLocaleRepresentative(encodeLocale("zhx", "CN"), "Nshu"));
+  EXPECT_TRUE(isLocaleRepresentative(0xDCF9434E, "Nshu"));
+}
+
+TEST(LocaleDataLookupTest, findParentLocalePackedKey) {
+  EXPECT_EQ(encodeLocale("en", "001"), findParentLocalePackedKey("Latn", encodeLocale("en", "GB")));
+  EXPECT_EQ(0x656E8400u, findParentLocalePackedKey("Latn", encodeLocale("en", "GB")));
+
+  EXPECT_EQ(encodeLocale("en", "IN"), findParentLocalePackedKey("Deva", encodeLocale("hi", "")));
+
+  EXPECT_EQ(encodeLocale("ar", "015"), findParentLocalePackedKey("Arab", encodeLocale("ar", "AE")));
+  EXPECT_EQ(0x61729420u, findParentLocalePackedKey("Arab", encodeLocale("ar", "AE")));
+
+  EXPECT_EQ(encodeLocale("ar", "015"), findParentLocalePackedKey("~~~B", encodeLocale("ar", "XB")));
+  EXPECT_EQ(0x61729420u, findParentLocalePackedKey("Arab", encodeLocale("ar", "AE")));
+
+  EXPECT_EQ(encodeLocale("zh", "HK"), findParentLocalePackedKey("Hant", encodeLocale("zh", "MO")));
+}
+
+}  // namespace android
diff --git a/services/core/java/com/android/server/am/OWNERS b/services/core/java/com/android/server/am/OWNERS
index d731912..ebe1fe8 100644
--- a/services/core/java/com/android/server/am/OWNERS
+++ b/services/core/java/com/android/server/am/OWNERS
@@ -21,6 +21,7 @@
 per-file HostingRecord.java = file:/ACTIVITY_MANAGER_OWNERS
 per-file App*ExitInfo* = file:/ACTIVITY_MANAGER_OWNERS
 per-file appexitinfo.proto = file:/ACTIVITY_MANAGER_OWNERS
+per-file UidObserverController* = file:/ACTIVITY_MANAGER_OWNERS
 per-file App*StartInfo* = file:/PERFORMANCE_OWNERS
 per-file appstartinfo.proto = file:/PERFORMANCE_OWNERS
 
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index c314ab0..3f91575 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -369,16 +369,7 @@
         @Override
         public void onBootPhase(int phase) {
             super.onBootPhase(phase);
-            if (phase == PHASE_ACTIVITY_MANAGER_READY) {
-                mLockSettingsService.migrateOldDataAfterSystemReady();
-                mLockSettingsService.deleteRepairModePersistentDataIfNeeded();
-            } else if (phase == PHASE_BOOT_COMPLETED) {
-                // In the case of an upgrade, PHASE_BOOT_COMPLETED means that a rollback to the old
-                // build can no longer occur.  This is the time to destroy any migrated protectors.
-                mLockSettingsService.destroyMigratedProtectors();
-
-                mLockSettingsService.loadEscrowData();
-            }
+            mLockSettingsService.onBootPhase(phase);
         }
 
         @Override
@@ -397,6 +388,21 @@
         }
     }
 
+    private void onBootPhase(int phase) {
+        if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
+            migrateOldDataAfterSystemReady();
+            deleteRepairModePersistentDataIfNeeded();
+        } else if (phase == SystemService.PHASE_BOOT_COMPLETED) {
+            mHandler.post(() -> {
+                // In the case of an upgrade, PHASE_BOOT_COMPLETED means that a rollback to the old
+                // build can no longer occur.  This is the time to destroy any migrated protectors.
+                destroyMigratedProtectors();
+
+                loadEscrowData();
+            });
+        }
+    }
+
     @VisibleForTesting
     protected static class SynchronizedStrongAuthTracker
             extends LockPatternUtils.StrongAuthTracker {
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index cb8e1a0..d009fa0 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -703,34 +703,29 @@
         @Override
         public void handleMessage(Message msg) {
             BatteryCallback cb = mCallback;
+            if (cb == null) {
+                return;
+            }
             switch (msg.what) {
                 case MSG_REPORT_CPU_UPDATE_NEEDED:
-                    if (cb != null) {
-                        cb.batteryNeedsCpuUpdate();
-                    }
+                    cb.batteryNeedsCpuUpdate();
                     break;
                 case MSG_REPORT_POWER_CHANGE:
-                    if (cb != null) {
-                        cb.batteryPowerChanged(msg.arg1 != 0);
-                    }
+                    cb.batteryPowerChanged(msg.arg1 != 0);
                     break;
                 case MSG_REPORT_CHARGING:
-                    if (cb != null) {
-                        final String action;
-                        synchronized (BatteryStatsImpl.this) {
-                            action = mCharging ? BatteryManager.ACTION_CHARGING
-                                    : BatteryManager.ACTION_DISCHARGING;
-                        }
-                        Intent intent = new Intent(action);
-                        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
-                        cb.batterySendBroadcast(intent);
+                    final String action;
+                    synchronized (BatteryStatsImpl.this) {
+                        action = mCharging ? BatteryManager.ACTION_CHARGING
+                                : BatteryManager.ACTION_DISCHARGING;
                     }
+                    Intent intent = new Intent(action);
+                    intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+                    cb.batterySendBroadcast(intent);
                     break;
                 case MSG_REPORT_RESET_STATS:
-                    if (cb != null) {
-                        cb.batteryStatsReset();
-                    }
-                }
+                    cb.batteryStatsReset();
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index e178203..8763c8f 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -100,7 +100,8 @@
             // isLeashReadyForDispatching (used to dispatch the leash of the control) is
             // depending on mGivenInsetsReady. Therefore, triggering notifyControlChanged here
             // again, so that the control with leash can be eventually dispatched
-            if (!mGivenInsetsReady && mServerVisible && !givenInsetsPending) {
+            if (!mGivenInsetsReady && mServerVisible && !givenInsetsPending
+                    && mControlTarget != null) {
                 mGivenInsetsReady = true;
                 ImeTracker.forLogging().onProgress(mStatsToken,
                         ImeTracker.PHASE_WM_POST_LAYOUT_NOTIFY_CONTROLS_CHANGED);
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index d3cae4c..8d7447c 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -379,7 +379,7 @@
         final boolean serverVisibleChanged = mServerVisible != isServerVisible;
         setServerVisible(isServerVisible);
         final boolean positionChanged = updateInsetsControlPosition(windowState);
-        if (mControl != null && !positionChanged
+        if (mControl != null && mControlTarget != null && !positionChanged
                 // The insets hint would be updated if the position is changed. Here updates it for
                 // the possible change of the bounds or the server visibility.
                 && (updateInsetsHint()
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index 3e39a45..6ae2341 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -373,7 +373,7 @@
         array.add(provider);
     }
 
-    void notifyControlChanged(InsetsControlTarget target, InsetsSourceProvider provider) {
+    void notifyControlChanged(@NonNull InsetsControlTarget target, InsetsSourceProvider provider) {
         addToPendingControlMaps(target, provider);
         notifyPendingInsetsControlChanged();
 
diff --git a/tools/systemfeatures/Android.bp b/tools/systemfeatures/Android.bp
index 2ebede3..87ea5db 100644
--- a/tools/systemfeatures/Android.bp
+++ b/tools/systemfeatures/Android.bp
@@ -100,3 +100,72 @@
         unit_test: true,
     },
 }
+
+java_library_host {
+    name: "systemfeatures-errorprone-lib",
+    srcs: [
+        ":systemfeatures-gen-metadata-srcs",
+        "errorprone/java/**/*.java",
+    ],
+    static_libs: [
+        "//external/error_prone:error_prone_core",
+        "guava",
+        "jsr305",
+    ],
+    libs: [
+        "//external/auto:auto_service_annotations",
+    ],
+    javacflags: [
+        // These exports are needed because this errorprone plugin access some private classes
+        // of the java compiler.
+        "--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
+        "--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
+        "--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
+    ],
+    plugins: [
+        "//external/auto:auto_service_plugin",
+    ],
+}
+
+java_plugin {
+    name: "systemfeatures-errorprone",
+    static_libs: ["systemfeatures-errorprone-lib"],
+}
+
+java_test_host {
+    name: "systemfeatures-errorprone-tests",
+    srcs: [
+        "errorprone/tests/java/**/*.java",
+    ],
+    java_resource_dirs: ["tests/src"],
+    java_resources: [
+        ":systemfeatures-errorprone-tests-data",
+    ],
+    static_libs: [
+        "compile-testing-prebuilt",
+        "error_prone_test_helpers",
+        "framework-annotations-lib",
+        "hamcrest",
+        "hamcrest-library",
+        "junit",
+        "systemfeatures-errorprone-lib",
+        "truth",
+    ],
+    test_options: {
+        unit_test: true,
+    },
+}
+
+java_system_features_srcs {
+    name: "systemfeatures-gen-metadata-srcs",
+    full_class_name: "com.android.systemfeatures.RoSystemFeaturesMetadata",
+    metadata_only: true,
+    visibility: ["//visibility:private"],
+}
+
+filegroup {
+    name: "systemfeatures-errorprone-tests-data",
+    path: "tests/src",
+    srcs: ["tests/src/android/**/*.java"],
+    visibility: ["//visibility:private"],
+}
diff --git a/tools/systemfeatures/README.md b/tools/systemfeatures/README.md
index 5836f81..b1fec1a 100644
--- a/tools/systemfeatures/README.md
+++ b/tools/systemfeatures/README.md
@@ -4,8 +4,110 @@
 
 System features exposed from `PackageManager` are defined and aggregated as
 `<feature>` xml attributes across various partitions, and are currently queried
-at runtime through the framework. This directory contains tooling that will
-support *build-time* queries of select system features, enabling optimizations
+at runtime through the framework. This directory contains tooling that supports
+*build-time* queries of select system features, enabling optimizations
 like code stripping and conditionally dependencies when so configured.
 
-### TODO(b/203143243): Expand readme after landing codegen.
+### System Feature Codegen
+
+As not all system features can be fully specified or defined at build time (e.g.
+updatable partitisions and apex modules can change/remove such features), we
+use a conditional, build flag approach that allows a given device to customize
+the subset of build-time defined system features that are immutable and cannot
+be updated.
+
+#### Build Flags
+
+System features that can be fixed at build-time are declared in a common
+location, `build/release/flag_declarations/`. These have the form
+`RELEASE_SYSTEM_FEATURE_${X}`, where `${X}` corresponds to a feature defined in
+`PackageManager`, e.g., `TELEVISION` or `WATCH`.
+
+Build flag values can then be defined per device (or form factor), where such
+values either indicate the existence/version of the system feature, or that the
+feature is unavailable, e.g., for TV, we could define these build flag values:
+```
+name: "RELEASE_SYSTEM_FEATURE_TELEVISION"
+value: {
+  string_value: "0"  # Feature version = 0
+}
+```
+```
+name: "RELEASE_SYSTEM_FEATURE_WATCH"
+value: {
+  string_value: "UNAVAILABLE"
+}
+```
+
+See also [SystemFeaturesGenerator](src/com/android/systemfeatures/SystemFeaturesGenerator.kt)
+for more details.
+
+#### Runtime Queries
+
+Each declared build flag system feature is routed into codegen, generating a
+getter API in the internal class, `com.android.internal.pm.RoSystemFeatures`:
+```
+class RoSystemFeatures {
+    ...
+    public static boolean hasFeatureX(Context context);
+    ...
+}
+```
+By default, these queries simply fall back to the usual
+`PackageManager.hasSystemFeature(...)` runtime queries. However, if a device
+defines these features via build flags, the generated code will add annotations
+indicating fixed value for this query, and adjust the generated code to return
+the value directly. This in turn enables build-time stripping and optimization.
+
+> **_NOTE:_** Any build-time defined system features will also be implicitly
+used to accelerate calls to `PackageManager.hasSystemFeature(...)` for the
+feature, avoiding binder calls when possible.
+
+#### Lint
+
+A new `ErrorProne` rule is introduced to assist with migration and maintenance
+of codegen APIs for build-time defined system features. This is defined in the
+`systemfeatures-errorprone` build rule, which can be added to any Java target's
+`plugins` list.
+
+// TODO(b/203143243): Add plugin to key system targets after initial migration.
+
+1) Add the plugin dependency to a given `${TARGET}`:
+```
+java_library {
+    name: "${TARGET}",
+    plugins: ["systemfeatures-errorprone"],
+}
+```
+2) Run locally:
+```
+RUN_ERROR_PRONE=true m ${TARGET}
+```
+3) (Optional) Update the target rule to generate in-place patch files:
+```
+java_library {
+    name: "${TARGET}",
+    plugins: ["systemfeatures-errorprone"],
+    // DO NOT SUBMIT: GENERATE IN-PLACE PATCH FILES
+    errorprone: {
+        javacflags: [
+            "-XepPatchChecks:RoSystemFeaturesChecker",
+            "-XepPatchLocation:IN_PLACE",
+        ],
+    }
+    ...
+}
+```
+```
+RUN_ERROR_PRONE=true m ${TARGET}
+```
+
+See also [RoSystemFeaturesChecker](errorprone/java/com/android/systemfeatures/errorprone/RoSystemFeaturesChecker.java)
+for more details.
+
+> **_NOTE:_** Not all system feature queries or targets need or should be
+migrated. Only system features that are explicitly declared with build flags,
+and only targets that are built with the platform (i.e., not updatable), are
+candidates for this linting and migration, e.g., SystemUI, System Server, etc...
+
+// TODO(b/203143243): Wrap the in-place lint updates with a simple script for convenience.
diff --git a/tools/systemfeatures/errorprone/java/com/android/systemfeatures/errorprone/RoSystemFeaturesChecker.java b/tools/systemfeatures/errorprone/java/com/android/systemfeatures/errorprone/RoSystemFeaturesChecker.java
new file mode 100644
index 0000000..7812377
--- /dev/null
+++ b/tools/systemfeatures/errorprone/java/com/android/systemfeatures/errorprone/RoSystemFeaturesChecker.java
@@ -0,0 +1,119 @@
+/*
+ * 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 com.android.systemfeatures.errorprone;
+
+import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
+
+import com.android.systemfeatures.RoSystemFeaturesMetadata;
+
+import com.google.auto.service.AutoService;
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker;
+import com.google.errorprone.fixes.SuggestedFix;
+import com.google.errorprone.matchers.Description;
+import com.google.errorprone.matchers.Matcher;
+import com.google.errorprone.matchers.Matchers;
+import com.google.errorprone.util.ASTHelpers;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.MethodInvocationTree;
+import com.sun.tools.javac.code.Symbol;
+
+@AutoService(BugChecker.class)
+@BugPattern(
+        name = "RoSystemFeaturesChecker",
+        summary = "Use RoSystemFeature instead of PackageManager.hasSystemFeature",
+        explanation =
+                "Directly invoking `PackageManager.hasSystemFeature` is less efficient than using"
+                    + " the `RoSystemFeatures` helper class. This check flags invocations like"
+                    + " `context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FOO)`"
+                    + " and suggests replacing them with"
+                    + " `com.android.internal.pm.RoSystemFeatures.hasFeatureFoo(context)`.",
+        severity = WARNING)
+public class RoSystemFeaturesChecker extends BugChecker
+        implements BugChecker.MethodInvocationTreeMatcher {
+
+    private static final String PACKAGE_MANAGER_CLASS = "android.content.pm.PackageManager";
+    private static final String CONTEXT_CLASS = "android.content.Context";
+    private static final String RO_SYSTEM_FEATURE_SIMPLE_CLASS = "RoSystemFeatures";
+    private static final String RO_SYSTEM_FEATURE_CLASS =
+            "com.android.internal.pm." + RO_SYSTEM_FEATURE_SIMPLE_CLASS;
+    private static final String GET_PACKAGE_MANAGER_METHOD = "getPackageManager";
+    private static final String HAS_SYSTEM_FEATURE_METHOD = "hasSystemFeature";
+    private static final String FEATURE_PREFIX = "FEATURE_";
+
+    private static final Matcher<ExpressionTree> HAS_SYSTEM_FEATURE_MATCHER =
+            Matchers.instanceMethod()
+                    .onDescendantOf(PACKAGE_MANAGER_CLASS)
+                    .named(HAS_SYSTEM_FEATURE_METHOD)
+                    .withParameters(String.class.getName());
+
+    private static final Matcher<ExpressionTree> GET_PACKAGE_MANAGER_MATCHER =
+            Matchers.instanceMethod()
+                    .onDescendantOf(CONTEXT_CLASS)
+                    .named(GET_PACKAGE_MANAGER_METHOD);
+
+    @Override
+    public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+        if (!HAS_SYSTEM_FEATURE_MATCHER.matches(tree, state)) {
+            return Description.NO_MATCH;
+        }
+
+        // Check if the PackageManager was obtained from a Context instance.
+        ExpressionTree packageManager = ASTHelpers.getReceiver(tree);
+        if (!GET_PACKAGE_MANAGER_MATCHER.matches(packageManager, state)) {
+            return Description.NO_MATCH;
+        }
+
+        // Get the feature argument and check if it's a PackageManager.FEATURE_X constant.
+        ExpressionTree feature = tree.getArguments().isEmpty() ? null : tree.getArguments().get(0);
+        Symbol featureSymbol = ASTHelpers.getSymbol(feature);
+        if (featureSymbol == null
+                || !featureSymbol.isStatic()
+                || !featureSymbol.getSimpleName().toString().startsWith(FEATURE_PREFIX)
+                || ASTHelpers.enclosingClass(featureSymbol) == null
+                || !ASTHelpers.enclosingClass(featureSymbol)
+                        .getQualifiedName()
+                        .contentEquals(PACKAGE_MANAGER_CLASS)) {
+            return Description.NO_MATCH;
+        }
+
+        // Check if the feature argument is part of the RoSystemFeatures API surface.
+        String featureName = featureSymbol.getSimpleName().toString();
+        String methodName = RoSystemFeaturesMetadata.getMethodNameForFeatureName(featureName);
+        if (methodName == null) {
+            return Description.NO_MATCH;
+        }
+
+        // Generate the appropriate fix.
+        String replacement =
+                String.format(
+                        "%s.%s(%s)",
+                        RO_SYSTEM_FEATURE_SIMPLE_CLASS,
+                        methodName,
+                        state.getSourceForNode(ASTHelpers.getReceiver(packageManager)));
+        // Note that ErrorProne doesn't offer a seamless way of removing the `PackageManager` import
+        // if unused after fix application, so for now we only offer best effort import suggestions.
+        SuggestedFix fix =
+                SuggestedFix.builder()
+                        .replace(tree, replacement)
+                        .addImport(RO_SYSTEM_FEATURE_CLASS)
+                        .removeStaticImport(PACKAGE_MANAGER_CLASS + "." + featureName)
+                        .build();
+        return describeMatch(tree, fix);
+    }
+}
diff --git a/tools/systemfeatures/errorprone/tests/java/com/android/systemfeatures/errorprone/RoSystemFeaturesCheckerTest.java b/tools/systemfeatures/errorprone/tests/java/com/android/systemfeatures/errorprone/RoSystemFeaturesCheckerTest.java
new file mode 100644
index 0000000..c517b24
--- /dev/null
+++ b/tools/systemfeatures/errorprone/tests/java/com/android/systemfeatures/errorprone/RoSystemFeaturesCheckerTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2020 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 com.android.systemfeatures.errorprone;
+
+import com.google.errorprone.BugCheckerRefactoringTestHelper;
+import com.google.errorprone.CompilationTestHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class RoSystemFeaturesCheckerTest {
+    private BugCheckerRefactoringTestHelper mRefactoringHelper;
+    private CompilationTestHelper mCompilationHelper;
+
+    @Before
+    public void setUp() {
+        mCompilationHelper =
+                CompilationTestHelper.newInstance(RoSystemFeaturesChecker.class, getClass());
+        mRefactoringHelper =
+                BugCheckerRefactoringTestHelper.newInstance(
+                        RoSystemFeaturesChecker.class, getClass());
+    }
+
+    @Test
+    public void testNoDiagnostic() {
+        mCompilationHelper
+                .addSourceFile("/android/content/Context.java")
+                .addSourceFile("/android/content/pm/PackageManager.java")
+                .addSourceLines("Example.java",
+                        """
+                        import android.content.Context;
+                        import android.content.pm.PackageManager;
+                        public class Example {
+                          void test(Context context) {
+                            boolean hasCustomFeature = context.getPackageManager()
+                                .hasSystemFeature("my.custom.feature");
+                            boolean hasNonAnnotatedFeature = context.getPackageManager()
+                                .hasSystemFeature(PackageManager.FEATURE_NOT_ANNOTATED);
+                            boolean hasNonRoApiFeature = context.getPackageManager()
+                                .hasSystemFeature(PackageManager.FEATURE_NOT_IN_RO_FEATURE_API);
+                          }
+                        }
+                        """)
+                .doTest();
+    }
+
+    @Test
+    public void testDiagnostic() {
+        mCompilationHelper
+                .addSourceFile("/android/content/Context.java")
+                .addSourceFile("/android/content/pm/PackageManager.java")
+                .addSourceLines("Example.java",
+                        """
+                        import android.content.Context;
+                        import android.content.pm.PackageManager;
+                        public class Example {
+                          void test(Context context) {
+                            boolean hasFeature = context.getPackageManager()
+                            // BUG: Diagnostic contains:
+                                .hasSystemFeature(PackageManager.FEATURE_PC);
+                          }
+                        }
+                        """)
+                .doTest();
+    }
+
+    @Test
+    public void testFix() {
+        mRefactoringHelper
+                .addInputLines("Example.java",
+                        """
+                        import static android.content.pm.PackageManager.FEATURE_WATCH;
+
+                        import android.content.Context;
+                        import android.content.pm.PackageManager;
+                        public class Example {
+                          static class CustomContext extends Context {};
+                          private CustomContext mContext;
+                          void test(Context context) {
+                            boolean hasPc = mContext.getPackageManager()
+                                .hasSystemFeature(PackageManager.FEATURE_PC);
+                            boolean hasWatch = context.getPackageManager()
+                                .hasSystemFeature(FEATURE_WATCH);
+                          }
+                        }
+                        """)
+                .addOutputLines("Example.java",
+                        """
+                        import android.content.Context;
+                        import android.content.pm.PackageManager;
+                        import com.android.internal.pm.RoSystemFeatures;
+                        public class Example {
+                          static class CustomContext extends Context {};
+                          private CustomContext mContext;
+                          void test(Context context) {
+                            boolean hasPc = RoSystemFeatures.hasFeaturePc(mContext);
+                            boolean hasWatch = RoSystemFeatures.hasFeatureWatch(context);
+                          }
+                        }
+                        """)
+                // Don't try compiling the output, as it requires pulling in the full set of code
+                // dependencies.
+                .allowBreakingChanges()
+                .doTest();
+    }
+}
diff --git a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt
index f260e27..ea660b0 100644
--- a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt
+++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt
@@ -53,11 +53,20 @@
  *     public static ArrayMap<String, FeatureInfo> getReadOnlySystemEnabledFeatures();
  * }
  * </pre>
+ *
+ * <p> If `--metadata-only=true` is set, the resulting class would simply be:
+ * <pre>
+ * package com.foo;
+ * public final class RoSystemFeatures {
+ *     public static String getMethodNameForFeatureName(String featureName);
+  * }
+ * </pre>
  */
 object SystemFeaturesGenerator {
     private const val FEATURE_ARG = "--feature="
     private const val FEATURE_APIS_ARG = "--feature-apis="
     private const val READONLY_ARG = "--readonly="
+    private const val METADATA_ONLY_ARG = "--metadata-only="
     private val PACKAGEMANAGER_CLASS = ClassName.get("android.content.pm", "PackageManager")
     private val CONTEXT_CLASS = ClassName.get("android.content", "Context")
     private val FEATUREINFO_CLASS = ClassName.get("android.content.pm", "FeatureInfo")
@@ -84,6 +93,8 @@
         println("                           runtime passthrough API will be generated, regardless")
         println("                           of the `--readonly` flag. This allows decoupling the")
         println("                           API surface from variations in device feature sets.")
+        println("  --metadata-only=true|false Whether to simply output metadata about the")
+        println("                             generated API surface.")
     }
 
     /** Main entrypoint for build-time system feature codegen. */
@@ -106,6 +117,7 @@
         }
 
         var readonly = false
+        var metadataOnly = false
         var outputClassName: ClassName? = null
         val featureArgs = mutableListOf<FeatureInfo>()
         // We could just as easily hardcode this list, as the static API surface should change
@@ -115,6 +127,8 @@
             when {
                 arg.startsWith(READONLY_ARG) ->
                     readonly = arg.substring(READONLY_ARG.length).toBoolean()
+                arg.startsWith(METADATA_ONLY_ARG) ->
+                    metadataOnly = arg.substring(METADATA_ONLY_ARG.length).toBoolean()
                 arg.startsWith(FEATURE_ARG) -> {
                     featureArgs.add(parseFeatureArg(arg))
                 }
@@ -155,9 +169,13 @@
                 .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                 .addJavadoc("@hide")
 
-        addFeatureMethodsToClass(classBuilder, features.values)
-        addMaybeFeatureMethodToClass(classBuilder, features.values)
-        addGetFeaturesMethodToClass(classBuilder, features.values)
+        if (metadataOnly) {
+            addMetadataMethodToClass(classBuilder, features.values)
+        } else {
+            addFeatureMethodsToClass(classBuilder, features.values)
+            addMaybeFeatureMethodToClass(classBuilder, features.values)
+            addGetFeaturesMethodToClass(classBuilder, features.values)
+        }
 
         // TODO(b/203143243): Add validation of build vs runtime values to ensure consistency.
         JavaFile.builder(outputClassName.packageName(), classBuilder.build())
@@ -214,11 +232,8 @@
         features: Collection<FeatureInfo>,
     ) {
         for (feature in features) {
-            // Turn "FEATURE_FOO" into "hasFeatureFoo".
-            val methodName =
-                "has" + CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, feature.name)
             val methodBuilder =
-                MethodSpec.methodBuilder(methodName)
+                MethodSpec.methodBuilder(feature.methodName)
                     .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                     .addJavadoc("Check for ${feature.name}.\n\n@hide")
                     .returns(Boolean::class.java)
@@ -341,5 +356,32 @@
         builder.addMethod(methodBuilder.build())
     }
 
-    private data class FeatureInfo(val name: String, val version: Int?, val readonly: Boolean)
+    /*
+     * Adds a metadata helper method that maps FEATURE_FOO names to their generated hasFeatureFoo()
+     * API counterpart, if defined.
+     */
+    private fun addMetadataMethodToClass(
+        builder: TypeSpec.Builder,
+        features: Collection<FeatureInfo>,
+    ) {
+        val methodBuilder =
+            MethodSpec.methodBuilder("getMethodNameForFeatureName")
+                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
+                .addJavadoc("@return \"hasFeatureFoo\" if FEATURE_FOO is in the API, else null")
+                .returns(String::class.java)
+                .addParameter(String::class.java, "featureVarName")
+
+        methodBuilder.beginControlFlow("switch (featureVarName)")
+        for (feature in features) {
+            methodBuilder.addStatement("case \$S: return \$S", feature.name, feature.methodName)
+        }
+        methodBuilder.addStatement("default: return null").endControlFlow()
+
+        builder.addMethod(methodBuilder.build())
+    }
+
+    private data class FeatureInfo(val name: String, val version: Int?, val readonly: Boolean) {
+        // Turn "FEATURE_FOO" into "hasFeatureFoo".
+        val methodName get() = "has" + CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, name)
+    }
 }
diff --git a/tools/systemfeatures/tests/src/SystemFeaturesMetadataProcessorTest.java b/tools/systemfeatures/tests/src/SystemFeaturesMetadataProcessorTest.java
index 74ce6da..560454b 100644
--- a/tools/systemfeatures/tests/src/SystemFeaturesMetadataProcessorTest.java
+++ b/tools/systemfeatures/tests/src/SystemFeaturesMetadataProcessorTest.java
@@ -36,8 +36,8 @@
     @Test
     public void testSdkFeatureCount() {
         // See the fake PackageManager definition in this directory.
-        // It defines 5 annotated features, and any/all other constants should be ignored.
-        assertThat(SystemFeaturesMetadata.SDK_FEATURE_COUNT).isEqualTo(5);
+        // It defines 6 annotated features, and any/all other constants should be ignored.
+        assertThat(SystemFeaturesMetadata.SDK_FEATURE_COUNT).isEqualTo(6);
     }
 
     @Test
diff --git a/tools/systemfeatures/tests/src/Context.java b/tools/systemfeatures/tests/src/android/content/Context.java
similarity index 100%
rename from tools/systemfeatures/tests/src/Context.java
rename to tools/systemfeatures/tests/src/android/content/Context.java
diff --git a/tools/systemfeatures/tests/src/FeatureInfo.java b/tools/systemfeatures/tests/src/android/content/pm/FeatureInfo.java
similarity index 100%
rename from tools/systemfeatures/tests/src/FeatureInfo.java
rename to tools/systemfeatures/tests/src/android/content/pm/FeatureInfo.java
diff --git a/tools/systemfeatures/tests/src/PackageManager.java b/tools/systemfeatures/tests/src/android/content/pm/PackageManager.java
similarity index 86%
rename from tools/systemfeatures/tests/src/PackageManager.java
rename to tools/systemfeatures/tests/src/android/content/pm/PackageManager.java
index 839a937..4a9edd6 100644
--- a/tools/systemfeatures/tests/src/PackageManager.java
+++ b/tools/systemfeatures/tests/src/android/content/pm/PackageManager.java
@@ -36,6 +36,9 @@
     @SdkConstant(SdkConstantType.FEATURE)
     public static final String FEATURE_WIFI = "wifi";
 
+    @SdkConstant(SdkConstantType.FEATURE)
+    public static final String FEATURE_NOT_IN_RO_FEATURE_API = "not_in_ro_feature_api";
+
     @SdkConstant(SdkConstantType.INTENT_CATEGORY)
     public static final String FEATURE_INTENT_CATEGORY = "intent_category_with_feature_name_prefix";
 
@@ -47,4 +50,9 @@
     public boolean hasSystemFeature(String featureName, int version) {
         return false;
     }
+
+    /** @hide */
+    public boolean hasSystemFeature(String featureName) {
+        return hasSystemFeature(featureName, 0);
+    }
 }
diff --git a/tools/systemfeatures/tests/src/ArrayMap.java b/tools/systemfeatures/tests/src/android/util/ArrayMap.java
similarity index 100%
rename from tools/systemfeatures/tests/src/ArrayMap.java
rename to tools/systemfeatures/tests/src/android/util/ArrayMap.java
diff --git a/tools/systemfeatures/tests/src/ArraySet.java b/tools/systemfeatures/tests/src/android/util/ArraySet.java
similarity index 100%
rename from tools/systemfeatures/tests/src/ArraySet.java
rename to tools/systemfeatures/tests/src/android/util/ArraySet.java