Merge "New aconfig flag for coroutine tracing" into main
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index c012ff3..4e7734c 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -19039,6 +19039,14 @@
             public static final int BATTERY_SAVER_MODE_CUSTOM = 4;
 
             /**
+             Whether 1P apps vote for enabling data during different modes,
+             i.e. BTM, BBSM
+             * @hide
+             */
+            @Readable(maxTargetSdk = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+            public static final String CONNECTIVITY_KEEP_DATA_ON = "wear_connectivity_keep_data_on";
+
+            /**
              * The maximum ambient mode duration when an activity is allowed to auto resume.
              * @hide
              */
diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml
index 709646b..3a2e50a 100644
--- a/core/res/res/xml/sms_short_codes.xml
+++ b/core/res/res/xml/sms_short_codes.xml
@@ -34,7 +34,7 @@
          http://smscoin.net/software/engine/WordPress/Paid+SMS-registration/ -->
 
     <!-- Arab Emirates -->
-    <shortcode country="ae" pattern="\\d{1,5}" free="1017|1355|3214|6253" />
+    <shortcode country="ae" pattern="\\d{1,5}" free="1017|1355|3214" />
 
     <!-- Albania: 5 digits, known short codes listed -->
     <shortcode country="al" pattern="\\d{5}" premium="15191|55[56]00" />
@@ -155,7 +155,7 @@
     <shortcode country="ie" pattern="\\d{5}" premium="5[3-9]\\d{3}" free="50\\d{3}|116\\d{3}" standard="5[12]\\d{3}" />
 
     <!-- Israel: 4 digits, known premium codes listed -->
-    <shortcode country="il" pattern="\\d{1,5}" premium="4422|4545"  free="37477" />
+    <shortcode country="il" pattern="\\d{4}" premium="4422|4545" />
 
     <!-- Italy: 5 digits (premium=41xxx,42xxx), plus EU:
          https://www.itu.int/dms_pub/itu-t/oth/02/02/T020200006B0001PDFE.pdf -->
@@ -198,9 +198,6 @@
     <!-- Malaysia: 5 digits: http://www.skmm.gov.my/attachment/Consumer_Regulation/Mobile_Content_Services_FAQs.pdf -->
     <shortcode country="my" pattern="\\d{5}" premium="32298|33776" free="22099|28288|66668" />
 
-    <!-- Namibia: 5 digits -->
-    <shortcode country="na" pattern="\\d{1,5}" free="40005" />
-
     <!-- The Netherlands, 4 digits, known premium codes listed, plus EU -->
     <shortcode country="nl" pattern="\\d{4}" premium="4466|5040" free="116\\d{3}|2223|6225|2223|1662" />
 
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index 7e8fe7e..d9fe733 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -445,5 +445,6 @@
         VALIDATORS.put(Global.Wearable.PHONE_SWITCHING_SUPPORTED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.Wearable.WEAR_LAUNCHER_UI_MODE, ANY_INTEGER_VALIDATOR);
         VALIDATORS.put(Global.Wearable.WEAR_POWER_ANOMALY_SERVICE_ENABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Global.Wearable.CONNECTIVITY_KEEP_DATA_ON, BOOLEAN_VALIDATOR);
     }
 }
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 2e174e2..c0d83c4 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -686,7 +686,8 @@
                     Settings.Global.Wearable.PHONE_SWITCHING_SUPPORTED,
                     Settings.Global.Wearable.WEAR_MEDIA_CONTROLS_PACKAGE,
                     Settings.Global.Wearable.WEAR_MEDIA_SESSIONS_PACKAGE,
-                    Settings.Global.Wearable.WEAR_POWER_ANOMALY_SERVICE_ENABLED);
+                    Settings.Global.Wearable.WEAR_POWER_ANOMALY_SERVICE_ENABLED,
+                    Settings.Global.Wearable.CONNECTIVITY_KEEP_DATA_ON);
 
     private static final Set<String> BACKUP_DENY_LIST_SECURE_SETTINGS =
              newHashSet(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
index fb50f69..243751fa 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
@@ -76,6 +76,8 @@
     val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState()
     val confirmButtonAppearance by viewModel.confirmButtonAppearance.collectAsState()
     val animateFailure: Boolean by viewModel.animateFailure.collectAsState()
+    val isDigitButtonAnimationEnabled: Boolean by
+        viewModel.isDigitButtonAnimationEnabled.collectAsState()
 
     val buttonScaleAnimatables = remember { List(12) { Animatable(1f) } }
     LaunchedEffect(animateFailure) {
@@ -94,10 +96,11 @@
     ) {
         repeat(9) { index ->
             DigitButton(
-                index + 1,
-                isInputEnabled,
-                viewModel::onPinButtonClicked,
-                buttonScaleAnimatables[index]::value,
+                digit = index + 1,
+                isInputEnabled = isInputEnabled,
+                onClicked = viewModel::onPinButtonClicked,
+                scaling = buttonScaleAnimatables[index]::value,
+                isAnimationEnabled = isDigitButtonAnimationEnabled,
             )
         }
 
@@ -116,10 +119,11 @@
         )
 
         DigitButton(
-            0,
-            isInputEnabled,
-            viewModel::onPinButtonClicked,
-            buttonScaleAnimatables[10]::value,
+            digit = 0,
+            isInputEnabled = isInputEnabled,
+            onClicked = viewModel::onPinButtonClicked,
+            scaling = buttonScaleAnimatables[10]::value,
+            isAnimationEnabled = isDigitButtonAnimationEnabled,
         )
 
         ActionButton(
@@ -143,15 +147,17 @@
     isInputEnabled: Boolean,
     onClicked: (Int) -> Unit,
     scaling: () -> Float,
+    isAnimationEnabled: Boolean,
 ) {
     PinPadButton(
         onClicked = { onClicked(digit) },
         isEnabled = isInputEnabled,
         backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
         foregroundColor = MaterialTheme.colorScheme.onSurfaceVariant,
+        isAnimationEnabled = isAnimationEnabled,
         modifier =
             Modifier.graphicsLayer {
-                val scale = scaling()
+                val scale = if (isAnimationEnabled) scaling() else 1f
                 scaleX = scale
                 scaleY = scale
             }
@@ -195,6 +201,7 @@
         isEnabled = isInputEnabled && !isHidden,
         backgroundColor = backgroundColor,
         foregroundColor = foregroundColor,
+        isAnimationEnabled = true,
         modifier =
             Modifier.graphicsLayer {
                 alpha = hiddenAlpha
@@ -216,6 +223,7 @@
     isEnabled: Boolean,
     backgroundColor: Color,
     foregroundColor: Color,
+    isAnimationEnabled: Boolean,
     modifier: Modifier = Modifier,
     onLongPressed: (() -> Unit)? = null,
     content: @Composable (contentColor: () -> Color) -> Unit,
@@ -243,7 +251,7 @@
 
     val cornerRadius: Dp by
         animateDpAsState(
-            if (isPressed) 24.dp else pinButtonSize / 2,
+            if (isAnimationEnabled && isPressed) 24.dp else pinButtonSize / 2,
             label = "PinButton round corners",
             animationSpec = tween(animDurationMillis, easing = animEasing)
         )
@@ -251,7 +259,7 @@
     val containerColor: Color by
         animateColorAsState(
             when {
-                isPressed -> MaterialTheme.colorScheme.primary
+                isAnimationEnabled && isPressed -> MaterialTheme.colorScheme.primary
                 else -> backgroundColor
             },
             label = "Pin button container color",
@@ -260,7 +268,7 @@
     val contentColor =
         animateColorAsState(
             when {
-                isPressed -> MaterialTheme.colorScheme.onPrimary
+                isAnimationEnabled && isPressed -> MaterialTheme.colorScheme.onPrimary
                 else -> foregroundColor
             },
             label = "Pin button container color",
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index ee3a55f..7769dd9 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -108,6 +108,9 @@
     /** The minimal length of a pattern. */
     val minPatternLength: Int
 
+    /** Whether the "enhanced PIN privacy" setting is enabled for the current user. */
+    val isPinEnhancedPrivacyEnabled: StateFlow<Boolean>
+
     /**
      * Returns the currently-configured authentication method. This determines how the
      * authentication challenge needs to be completed in order to unlock an otherwise locked device.
@@ -212,6 +215,12 @@
 
     override val minPatternLength: Int = LockPatternUtils.MIN_LOCK_PATTERN_SIZE
 
+    override val isPinEnhancedPrivacyEnabled: StateFlow<Boolean> =
+        refreshingFlow(
+            initialValue = true,
+            getFreshValue = { userId -> lockPatternUtils.isPinEnhancedPrivacyEnabled(userId) },
+        )
+
     override suspend fun getAuthenticationMethod(): AuthenticationMethodModel {
         return withContext(backgroundDispatcher) {
             blockingAuthenticationMethodInternal(userRepository.selectedUserId)
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index 1ede530..5eefbf5 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -145,6 +145,9 @@
     val authenticationChallengeResult: SharedFlow<Boolean> =
         repository.authenticationChallengeResult
 
+    /** Whether the "enhanced PIN privacy" setting is enabled for the current user. */
+    val isPinEnhancedPrivacyEnabled: StateFlow<Boolean> = repository.isPinEnhancedPrivacyEnabled
+
     private var throttlingCountdownJob: Job? = null
 
     init {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index 4e1cddc..ff36839 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -92,6 +92,10 @@
     /** Whether the pattern should be visible for the currently-selected user. */
     val isPatternVisible: StateFlow<Boolean> = authenticationInteractor.isPatternVisible
 
+    /** Whether the "enhanced PIN privacy" setting is enabled for the current user. */
+    val isPinEnhancedPrivacyEnabled: StateFlow<Boolean> =
+        authenticationInteractor.isPinEnhancedPrivacyEnabled
+
     /** Whether the user switcher should be displayed within the bouncer UI on large screens. */
     val isUserSwitcherVisible: Boolean
         get() = repository.isUserSwitcherVisible
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index 2ed0d5d..b2b8049 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -84,6 +84,19 @@
 
     override val throttlingMessageId = R.string.kg_too_many_failed_pin_attempts_dialog_message
 
+    /**
+     * Whether the digit buttons should be animated when touched. Note that this doesn't affect the
+     * delete or enter buttons; those should always animate.
+     */
+    val isDigitButtonAnimationEnabled: StateFlow<Boolean> =
+        interactor.isPinEnhancedPrivacyEnabled
+            .map { !it }
+            .stateIn(
+                scope = viewModelScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = !interactor.isPinEnhancedPrivacyEnabled.value,
+            )
+
     /** Notifies that the user clicked on a PIN button with the given digit value. */
     fun onPinButtonClicked(input: Int) {
         val pinInput = mutablePinInput.value
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
index ae2ec2c..87ab5b0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
@@ -143,6 +143,23 @@
             assertThat(authenticationChallengeResults).isEqualTo(listOf(true, false, true))
         }
 
+    @Test
+    fun isPinEnhancedPrivacyEnabled() =
+        testScope.runTest {
+            whenever(lockPatternUtils.isPinEnhancedPrivacyEnabled(USER_INFOS[0].id))
+                .thenReturn(false)
+            whenever(lockPatternUtils.isPinEnhancedPrivacyEnabled(USER_INFOS[1].id))
+                .thenReturn(true)
+
+            val values by collectValues(underTest.isPinEnhancedPrivacyEnabled)
+            assertThat(values.first()).isTrue()
+            assertThat(values.last()).isFalse()
+
+            userRepository.setSelectedUserInfo(USER_INFOS[1])
+            assertThat(values.last()).isTrue()
+
+    }
+
     private fun setSecurityModeAndDispatchBroadcast(
         securityMode: KeyguardSecurityModel.SecurityMode,
     ) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 6da6951..7a9cb6c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -354,6 +354,18 @@
             assertThat(confirmButtonAppearance).isEqualTo(ActionButtonAppearance.Hidden)
         }
 
+    @Test
+    fun isDigitButtonAnimationEnabled() =
+        testScope.runTest {
+            val isAnimationEnabled by collectLastValue(underTest.isDigitButtonAnimationEnabled)
+
+            utils.authenticationRepository.setPinEnhancedPrivacyEnabled(true)
+            assertThat(isAnimationEnabled).isFalse()
+
+            utils.authenticationRepository.setPinEnhancedPrivacyEnabled(false)
+            assertThat(isAnimationEnabled).isTrue()
+        }
+
     private fun TestScope.lockDeviceAndOpenPinBouncer() {
         utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
         utils.deviceEntryRepository.setUnlocked(false)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
index 6e9363b..af1930e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
@@ -60,6 +60,10 @@
 
     override val minPatternLength: Int = 4
 
+    private val _isPinEnhancedPrivacyEnabled = MutableStateFlow(false)
+    override val isPinEnhancedPrivacyEnabled: StateFlow<Boolean> =
+        _isPinEnhancedPrivacyEnabled.asStateFlow()
+
     private var failedAttemptCount = 0
     private var throttlingEndTimestamp = 0L
     private var credentialOverride: List<Any>? = null
@@ -138,6 +142,10 @@
         }
     }
 
+    fun setPinEnhancedPrivacyEnabled(isEnabled: Boolean) {
+        _isPinEnhancedPrivacyEnabled.value = isEnabled
+    }
+
     private fun getExpectedCredential(securityMode: SecurityMode): List<Any> {
         return when (val credentialType = getCurrentCredentialType(securityMode)) {
             LockPatternUtils.CREDENTIAL_TYPE_PIN -> credentialOverride ?: DEFAULT_PIN
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 159c6fd..c638873 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -2514,6 +2514,28 @@
     }
   }
 
+  // Parse the feature flag values. An argument that starts with '@' points to a file to read flag
+  // values from.
+  std::vector<std::string> all_feature_flags_args;
+  for (const std::string& arg : feature_flags_args_) {
+    if (util::StartsWith(arg, "@")) {
+      const std::string path = arg.substr(1, arg.size() - 1);
+      std::string error;
+      if (!file::AppendArgsFromFile(path, &all_feature_flags_args, &error)) {
+        context.GetDiagnostics()->Error(android::DiagMessage(path) << error);
+        return 1;
+      }
+    } else {
+      all_feature_flags_args.push_back(arg);
+    }
+  }
+
+  for (const std::string& arg : all_feature_flags_args) {
+    if (ParseFeatureFlagsParameter(arg, context.GetDiagnostics(), &options_.feature_flag_values)) {
+      return 1;
+    }
+  }
+
   if (context.GetPackageType() != PackageType::kStaticLib && stable_id_file_path_) {
     if (!LoadStableIdMap(context.GetDiagnostics(), stable_id_file_path_.value(),
         &options_.stable_id_map)) {
diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h
index a08f385..26713fd 100644
--- a/tools/aapt2/cmd/Link.h
+++ b/tools/aapt2/cmd/Link.h
@@ -17,11 +17,17 @@
 #ifndef AAPT2_LINK_H
 #define AAPT2_LINK_H
 
+#include <optional>
 #include <regex>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
 
 #include "Command.h"
 #include "Resource.h"
 #include "androidfw/IDiagnostics.h"
+#include "cmd/Util.h"
 #include "format/binary/TableFlattener.h"
 #include "format/proto/ProtoSerialize.h"
 #include "link/ManifestFixer.h"
@@ -72,6 +78,7 @@
   bool use_sparse_encoding = false;
   std::unordered_set<std::string> extensions_to_not_compress;
   std::optional<std::regex> regex_to_not_compress;
+  FeatureFlagValues feature_flag_values;
 
   // Static lib options.
   bool no_static_lib_packages = false;
diff --git a/tools/aapt2/cmd/Util.cpp b/tools/aapt2/cmd/Util.cpp
index a92f24b..678d846 100644
--- a/tools/aapt2/cmd/Util.cpp
+++ b/tools/aapt2/cmd/Util.cpp
@@ -113,6 +113,56 @@
   return std::move(filter);
 }
 
+bool ParseFeatureFlagsParameter(StringPiece arg, android::IDiagnostics* diag,
+                                FeatureFlagValues* out_feature_flag_values) {
+  if (arg.empty()) {
+    return true;
+  }
+
+  for (StringPiece flag_and_value : util::Tokenize(arg, ',')) {
+    std::vector<std::string> parts = util::Split(flag_and_value, '=');
+    if (parts.empty()) {
+      continue;
+    }
+
+    if (parts.size() > 2) {
+      diag->Error(android::DiagMessage()
+                  << "Invalid feature flag and optional value '" << flag_and_value
+                  << "'. Must be in the format 'flag_name[=true|false]");
+      return false;
+    }
+
+    StringPiece flag_name = util::TrimWhitespace(parts[0]);
+    if (flag_name.empty()) {
+      diag->Error(android::DiagMessage() << "No name given for one or more flags in: " << arg);
+      return false;
+    }
+
+    std::optional<bool> flag_value = {};
+    if (parts.size() == 2) {
+      StringPiece str_flag_value = util::TrimWhitespace(parts[1]);
+      if (!str_flag_value.empty()) {
+        flag_value = ResourceUtils::ParseBool(parts[1]);
+        if (!flag_value.has_value()) {
+          diag->Error(android::DiagMessage() << "Invalid value for feature flag '" << flag_and_value
+                                             << "'. Value must be 'true' or 'false'");
+          return false;
+        }
+      }
+    }
+
+    if (auto [it, inserted] =
+            out_feature_flag_values->try_emplace(std::string(flag_name), flag_value);
+        !inserted) {
+      // We are allowing the same flag to appear multiple times, last value wins.
+      diag->Note(android::DiagMessage()
+                 << "Value for feature flag '" << flag_name << "' was given more than once");
+      it->second = flag_value;
+    }
+  }
+  return true;
+}
+
 // Adjust the SplitConstraints so that their SDK version is stripped if it
 // is less than or equal to the minSdk. Otherwise the resources that have had
 // their SDK version stripped due to minSdk won't ever match.
diff --git a/tools/aapt2/cmd/Util.h b/tools/aapt2/cmd/Util.h
index 712c07b..9ece5dd 100644
--- a/tools/aapt2/cmd/Util.h
+++ b/tools/aapt2/cmd/Util.h
@@ -17,8 +17,13 @@
 #ifndef AAPT_SPLIT_UTIL_H
 #define AAPT_SPLIT_UTIL_H
 
+#include <functional>
+#include <map>
+#include <memory>
+#include <optional>
 #include <regex>
 #include <set>
+#include <string>
 #include <unordered_set>
 
 #include "AppInfo.h"
@@ -32,6 +37,8 @@
 
 namespace aapt {
 
+using FeatureFlagValues = std::map<std::string, std::optional<bool>, std::less<>>;
+
 // Parses a configuration density (ex. hdpi, xxhdpi, 234dpi, anydpi, etc).
 // Returns Nothing and logs a human friendly error message if the string was not legal.
 std::optional<uint16_t> ParseTargetDensityParameter(android::StringPiece arg,
@@ -48,6 +55,13 @@
 std::unique_ptr<IConfigFilter> ParseConfigFilterParameters(const std::vector<std::string>& args,
                                                            android::IDiagnostics* diag);
 
+// Parses a feature flags parameter, which can contain one or more pairs of flag names and optional
+// values, and fills in `out_feature_flag_values` with the parsed values. The pairs in the argument
+// are separated by ',' and the name is separated from the value by '=' if there is a value given.
+// Example arg: "flag1=true,flag2=false,flag3=,flag4" where flag3 and flag4 have no given value.
+bool ParseFeatureFlagsParameter(android::StringPiece arg, android::IDiagnostics* diag,
+                                FeatureFlagValues* out_feature_flag_values);
+
 // Adjust the SplitConstraints so that their SDK version is stripped if it
 // is less than or equal to the min_sdk. Otherwise the resources that have had
 // their SDK version stripped due to min_sdk won't ever match.
diff --git a/tools/aapt2/cmd/Util_test.cpp b/tools/aapt2/cmd/Util_test.cpp
index 139bfbc..723d87e 100644
--- a/tools/aapt2/cmd/Util_test.cpp
+++ b/tools/aapt2/cmd/Util_test.cpp
@@ -25,6 +25,7 @@
 #include "util/Files.h"
 
 using ::android::ConfigDescription;
+using testing::Pair;
 using testing::UnorderedElementsAre;
 
 namespace aapt {
@@ -354,6 +355,51 @@
   EXPECT_CONFIG_EQ(constraints, expected_configuration);
 }
 
+TEST(UtilTest, ParseFeatureFlagsParameter_Empty) {
+  auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics();
+  FeatureFlagValues feature_flag_values;
+  ASSERT_TRUE(ParseFeatureFlagsParameter("", diagnostics, &feature_flag_values));
+  EXPECT_TRUE(feature_flag_values.empty());
+}
+
+TEST(UtilTest, ParseFeatureFlagsParameter_TooManyParts) {
+  auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics();
+  FeatureFlagValues feature_flag_values;
+  ASSERT_FALSE(ParseFeatureFlagsParameter("foo=bar=baz", diagnostics, &feature_flag_values));
+}
+
+TEST(UtilTest, ParseFeatureFlagsParameter_NoNameGiven) {
+  auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics();
+  FeatureFlagValues feature_flag_values;
+  ASSERT_FALSE(ParseFeatureFlagsParameter("foo=true,=false", diagnostics, &feature_flag_values));
+}
+
+TEST(UtilTest, ParseFeatureFlagsParameter_InvalidValue) {
+  auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics();
+  FeatureFlagValues feature_flag_values;
+  ASSERT_FALSE(ParseFeatureFlagsParameter("foo=true,bar=42", diagnostics, &feature_flag_values));
+}
+
+TEST(UtilTest, ParseFeatureFlagsParameter_DuplicateFlag) {
+  auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics();
+  FeatureFlagValues feature_flag_values;
+  ASSERT_TRUE(
+      ParseFeatureFlagsParameter("foo=true,bar=true,foo=false", diagnostics, &feature_flag_values));
+  EXPECT_THAT(feature_flag_values, UnorderedElementsAre(Pair("foo", std::optional<bool>(false)),
+                                                        Pair("bar", std::optional<bool>(true))));
+}
+
+TEST(UtilTest, ParseFeatureFlagsParameter_Valid) {
+  auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics();
+  FeatureFlagValues feature_flag_values;
+  ASSERT_TRUE(ParseFeatureFlagsParameter("foo= true, bar =FALSE,baz=, quux", diagnostics,
+                                         &feature_flag_values));
+  EXPECT_THAT(feature_flag_values,
+              UnorderedElementsAre(Pair("foo", std::optional<bool>(true)),
+                                   Pair("bar", std::optional<bool>(false)),
+                                   Pair("baz", std::nullopt), Pair("quux", std::nullopt)));
+}
+
 TEST (UtilTest, AdjustSplitConstraintsForMinSdk) {
   std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
 
diff --git a/tools/lint/README.md b/tools/lint/README.md
index b235ad6..ff8e442 100644
--- a/tools/lint/README.md
+++ b/tools/lint/README.md
@@ -103,10 +103,15 @@
 
 As noted above, this baseline file contains warnings too, which might be undesirable. For example,
 CI tools might surface these warnings in code reviews. In order to create this file without
-warnings, we need to pass another flag to lint: `--nowarn`. The easiest way to do this is to
-locally change the soong code in
-[lint.go](http://cs/aosp-master/build/soong/java/lint.go;l=451;rcl=2e778d5bc4a8d1d77b4f4a3029a4a254ad57db75)
-adding `cmd.Flag("--nowarn")` and running lint again.
+warnings, we need to pass another flag to lint: `--nowarn`. One option is to add the flag to your
+Android.bp file and then run lint again:
+
+```
+  lint: {
+    extra_check_modules: ["AndroidFrameworkLintChecker"],
+    flags: ["--nowarn"],
+  }
+```
 
 # Documentation