Merge changes from topic "flexi-simpin" into main

* changes:
  Add tests for Flexiglass sim pin.
  SimPin for Flexiglass
diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java
index b17d2d1..c6601e8d 100644
--- a/core/java/android/view/KeyEvent.java
+++ b/core/java/android/view/KeyEvent.java
@@ -2065,6 +2065,7 @@
             case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN:
             case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT:
             case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT:
+            case KeyEvent.KEYCODE_STEM_PRIMARY:
                 return true;
         }
 
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index ab9566e..5296b99 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -17,6 +17,13 @@
 }
 
 flag {
+    name: "deduplicate_accessibility_warning_dialog"
+    namespace: "accessibility"
+    description: "Removes duplicate definition of the accessibility warning dialog."
+    bug: "303511250"
+}
+
+flag {
     namespace: "accessibility"
     name: "force_invert_color"
     description: "Enable force force-dark for smart inversion and dark theme everywhere"
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java
index 6497409..2b6913c 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java
@@ -34,6 +34,8 @@
  */
 class AccessibilityServiceTarget extends AccessibilityTarget {
 
+    private final AccessibilityServiceInfo mAccessibilityServiceInfo;
+
     AccessibilityServiceTarget(Context context, @ShortcutType int shortcutType,
             @AccessibilityFragmentType int fragmentType,
             @NonNull AccessibilityServiceInfo serviceInfo) {
@@ -47,6 +49,7 @@
                 serviceInfo.getResolveInfo().loadLabel(context.getPackageManager()),
                 serviceInfo.getResolveInfo().loadIcon(context.getPackageManager()),
                 convertToKey(convertToUserType(shortcutType)));
+        mAccessibilityServiceInfo = serviceInfo;
     }
 
     @Override
@@ -64,4 +67,8 @@
         holder.mLabelView.setEnabled(enabled);
         holder.mStatusView.setEnabled(enabled);
     }
+
+    public AccessibilityServiceInfo getAccessibilityServiceInfo() {
+        return mAccessibilityServiceInfo;
+    }
 }
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceWarning.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceWarning.java
new file mode 100644
index 0000000..0f8ced2
--- /dev/null
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceWarning.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2023 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.internal.accessibility.dialog;
+
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.annotation.SuppressLint;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.text.BidiFormatter;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Locale;
+
+/**
+ * Utility class for creating the dialog that asks the user for explicit permission
+ * before an accessibility service is enabled.
+ */
+public class AccessibilityServiceWarning {
+
+    /**
+     * Returns an {@link AlertDialog} to be shown to confirm that the user
+     * wants to enable an {@link android.accessibilityservice.AccessibilityService}.
+     */
+    public static AlertDialog createAccessibilityServiceWarningDialog(@NonNull Context context,
+            @NonNull AccessibilityServiceInfo info,
+            @NonNull View.OnClickListener allowListener,
+            @NonNull View.OnClickListener denyListener,
+            @NonNull View.OnClickListener uninstallListener) {
+        final AlertDialog ad = new AlertDialog.Builder(context)
+                .setView(createAccessibilityServiceWarningDialogContentView(
+                                context, info, allowListener, denyListener, uninstallListener))
+                .setCancelable(true)
+                .create();
+        Window window = ad.getWindow();
+        WindowManager.LayoutParams params = window.getAttributes();
+        params.privateFlags |= SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+        params.type = WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
+        window.setAttributes(params);
+        return ad;
+    }
+
+    @VisibleForTesting
+    public static View createAccessibilityServiceWarningDialogContentView(Context context,
+            AccessibilityServiceInfo info,
+            View.OnClickListener allowListener,
+            View.OnClickListener denyListener,
+            View.OnClickListener uninstallListener) {
+        final LayoutInflater inflater = context.getSystemService(LayoutInflater.class);
+        final View content = inflater.inflate(R.layout.accessibility_service_warning, null);
+
+        final Drawable icon;
+        if (info.getResolveInfo().getIconResource() == 0) {
+            icon = context.getDrawable(R.drawable.ic_accessibility_generic);
+        } else {
+            icon = info.getResolveInfo().loadIcon(context.getPackageManager());
+        }
+        final ImageView permissionDialogIcon = content.findViewById(
+                R.id.accessibility_permissionDialog_icon);
+        permissionDialogIcon.setImageDrawable(icon);
+
+        final TextView permissionDialogTitle = content.findViewById(
+                R.id.accessibility_permissionDialog_title);
+        permissionDialogTitle.setText(context.getString(R.string.accessibility_enable_service_title,
+                getServiceName(context, info)));
+
+        final Button permissionAllowButton = content.findViewById(
+                R.id.accessibility_permission_enable_allow_button);
+        final Button permissionDenyButton = content.findViewById(
+                R.id.accessibility_permission_enable_deny_button);
+        permissionAllowButton.setOnClickListener(allowListener);
+        permissionAllowButton.setOnTouchListener(getTouchConsumingListener());
+        permissionDenyButton.setOnClickListener(denyListener);
+
+        final Button uninstallButton = content.findViewById(
+                R.id.accessibility_permission_enable_uninstall_button);
+        // Show an uninstall button to help users quickly remove non-preinstalled apps.
+        if (!info.getResolveInfo().serviceInfo.applicationInfo.isSystemApp()) {
+            uninstallButton.setVisibility(View.VISIBLE);
+            uninstallButton.setOnClickListener(uninstallListener);
+        }
+        return content;
+    }
+
+    @VisibleForTesting
+    @SuppressLint("ClickableViewAccessibility") // Touches are intentionally consumed
+    public static View.OnTouchListener getTouchConsumingListener() {
+        return (view, event) -> {
+            // Filter obscured touches by consuming them.
+            if (((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0)
+                    || ((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED) != 0)) {
+                if (event.getAction() == MotionEvent.ACTION_UP) {
+                    Toast.makeText(view.getContext(),
+                            R.string.accessibility_dialog_touch_filtered_warning,
+                            Toast.LENGTH_SHORT).show();
+                }
+                return true;
+            }
+            return false;
+        };
+    }
+
+    // Get the service name and bidi wrap it to protect from bidi side effects.
+    private static CharSequence getServiceName(Context context, AccessibilityServiceInfo info) {
+        final Locale locale = context.getResources().getConfiguration().getLocales().get(0);
+        final CharSequence label =
+                info.getResolveInfo().loadLabel(context.getPackageManager());
+        return BidiFormatter.getInstance(locale).unicodeWrap(label);
+    }
+}
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
index 987c14c..d4eccd4 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
@@ -28,6 +28,7 @@
 import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.AlertDialog;
+import android.app.Dialog;
 import android.app.KeyguardManager;
 import android.content.Context;
 import android.content.DialogInterface;
@@ -56,7 +57,7 @@
             "accessibility_shortcut_menu_mode";
     private final List<AccessibilityTarget> mTargets = new ArrayList<>();
     private AlertDialog mMenuDialog;
-    private AlertDialog mPermissionDialog;
+    private Dialog mPermissionDialog;
     private ShortcutTargetAdapter mTargetAdapter;
 
     @Override
@@ -123,7 +124,7 @@
 
             if (target instanceof AccessibilityServiceTarget) {
                 showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target,
-                        mTargetAdapter);
+                        position, mTargetAdapter);
                 return;
             }
         }
@@ -149,20 +150,43 @@
     }
 
     private void showPermissionDialogIfNeeded(Context context,
-            AccessibilityServiceTarget serviceTarget, ShortcutTargetAdapter targetAdapter) {
+            AccessibilityServiceTarget serviceTarget, int position,
+            ShortcutTargetAdapter targetAdapter) {
         if (mPermissionDialog != null) {
             return;
         }
 
-        mPermissionDialog = new AlertDialog.Builder(context)
-                .setView(createEnableDialogContentView(context, serviceTarget,
-                        v -> {
-                            mPermissionDialog.dismiss();
-                            targetAdapter.notifyDataSetChanged();
-                        },
-                        v -> mPermissionDialog.dismiss()))
-                .setOnDismissListener(dialog -> mPermissionDialog = null)
-                .create();
+        if (Flags.deduplicateAccessibilityWarningDialog()) {
+            mPermissionDialog = AccessibilityServiceWarning
+                    .createAccessibilityServiceWarningDialog(context,
+                            serviceTarget.getAccessibilityServiceInfo(),
+                            v -> {
+                                serviceTarget.onCheckedChanged(true);
+                                targetAdapter.notifyDataSetChanged();
+                                mPermissionDialog.dismiss();
+                            }, v -> {
+                                serviceTarget.onCheckedChanged(false);
+                                mPermissionDialog.dismiss();
+                            },
+                            v -> {
+                                mTargets.remove(position);
+                                context.getPackageManager().getPackageInstaller().uninstall(
+                                        serviceTarget.getComponentName().getPackageName(), null);
+                                targetAdapter.notifyDataSetChanged();
+                                mPermissionDialog.dismiss();
+                            });
+            mPermissionDialog.setOnDismissListener(dialog -> mPermissionDialog = null);
+        } else {
+            mPermissionDialog = new AlertDialog.Builder(context)
+                    .setView(createEnableDialogContentView(context, serviceTarget,
+                            v -> {
+                                mPermissionDialog.dismiss();
+                                targetAdapter.notifyDataSetChanged();
+                            },
+                            v -> mPermissionDialog.dismiss()))
+                    .setOnDismissListener(dialog -> mPermissionDialog = null)
+                    .create();
+        }
         mPermissionDialog.show();
     }
 
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
index 0f85075..51a5ddf 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
@@ -296,6 +296,10 @@
         }
     }
 
+    /**
+     * @deprecated Use {@link AccessibilityServiceWarning}.
+     */
+    @Deprecated
     static View createEnableDialogContentView(Context context,
             AccessibilityServiceTarget target, View.OnClickListener allowListener,
             View.OnClickListener denyListener) {
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 6859f1f..76ae3e0 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -8391,6 +8391,10 @@
             android:exported="true">
         </provider>
 
+        <meta-data
+            android:name="com.android.server.patch.25239169"
+            android:value="true" />
+
     </application>
 
 </manifest>
diff --git a/core/res/res/drawable/ic_accessibility_generic.xml b/core/res/res/drawable/ic_accessibility_generic.xml
new file mode 100644
index 0000000..68a89e6
--- /dev/null
+++ b/core/res/res/drawable/ic_accessibility_generic.xml
@@ -0,0 +1,30 @@
+<!--
+  Copyright 2023 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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24">
+    <path
+        android:pathData="M5.6875,22.8235C4.9092,22.4776 4.8184,22.2615 2.8752,16.1257 1.8439,12.8691 1.0015,10.0882 1.0033,9.946 1.0137,9.1246 1.3166,8.8389 6.25,4.9976 9.2052,2.6966 11.2442,1.1943 11.5332,1.1049 11.8724,0.9999 12.1235,0.996 12.432,1.0907 12.9214,1.2408 22.3634,8.7104 22.6857,9.2024 23.1266,9.8752 23.0768,10.1907 22.0053,13.5155 19.0153,22.7935 19.1481,22.461 18.2853,22.8286 17.7053,23.0757 6.2446,23.0711 5.6875,22.8235Z"
+        android:strokeWidth="0.31999999"
+        android:fillColor="#ced6da"/>
+    <path
+        android:pathData="M10.0615,19.3507C10.028,19.2609 9.9864,17.362 9.9691,15.1308L9.9375,11.0741 8.5,10.853c-2.1981,-0.3381 -2.1924,-0.3355 -2.1619,-0.978 0.0141,-0.2963 0.074,-0.587 0.1331,-0.6462 0.06,-0.06 0.7667,0.0113 1.5994,0.1614 2.1217,0.3824 5.7371,0.3824 7.8588,0 0.8206,-0.1479 1.5349,-0.2259 1.5874,-0.1733 0.0525,0.0526 0.1334,0.3334 0.1799,0.624 0.078,0.4881 0.0598,0.5378 -0.2384,0.6512 -0.1776,0.0675 -1.0143,0.2259 -1.8593,0.352l-1.5364,0.2293 -0.0625,4.182 -0.0625,4.182l-0.625,0 -0.625,0l-0.0625,-1.875 -0.0625,-1.875l-0.5625,0L11.4375,15.6875l-0.0625,1.875 -0.0625,1.875 -0.595,0.0382c-0.4038,0.0259 -0.6146,-0.0143 -0.6559,-0.125zM11.3716,8.912c-0.4861,-0.3351 -0.6133,-0.5622 -0.6176,-1.1029 -0.0047,-0.6005 0.2255,-0.9684 0.739,-1.1811 0.8994,-0.3726 1.7571,0.2075 1.7571,1.1885 0,0.4533 -0.0659,0.5905 -0.4418,0.9206 -0.5007,0.4396 -0.9697,0.4967 -1.4366,0.1749z"
+        android:strokeWidth="0.31999999"
+        android:fillColor="#ffffff"/>
+</vector>
diff --git a/core/res/res/layout/accessibility_service_warning.xml b/core/res/res/layout/accessibility_service_warning.xml
new file mode 100644
index 0000000..0381fac
--- /dev/null
+++ b/core/res/res/layout/accessibility_service_warning.xml
@@ -0,0 +1,137 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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.
+-->
+
+<ScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:textDirection="locale"
+    android:scrollbarStyle="outsideOverlay"
+    android:gravity="top">
+
+    <LinearLayout
+        android:accessibilityDataSensitive="yes"
+        style="@style/AccessibilityDialog">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            android:gravity="center_horizontal"
+            android:paddingLeft="24dp"
+            android:paddingRight="24dp">
+
+            <ImageView
+                android:id="@+id/accessibility_permissionDialog_icon"
+                style="@style/AccessibilityDialogServiceIcon" />
+
+            <TextView
+                android:id="@+id/accessibility_permissionDialog_title"
+                style="@style/AccessibilityDialogTitle" />
+
+            <TextView
+                android:id="@+id/permissionDialog_description"
+                android:text="@string/accessibility_service_warning_description"
+                style="@style/AccessibilityDialogDescription" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal"
+                android:layout_marginBottom="24dp" >
+
+                <ImageView
+                    android:id="@+id/controlScreen_icon"
+                    android:src="@drawable/ic_visibility"
+                    style="@style/AccessibilityDialogIcon" />
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="vertical" >
+
+                    <TextView
+                        android:id="@+id/controlScreen_title"
+                        android:text="@string/accessibility_service_screen_control_title"
+                        style="@style/AccessibilityDialogPermissionTitle" />
+
+                    <TextView
+                        android:id="@+id/controlScreen_description"
+                        android:text="@string/accessibility_service_screen_control_description"
+                        style="@style/AccessibilityDialogPermissionDescription" />
+
+                </LinearLayout>
+
+            </LinearLayout>
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal"
+                android:layout_marginBottom="24dp" >
+
+                <ImageView
+                    android:id="@+id/performAction_icon"
+                    android:src="@drawable/ic_pan_tool"
+                    style="@style/AccessibilityDialogIcon" />
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="vertical" >
+
+                    <TextView
+                        android:id="@+id/performAction_title"
+                        android:text="@string/accessibility_service_action_perform_title"
+                        style="@style/AccessibilityDialogPermissionTitle" />
+
+                    <TextView
+                        android:id="@+id/performAction_description"
+                        android:text="@string/accessibility_service_action_perform_description"
+                        style="@style/AccessibilityDialogPermissionDescription" />
+
+                </LinearLayout>
+
+            </LinearLayout>
+
+        </LinearLayout>
+
+        <!-- Buttons on bottom of dialog -->
+        <LinearLayout
+            style="@style/AccessibilityDialogButtonList">
+
+            <Space
+                style="@style/AccessibilityDialogButtonBarSpace"/>
+
+            <Button
+                android:id="@+id/accessibility_permission_enable_allow_button"
+                android:text="@string/accessibility_dialog_button_allow"
+                style="@style/AccessibilityDialogButton" />
+
+            <Button
+                android:id="@+id/accessibility_permission_enable_deny_button"
+                android:text="@string/accessibility_dialog_button_deny"
+                style="@style/AccessibilityDialogButton" />
+
+            <Button
+                android:id="@+id/accessibility_permission_enable_uninstall_button"
+                android:text="@string/accessibility_dialog_button_uninstall"
+                android:visibility="gone"
+                style="@style/AccessibilityDialogButton" />
+        </LinearLayout>
+    </LinearLayout>
+
+</ScrollView>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index cec83de..eed186a 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -4701,6 +4701,13 @@
     <string name="accessibility_dialog_button_allow">Allow</string>
     <!-- String for the deny button in accessibility permission dialog. [CHAR LIMIT=10] -->
     <string name="accessibility_dialog_button_deny">Deny</string>
+    <!-- String for the uninstall button in accessibility permission dialog. -->
+    <string name="accessibility_dialog_button_uninstall">Uninstall</string>
+    <!-- Warning shown when user input has been blocked due to another app overlaying screen
+         content. Since we don't know what the app is showing on top of the input target, we
+         can't verify user consent. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_dialog_touch_filtered_warning">An app is obscuring the permission
+        request so your response cannot be verified.</string>
 
     <!-- Title for accessibility select shortcut menu dialog. [CHAR LIMIT=100] -->
     <string name="accessibility_select_shortcut_menu_title">Tap a feature to start using it:</string>
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index 13d04e5..619ec31 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -1560,4 +1560,87 @@
     <!-- The default style for input method switch dialog -->
     <style name="InputMethodSwitchDialogStyle" parent="AlertDialog.DeviceDefault">
     </style>
+
+    <style name="AccessibilityDialog">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:orientation">vertical</item>
+        <item name="android:divider">@*android:drawable/list_divider_material</item>
+        <item name="android:showDividers">middle</item>
+    </style>
+
+    <style name="AccessibilityDialogServiceIcon">
+        <item name="android:layout_width">36dp</item>
+        <item name="android:layout_height">36dp</item>
+        <item name="android:layout_marginTop">16dp</item>
+        <item name="android:layout_marginBottom">16dp</item>
+        <item name="android:scaleType">fitCenter</item>
+    </style>
+
+    <style name="AccessibilityDialogIcon">
+        <item name="android:layout_width">18dp</item>
+        <item name="android:layout_height">18dp</item>
+        <item name="android:layout_marginEnd">12dp</item>
+        <item name="android:scaleType">fitCenter</item>
+    </style>
+
+    <style name="AccessibilityDialogTitle"
+           parent="@android:style/TextAppearance.DeviceDefault">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:gravity">center</item>
+        <item name="android:textSize">20sp</item>
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
+    </style>
+
+    <style name="AccessibilityDialogDescription"
+           parent="@android:style/TextAppearance.DeviceDefault">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:layout_marginTop">16dp</item>
+        <item name="android:layout_marginBottom">32dp</item>
+        <item name="android:textSize">16sp</item>
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+    </style>
+
+    <style name="AccessibilityDialogPermissionTitle"
+           parent="@android:style/TextAppearance.DeviceDefault">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:textSize">16sp</item>
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
+    </style>
+
+    <style name="AccessibilityDialogPermissionDescription"
+           parent="@android:style/TextAppearance.DeviceDefault">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:textSize">14sp</item>
+        <item name="android:textColor">?android:attr/textColorSecondary</item>
+    </style>
+
+    <style name="AccessibilityDialogButtonBarSpace">
+        <item name="android:layout_width">0dp</item>
+        <item name="android:layout_height">0dp</item>
+        <item name="android:visibility">gone</item>
+    </style>
+
+    <style name="AccessibilityDialogButtonList">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:orientation">vertical</item>
+        <item name="android:divider">@*android:drawable/list_divider_material</item>
+        <item name="android:showDividers">middle</item>
+    </style>
+
+    <style name="AccessibilityDialogButton"
+           parent="@*android:style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">56dp</item>
+        <item name="android:paddingEnd">8dp</item>
+        <item name="android:paddingStart">8dp</item>
+        <item name="android:background">?android:attr/selectableItemBackground</item>
+    </style>
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 16ad5c9..f3aa936 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3622,11 +3622,14 @@
   <java-symbol type="string" name="accessibility_uncheck_legacy_item_warning" />
 
   <java-symbol type="layout" name="accessibility_enable_service_warning" />
+  <java-symbol type="layout" name="accessibility_service_warning" />
   <java-symbol type="id" name="accessibility_permissionDialog_icon" />
   <java-symbol type="id" name="accessibility_permissionDialog_title" />
   <java-symbol type="id" name="accessibility_permission_enable_allow_button" />
   <java-symbol type="id" name="accessibility_permission_enable_deny_button" />
+  <java-symbol type="id" name="accessibility_permission_enable_uninstall_button" />
   <java-symbol type="string" name="accessibility_enable_service_title" />
+  <java-symbol type="string" name="accessibility_dialog_touch_filtered_warning" />
 
   <java-symbol type="layout" name="accessibility_shortcut_chooser_item" />
   <java-symbol type="id" name="accessibility_shortcut_target_checkbox" />
@@ -3655,6 +3658,7 @@
 
   <java-symbol type="drawable" name="ic_accessibility_color_inversion" />
   <java-symbol type="drawable" name="ic_accessibility_color_correction" />
+  <java-symbol type="drawable" name="ic_accessibility_generic" />
   <java-symbol type="drawable" name="ic_accessibility_hearing_aid" />
   <java-symbol type="drawable" name="ic_accessibility_magnification" />
   <java-symbol type="drawable" name="ic_accessibility_reduce_bright_colors" />
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index 20d8d91..62d58b6 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -165,6 +165,7 @@
 
     <!-- AccessibilityShortcutChooserActivityTest permissions -->
     <uses-permission android:name="android.permission.MANAGE_ACCESSIBILITY" />
+    <uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW" />
 
     <application
         android:theme="@style/Theme"
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
index dd116b5..088b57f 100644
--- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
@@ -44,14 +44,19 @@
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.app.AlertDialog;
 import android.app.KeyguardManager;
+import android.app.UiAutomation;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
 import android.content.pm.ParceledListSlice;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
+import android.graphics.Rect;
+import android.os.Bundle;
 import android.os.Handler;
+import android.platform.test.annotations.RequiresFlagsDisabled;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -62,6 +67,7 @@
 import android.view.View;
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.Flags;
 import android.view.accessibility.IAccessibilityManager;
 
@@ -90,12 +96,17 @@
 @RunWith(AndroidJUnit4.class)
 public class AccessibilityShortcutChooserActivityTest {
     private static final String ONE_HANDED_MODE = "One-Handed mode";
+    private static final String ALLOW_LABEL = "Allow";
     private static final String DENY_LABEL = "Deny";
+    private static final String UNINSTALL_LABEL = "Uninstall";
     private static final String EDIT_LABEL = "Edit shortcuts";
     private static final String LIST_TITLE_LABEL = "Choose features to use";
     private static final String TEST_LABEL = "TEST_LABEL";
-    private static final ComponentName TEST_COMPONENT_NAME = new ComponentName("package", "class");
+    private static final String TEST_PACKAGE = "TEST_LABEL";
+    private static final ComponentName TEST_COMPONENT_NAME = new ComponentName(TEST_PACKAGE,
+            "class");
     private static final long UI_TIMEOUT_MS = 1000;
+    private UiAutomation mUiAutomation;
     private UiDevice mDevice;
     private ActivityScenario<TestAccessibilityShortcutChooserActivity> mScenario;
     private TestAccessibilityShortcutChooserActivity mActivity;
@@ -117,6 +128,10 @@
     private IAccessibilityManager mAccessibilityManagerService;
     @Mock
     private KeyguardManager mKeyguardManager;
+    @Mock
+    private PackageManager mPackageManager;
+    @Mock
+    private PackageInstaller mPackageInstaller;
 
     @Before
     public void setUp() throws Exception {
@@ -125,6 +140,7 @@
         assumeFalse("AccessibilityShortcutChooserActivity not supported on watch",
                 pm.hasSystemFeature(PackageManager.FEATURE_WATCH));
 
+        mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
         mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
         mDevice.wakeUp();
         when(mAccessibilityServiceInfo.getResolveInfo()).thenReturn(mResolveInfo);
@@ -134,12 +150,15 @@
         when(mAccessibilityServiceInfo.getComponentName()).thenReturn(TEST_COMPONENT_NAME);
         when(mAccessibilityManagerService.getInstalledAccessibilityServiceList(
                 anyInt())).thenReturn(new ParceledListSlice<>(
-                        Collections.singletonList(mAccessibilityServiceInfo)));
+                Collections.singletonList(mAccessibilityServiceInfo)));
         when(mAccessibilityManagerService.isAccessibilityTargetAllowed(
                 anyString(), anyInt(), anyInt())).thenReturn(true);
         when(mKeyguardManager.isKeyguardLocked()).thenReturn(false);
+        when(mPackageManager.getPackageInstaller()).thenReturn(mPackageInstaller);
+
         TestAccessibilityShortcutChooserActivity.setupForTesting(
-                mAccessibilityManagerService, mKeyguardManager);
+                mAccessibilityManagerService, mKeyguardManager,
+                mPackageManager);
     }
 
     @After
@@ -150,18 +169,12 @@
     }
 
     @Test
-    public void doubleClickTestServiceAndClickDenyButton_permissionDialogDoesNotExist() {
+    @RequiresFlagsDisabled(Flags.FLAG_DEDUPLICATE_ACCESSIBILITY_WARNING_DIALOG)
+    public void selectTestService_oldPermissionDialog_deny_dialogIsHidden() {
         launchActivity();
         openShortcutsList();
 
-        // Performing the double-click is flaky so retry if needed.
-        for (int attempt = 1; attempt <= 2; attempt++) {
-            onView(withText(TEST_LABEL)).perform(scrollTo(), doubleClick());
-            if (mDevice.wait(Until.hasObject(By.text(DENY_LABEL)), UI_TIMEOUT_MS)) {
-                break;
-            }
-        }
-
+        mDevice.findObject(By.text(TEST_LABEL)).clickAndWait(Until.newWindow(), UI_TIMEOUT_MS);
         onView(withText(DENY_LABEL)).perform(scrollTo(), click());
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
@@ -170,6 +183,50 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DEDUPLICATE_ACCESSIBILITY_WARNING_DIALOG)
+    public void selectTestService_permissionDialog_allow_rowChecked() {
+        launchActivity();
+        openShortcutsList();
+
+        mDevice.findObject(By.text(TEST_LABEL)).clickAndWait(Until.newWindow(), UI_TIMEOUT_MS);
+        clickSystemDialogButton(ALLOW_LABEL);
+
+        assertThat(mDevice.wait(Until.hasObject(By.textStartsWith(LIST_TITLE_LABEL)),
+                UI_TIMEOUT_MS)).isTrue();
+        assertThat(mDevice.wait(Until.hasObject(By.checked(true)), UI_TIMEOUT_MS)).isTrue();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DEDUPLICATE_ACCESSIBILITY_WARNING_DIALOG)
+    public void selectTestService_permissionDialog_deny_rowNotChecked() {
+        launchActivity();
+        openShortcutsList();
+
+        mDevice.findObject(By.text(TEST_LABEL)).clickAndWait(Until.newWindow(), UI_TIMEOUT_MS);
+        clickSystemDialogButton(DENY_LABEL);
+
+        assertThat(mDevice.wait(Until.hasObject(By.textStartsWith(LIST_TITLE_LABEL)),
+                UI_TIMEOUT_MS)).isTrue();
+        assertThat(mDevice.wait(Until.hasObject(By.checked(true)), UI_TIMEOUT_MS)).isFalse();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DEDUPLICATE_ACCESSIBILITY_WARNING_DIALOG)
+    public void selectTestService_permissionDialog_uninstall_callsUninstaller_rowRemoved() {
+        launchActivity();
+        openShortcutsList();
+
+        mDevice.findObject(By.text(TEST_LABEL)).clickAndWait(Until.newWindow(), UI_TIMEOUT_MS);
+        clickSystemDialogButton(UNINSTALL_LABEL);
+
+        verify(mPackageInstaller).uninstall(eq(TEST_PACKAGE), any());
+        assertThat(mDevice.wait(Until.hasObject(By.textStartsWith(LIST_TITLE_LABEL)),
+                UI_TIMEOUT_MS)).isTrue();
+        assertThat(mDevice.wait(Until.hasObject(By.textStartsWith(TEST_LABEL)),
+                UI_TIMEOUT_MS)).isFalse();
+    }
+
+    @Test
     public void clickServiceTarget_notPermittedByAdmin_sendRestrictedDialogIntent()
             throws Exception {
         when(mAccessibilityManagerService.isAccessibilityTargetAllowed(
@@ -239,6 +296,18 @@
         mDevice.wait(Until.hasObject(By.textStartsWith(LIST_TITLE_LABEL)), UI_TIMEOUT_MS);
     }
 
+    private void clickSystemDialogButton(String dialogButtonText) {
+        // Use UiAutomation to find the button because UiDevice struggles to find
+        // a UI element in a system dialog.
+        final AccessibilityNodeInfo button =
+                mUiAutomation.getRootInActiveWindow()
+                        .findAccessibilityNodeInfosByText(dialogButtonText).stream()
+                        .filter(AccessibilityNodeInfo::isClickable).findFirst().get();
+        final Rect bounds = new Rect();
+        button.getBoundsInScreen(bounds);
+        mDevice.click(bounds.centerX(), bounds.centerY());
+    }
+
     /**
      * Used for testing.
      */
@@ -246,12 +315,30 @@
             AccessibilityShortcutChooserActivity {
         private static IAccessibilityManager sAccessibilityManagerService;
         private static KeyguardManager sKeyguardManager;
+        private static PackageManager sPackageManager;
 
         public static void setupForTesting(
                 IAccessibilityManager accessibilityManagerService,
-                KeyguardManager keyguardManager) {
+                KeyguardManager keyguardManager,
+                PackageManager packageManager) {
             sAccessibilityManagerService = accessibilityManagerService;
             sKeyguardManager = keyguardManager;
+            sPackageManager = packageManager;
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            if (Flags.deduplicateAccessibilityWarningDialog()) {
+                // Setting the Theme is necessary here for the dialog to use the proper style
+                // resources as designated in its layout XML.
+                setTheme(R.style.Theme_DeviceDefault_DayNight);
+            }
+        }
+
+        @Override
+        public PackageManager getPackageManager() {
+            return sPackageManager;
         }
 
         @Override
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java b/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java
new file mode 100644
index 0000000..b76dd51
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2023 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.internal.accessibility.dialog;
+
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.os.RemoteException;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.widget.TextView;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.internal.R;
+import com.android.internal.accessibility.TestUtils;
+
+import com.google.common.truth.Expect;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Unit Tests for
+ * {@link com.android.internal.accessibility.dialog.AccessibilityServiceWarning}
+ */
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+@RequiresFlagsEnabled(
+        android.view.accessibility.Flags.FLAG_DEDUPLICATE_ACCESSIBILITY_WARNING_DIALOG)
+public class AccessibilityServiceWarningTest {
+    private static final String A11Y_SERVICE_PACKAGE_LABEL = "TestA11yService";
+    private static final String A11Y_SERVICE_SUMMARY = "TestA11yService summary";
+    private static final String A11Y_SERVICE_COMPONENT_NAME =
+            "fake.package/test.a11yservice.name";
+
+    private Context mContext;
+    private AccessibilityServiceInfo mAccessibilityServiceInfo;
+    private AtomicBoolean mAllowListener;
+    private AtomicBoolean mDenyListener;
+    private AtomicBoolean mUninstallListener;
+
+    @Rule
+    public final Expect expect = Expect.create();
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @Before
+    public void setUp() throws RemoteException {
+        MockitoAnnotations.initMocks(this);
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        mAccessibilityServiceInfo = TestUtils.createFakeServiceInfo(
+                A11Y_SERVICE_PACKAGE_LABEL,
+                A11Y_SERVICE_SUMMARY,
+                A11Y_SERVICE_COMPONENT_NAME,
+                /* isAlwaysOnService*/ false);
+        mAllowListener = new AtomicBoolean(false);
+        mDenyListener = new AtomicBoolean(false);
+        mUninstallListener = new AtomicBoolean(false);
+    }
+
+    @Test
+    public void createAccessibilityServiceWarningDialog_hasExpectedWindowParams() {
+        final AlertDialog dialog =
+                AccessibilityServiceWarning.createAccessibilityServiceWarningDialog(
+                        mContext,
+                        mAccessibilityServiceInfo,
+                        null, null, null);
+        final Window dialogWindow = dialog.getWindow();
+        assertThat(dialogWindow).isNotNull();
+
+        expect.that(dialogWindow.getAttributes().privateFlags
+                & SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS).isEqualTo(
+                SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+        expect.that(dialogWindow.getAttributes().type).isEqualTo(TYPE_SYSTEM_DIALOG);
+    }
+
+    @Test
+    public void createAccessibilityServiceWarningDialog_hasExpectedServiceName() {
+        final TextView title = createDialogContentView().findViewById(
+                R.id.accessibility_permissionDialog_title);
+        assertThat(title).isNotNull();
+
+        assertThat(title.getText().toString()).contains(A11Y_SERVICE_PACKAGE_LABEL);
+    }
+
+    @Test
+    public void createAccessibilityServiceWarningDialog_clickAllow() {
+        final View allowButton = createDialogContentView().findViewById(
+                R.id.accessibility_permission_enable_allow_button);
+        assertThat(allowButton).isNotNull();
+
+        allowButton.performClick();
+
+        expect.that(mAllowListener.get()).isTrue();
+        expect.that(mDenyListener.get()).isFalse();
+        expect.that(mUninstallListener.get()).isFalse();
+    }
+
+    @Test
+    public void createAccessibilityServiceWarningDialog_clickDeny() {
+        final View denyButton = createDialogContentView().findViewById(
+                R.id.accessibility_permission_enable_deny_button);
+        assertThat(denyButton).isNotNull();
+
+        denyButton.performClick();
+
+        expect.that(mAllowListener.get()).isFalse();
+        expect.that(mDenyListener.get()).isTrue();
+        expect.that(mUninstallListener.get()).isFalse();
+    }
+
+    @Test
+    public void createAccessibilityServiceWarningDialog_clickUninstall() {
+        final View uninstallButton = createDialogContentView().findViewById(
+                R.id.accessibility_permission_enable_uninstall_button);
+        assertThat(uninstallButton).isNotNull();
+
+        uninstallButton.performClick();
+
+        expect.that(mAllowListener.get()).isFalse();
+        expect.that(mDenyListener.get()).isFalse();
+        expect.that(mUninstallListener.get()).isTrue();
+    }
+
+    @Test
+    public void getTouchConsumingListener() {
+        final View allowButton = createDialogContentView().findViewById(
+                R.id.accessibility_permission_enable_allow_button);
+        assertThat(allowButton).isNotNull();
+        final View.OnTouchListener listener =
+                AccessibilityServiceWarning.getTouchConsumingListener();
+
+        expect.that(listener.onTouch(allowButton, createMotionEvent(0))).isFalse();
+        expect.that(listener.onTouch(allowButton,
+                createMotionEvent(MotionEvent.FLAG_WINDOW_IS_OBSCURED))).isTrue();
+        expect.that(listener.onTouch(allowButton,
+                createMotionEvent(MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED))).isTrue();
+    }
+
+    private View createDialogContentView() {
+        return AccessibilityServiceWarning.createAccessibilityServiceWarningDialogContentView(
+                mContext,
+                mAccessibilityServiceInfo,
+                (v) -> mAllowListener.set(true),
+                (v) -> mDenyListener.set(true),
+                (v) -> mUninstallListener.set(true));
+    }
+
+    private MotionEvent createMotionEvent(int flags) {
+        MotionEvent.PointerProperties[] props = new MotionEvent.PointerProperties[]{
+                new MotionEvent.PointerProperties()
+        };
+        MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[]{
+                new MotionEvent.PointerCoords()
+        };
+        return MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1, props, coords,
+                0, 0, 0, 0, -1, 0, InputDevice.SOURCE_TOUCHSCREEN, flags);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 36e18b3..dedac55 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -604,7 +604,6 @@
                     mAodIconsBindHandle.dispose();
                 }
                 if (nic != null) {
-                    nic.setOnLockScreen(true);
                     final DisposableHandle viewHandle = NotificationIconContainerViewBinder.bind(
                             nic,
                             mAodIconsViewModel,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
index c438e49..12de185 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
@@ -87,7 +87,6 @@
         }
 
         if (NotificationIconContainerRefactor.isEnabled) {
-            nic.setOnLockScreen(true)
             nicBindingDisposable?.dispose()
             nicBindingDisposable =
                 NotificationIconContainerViewBinder.bind(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 0b6e400..748ad92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -92,6 +92,7 @@
     private int mIndexOfFirstViewInShelf = -1;
     private float mCornerAnimationDistance;
     private float mActualWidth = -1;
+    private int mMaxIconsOnLockscreen;
     private final RefactorFlag mSensitiveRevealAnim =
             RefactorFlag.forView(Flags.SENSITIVE_REVEAL_ANIM);
     private boolean mCanModifyColorOfNotifications;
@@ -136,6 +137,7 @@
         Resources res = getResources();
         mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext);
         mPaddingBetweenElements = res.getDimensionPixelSize(R.dimen.notification_divider_height);
+        mMaxIconsOnLockscreen = res.getInteger(R.integer.max_notif_icons_on_lockscreen);
 
         ViewGroup.LayoutParams layoutParams = getLayoutParams();
         final int newShelfHeight = res.getDimensionPixelOffset(R.dimen.notification_shelf_height);
@@ -271,6 +273,7 @@
     }
 
     private int getSpeedBumpIndex() {
+        NotificationIconContainerRefactor.assertInLegacyMode();
         return mHostLayout.getSpeedBumpIndex();
     }
 
@@ -280,6 +283,7 @@
      */
     @VisibleForTesting
     public void updateActualWidth(float fractionToShade, float shortestWidth) {
+        NotificationIconContainerRefactor.assertInLegacyMode();
         final float actualWidth = mAmbientState.isOnKeyguard()
                 ? MathUtils.lerp(shortestWidth, getWidth(), fractionToShade)
                 : getWidth();
@@ -290,6 +294,15 @@
         mActualWidth = actualWidth;
     }
 
+    private void setActualWidth(float actualWidth) {
+        if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return;
+        setBackgroundWidth((int) actualWidth);
+        if (mShelfIcons != null) {
+            mShelfIcons.setActualLayoutWidth((int) actualWidth);
+        }
+        mActualWidth = actualWidth;
+    }
+
     @Override
     public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
         super.getBoundsOnScreen(outRect, clipToParent);
@@ -467,12 +480,26 @@
 
         final float fractionToShade = Interpolators.STANDARD.getInterpolation(
                 mAmbientState.getFractionToShade());
-        final float shortestWidth = mShelfIcons.calculateWidthFor(numViewsInShelf);
-        updateActualWidth(fractionToShade, shortestWidth);
+
+        if (NotificationIconContainerRefactor.isEnabled()) {
+            if (mAmbientState.isOnKeyguard()) {
+                float numViews = MathUtils.min(numViewsInShelf, mMaxIconsOnLockscreen + 1);
+                float shortestWidth = mShelfIcons.calculateWidthFor(numViews);
+                float actualWidth = MathUtils.lerp(shortestWidth, getWidth(), fractionToShade);
+                setActualWidth(actualWidth);
+            } else {
+                setActualWidth(getWidth());
+            }
+        } else {
+            final float shortestWidth = mShelfIcons.calculateWidthFor(numViewsInShelf);
+            updateActualWidth(fractionToShade, shortestWidth);
+        }
 
         // TODO(b/172289889) transition last icon in shelf to notification icon and vice versa.
         setVisibility(isHidden ? View.INVISIBLE : View.VISIBLE);
-        mShelfIcons.setSpeedBumpIndex(getSpeedBumpIndex());
+        if (!NotificationIconContainerRefactor.isEnabled()) {
+            mShelfIcons.setSpeedBumpIndex(getSpeedBumpIndex());
+        }
         mShelfIcons.calculateIconXTranslations();
         mShelfIcons.applyIconStates();
         for (int i = 0; i < getHostLayoutChildCount(); i++) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
index f8bc0ee..a85c440 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
@@ -25,9 +25,7 @@
 import com.android.internal.policy.SystemBarUtils
 import com.android.internal.statusbar.StatusBarIcon
 import com.android.internal.util.ContrastColorUtil
-import com.android.systemui.CoreStartable
 import com.android.systemui.common.ui.ConfigurationState
-import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.StatusBarIconView
@@ -39,21 +37,15 @@
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconsViewData
-import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconsViewData.LimitType
 import com.android.systemui.statusbar.phone.NotificationIconContainer
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.onConfigChanged
-import com.android.systemui.util.asIndenting
 import com.android.systemui.util.kotlin.mapValuesNotNullTo
 import com.android.systemui.util.kotlin.stateFlow
-import com.android.systemui.util.printCollection
 import com.android.systemui.util.ui.isAnimating
 import com.android.systemui.util.ui.stopAnimating
 import com.android.systemui.util.ui.value
-import dagger.Binds
-import dagger.multibindings.ClassKey
-import dagger.multibindings.IntoMap
-import java.io.PrintWriter
 import javax.inject.Inject
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.Job
@@ -134,6 +126,7 @@
     ): DisposableHandle {
         return view.repeatWhenAttached {
             lifecycleScope.launch {
+                view.setUseIncreasedIconScale(true)
                 launch {
                     viewModel.icons.bindIcons(
                         view,
@@ -229,7 +222,6 @@
                 ->
                 FrameLayout.LayoutParams(iconSize + 2 * iconHPadding, statusBarHeight)
             }
-
         val failedBindings = mutableSetOf<String>()
         val boundViewsByNotifKey = ArrayMap<String, Pair<StatusBarIconView, Job>>()
         var prevIcons = NotificationIconsViewData()
@@ -237,64 +229,111 @@
             val iconsDiff = NotificationIconsViewData.computeDifference(iconsData, prevIcons)
             prevIcons = iconsData
 
+            // Lookup 1:1 group icon replacements
             val replacingIcons: ArrayMap<String, StatusBarIcon> =
-                iconsDiff.groupReplacements.mapValuesNotNullTo(ArrayMap()) { (_, info) ->
-                    boundViewsByNotifKey[info.notifKey]?.first?.statusBarIcon
+                iconsDiff.groupReplacements.mapValuesNotNullTo(ArrayMap()) { (_, notifKey) ->
+                    boundViewsByNotifKey[notifKey]?.first?.statusBarIcon
                 }
-            view.setReplacingIcons(replacingIcons)
-
-            for (notifKey in iconsDiff.removed) {
-                failedBindings.remove(notifKey)
-                val (child, job) = boundViewsByNotifKey.remove(notifKey) ?: continue
-                view.removeView(child)
-                job.cancel()
-            }
-
-            val toAdd: Sequence<String> =
-                iconsDiff.added.asSequence().map { it.notifKey } + failedBindings
-            for ((idx, notifKey) in toAdd.withIndex()) {
-                val sbiv = viewStore.iconView(notifKey)
-                if (sbiv == null) {
-                    failedBindings.add(notifKey)
-                    continue
+            view.withIconReplacements(replacingIcons) {
+                // Remove and unbind.
+                for (notifKey in iconsDiff.removed) {
+                    failedBindings.remove(notifKey)
+                    val (child, job) = boundViewsByNotifKey.remove(notifKey) ?: continue
+                    view.removeView(child)
+                    job.cancel()
                 }
-                // The view might still be transiently added if it was just removed and added again
-                view.removeTransientView(sbiv)
-                view.addView(sbiv, idx)
-                boundViewsByNotifKey.remove(notifKey)?.second?.cancel()
-                boundViewsByNotifKey[notifKey] =
-                    Pair(
-                        sbiv,
-                        launch {
-                            launch { layoutParams.collect { sbiv.layoutParams = it } }
-                            bindIcon(notifKey, sbiv)
-                        },
-                    )
-            }
 
-            notifyBindingFailures(failedBindings)
-
-            view.setChangingViewPositions(true)
-
-            // Re-sort notification icons
-            val expectedChildren =
-                iconsData.visibleKeys.mapNotNull { boundViewsByNotifKey[it.notifKey]?.first }
-            val childCount = view.childCount
-            for (i in 0 until childCount) {
-                val actual = view.getChildAt(i)
-                val expected = expectedChildren[i]
-                if (actual === expected) {
-                    continue
+                // Add and bind.
+                val toAdd: Sequence<String> = iconsDiff.added.asSequence() + failedBindings
+                for ((idx, notifKey) in toAdd.withIndex()) {
+                    // Lookup the StatusBarIconView from the store.
+                    val sbiv = viewStore.iconView(notifKey)
+                    if (sbiv == null) {
+                        failedBindings.add(notifKey)
+                        continue
+                    }
+                    // The view might still be transiently added if it was just removed and added
+                    // again
+                    view.removeTransientView(sbiv)
+                    view.addView(sbiv, idx)
+                    boundViewsByNotifKey.remove(notifKey)?.second?.cancel()
+                    boundViewsByNotifKey[notifKey] =
+                        Pair(
+                            sbiv,
+                            launch {
+                                launch { layoutParams.collect { sbiv.layoutParams = it } }
+                                bindIcon(notifKey, sbiv)
+                            },
+                        )
                 }
-                view.removeView(expected)
-                view.addView(expected, i)
-            }
-            view.setChangingViewPositions(false)
 
-            view.setReplacingIcons(null)
+                // Set the maximum number of icons to show in the container. Any icons over this
+                // amount will render as an "overflow dot".
+                val maxIconsAmount: Int =
+                    when (iconsData.limitType) {
+                        LimitType.MaximumIndex -> {
+                            iconsData.visibleIcons
+                                .asSequence()
+                                .take(iconsData.iconLimit)
+                                .count { info -> info.notifKey in boundViewsByNotifKey }
+                        }
+                        LimitType.MaximumAmount -> {
+                            iconsData.iconLimit
+                        }
+                    }
+                view.setMaxIconsAmount(maxIconsAmount)
+
+                // Track the binding failures so that they appear in dumpsys.
+                notifyBindingFailures(failedBindings)
+
+                // Re-sort notification icons
+                view.changeViewPositions {
+                    val expectedChildren: List<StatusBarIconView> =
+                        iconsData.visibleIcons.mapNotNull {
+                            boundViewsByNotifKey[it.notifKey]?.first
+                        }
+                    val childCount = view.childCount
+                    for (i in 0 until childCount) {
+                        val actual = view.getChildAt(i)
+                        val expected = expectedChildren[i]
+                        if (actual === expected) {
+                            continue
+                        }
+                        view.removeView(expected)
+                        view.addView(expected, i)
+                    }
+                }
+            }
+            // Recalculate all icon positions, to reflect our updates.
+            view.calculateIconXTranslations()
         }
     }
 
+    /**
+     * Track which groups are being replaced with a different icon instance, but with the same
+     * visual icon. This prevents a weird animation where it looks like an icon disappears and
+     * reappears unchanged.
+     */
+    // TODO(b/305739416): Ideally we wouldn't swap out the StatusBarIconView at all, and instead use
+    //  a single SBIV instance for the group. Then this whole concept can go away.
+    private inline fun <R> NotificationIconContainer.withIconReplacements(
+        replacements: ArrayMap<String, StatusBarIcon>,
+        block: () -> R
+    ): R {
+        setReplacingIcons(replacements)
+        return block().also { setReplacingIcons(null) }
+    }
+
+    /**
+     * Any invocations of [NotificationIconContainer.addView] /
+     * [NotificationIconContainer.removeView] inside of [block] will not cause a new add / remove
+     * animation.
+     */
+    private inline fun <R> NotificationIconContainer.changeViewPositions(block: () -> R): R {
+        setChangingViewPositions(true)
+        return block().also { setChangingViewPositions(false) }
+    }
+
     /** External storage for [StatusBarIconView] instances. */
     fun interface IconViewStore {
         fun iconView(key: String): StatusBarIconView?
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
index b11eca2c..9cb60d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
@@ -15,10 +15,13 @@
  */
 package com.android.systemui.statusbar.notification.icon.ui.viewmodel
 
+import android.content.res.Resources
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.res.R
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.notification.icon.domain.interactor.AlwaysOnDisplayNotificationIconsInteractor
 import javax.inject.Inject
@@ -35,9 +38,12 @@
     iconsInteractor: AlwaysOnDisplayNotificationIconsInteractor,
     keyguardInteractor: KeyguardInteractor,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    @Main resources: Resources,
     shadeInteractor: ShadeInteractor,
 ) {
 
+    private val maxIcons = resources.getInteger(R.integer.max_notif_icons_on_aod)
+
     /** Are changes to the icon container animated? */
     val areContainerChangesAnimated: Flow<Boolean> =
         combine(
@@ -67,7 +73,8 @@
     val icons: Flow<NotificationIconsViewData> =
         iconsInteractor.aodNotifs.map { entries ->
             NotificationIconsViewData(
-                visibleKeys = entries.mapNotNull { it.toIconInfo(it.aodIcon) },
+                visibleIcons = entries.mapNotNull { it.toIconInfo(it.aodIcon) },
+                iconLimit = maxIcons,
             )
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
index 1560106..8484fdc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
@@ -16,6 +16,7 @@
 package com.android.systemui.statusbar.notification.icon.ui.viewmodel
 
 import com.android.systemui.statusbar.notification.icon.domain.interactor.NotificationIconsInteractor
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconsViewData.LimitType
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.map
@@ -29,8 +30,23 @@
     /** [NotificationIconsViewData] indicating which icons to display in the view. */
     val icons: Flow<NotificationIconsViewData> =
         interactor.filteredNotifSet().map { entries ->
+            var firstAmbient = 0
+            val visibleKeys = buildList {
+                for (entry in entries) {
+                    entry.toIconInfo(entry.shelfIcon)?.let { info ->
+                        add(info)
+                        // NOTE: we assume that all ambient notifications will be at the end of the
+                        // list
+                        if (!entry.isAmbient) {
+                            firstAmbient++
+                        }
+                    }
+                }
+            }
             NotificationIconsViewData(
-                visibleKeys = entries.mapNotNull { it.toIconInfo(it.shelfIcon) },
+                visibleKeys,
+                iconLimit = firstAmbient,
+                limitType = LimitType.MaximumIndex,
             )
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
index c03a4a5..af37e49 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
@@ -15,9 +15,12 @@
  */
 package com.android.systemui.statusbar.notification.icon.ui.viewmodel
 
+import android.content.res.Resources
 import android.graphics.Rect
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.plugins.DarkIconDispatcher
+import com.android.systemui.res.R
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationIconInteractor
@@ -44,9 +47,12 @@
     headsUpIconInteractor: HeadsUpNotificationIconInteractor,
     keyguardInteractor: KeyguardInteractor,
     notificationsInteractor: ActiveNotificationsInteractor,
+    @Main resources: Resources,
     shadeInteractor: ShadeInteractor,
 ) {
 
+    private val maxIcons = resources.getInteger(R.integer.max_notif_static_icons)
+
     /** Are changes to the icon container animated? */
     val animationsEnabled: Flow<Boolean> =
         combine(
@@ -77,7 +83,8 @@
     val icons: Flow<NotificationIconsViewData> =
         iconsInteractor.statusBarNotifs.map { entries ->
             NotificationIconsViewData(
-                visibleKeys = entries.mapNotNull { it.toIconInfo(it.statusBarIcon) },
+                visibleIcons = entries.mapNotNull { it.toIconInfo(it.statusBarIcon) },
+                iconLimit = maxIcons,
             )
         }
 
@@ -86,7 +93,7 @@
         headsUpIconInteractor.isolatedNotification
             .combine(icons) { isolatedNotif, iconsViewData ->
                 isolatedNotif?.let {
-                    iconsViewData.visibleKeys.firstOrNull { it.notifKey == isolatedNotif }
+                    iconsViewData.visibleIcons.firstOrNull { it.notifKey == isolatedNotif }
                 }
             }
             .pairwise(initialValue = null)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconsViewData.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconsViewData.kt
index 867be84..e8756d0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconsViewData.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconsViewData.kt
@@ -23,16 +23,33 @@
 /** Encapsulates the collection of notification icons present on the device. */
 data class NotificationIconsViewData(
     /** Icons that are visible in the container. */
-    val visibleKeys: List<NotificationIconInfo> = emptyList(),
-    /** Keys of icons that are "behind" the overflow dot. */
-    val collapsedKeys: Set<String> = emptySet(),
-    /** Whether the overflow dot should be shown regardless if [collapsedKeys] is empty. */
-    val forceShowDot: Boolean = false,
+    val visibleIcons: List<NotificationIconInfo> = emptyList(),
+    /** Limit applied to the [visibleIcons]; can be interpreted different based on [limitType]. */
+    val iconLimit: Int = visibleIcons.size,
+    /** How [iconLimit] is applied to [visibleIcons]. */
+    val limitType: LimitType = LimitType.MaximumAmount,
 ) {
+    // TODO(b/305739416): This can be removed once we are no longer looking up the StatusBarIconView
+    //  instances from outside of the view-binder layer. Ideally, we would just use MaximumAmount,
+    //  and apply it directly to the list of visibleIcons by truncating the list to that amount.
+    //  At the time of this writing, we cannot do that because looking up the SBIV can fail, and so
+    //  we need additional icons to fall-back to.
+    /** Determines how a limit to the icons is to be applied. */
+    enum class LimitType {
+        /** The [Int] limit is a maximum amount of icons to be displayed. */
+        MaximumAmount,
+        /**
+         * The [Int] limit is a maximum index into the
+         * [list of visible icons][NotificationIconsViewData.visibleIcons] to be displayed; any
+         * icons beyond that index should be omitted.
+         */
+        MaximumIndex,
+    }
+
     /** The difference between two [NotificationIconsViewData]s. */
     data class Diff(
         /** Icons added in the newer dataset. */
-        val added: List<NotificationIconInfo> = emptyList(),
+        val added: List<String> = emptyList(),
         /** Icons removed from the older dataset. */
         val removed: List<String> = emptyList(),
         /**
@@ -44,7 +61,7 @@
          * same group. A view binder can use this information for special animations for this
          * specific change.
          */
-        val groupReplacements: Map<String, NotificationIconInfo> = emptyMap(),
+        val groupReplacements: Map<String, String> = emptyMap(),
     )
 
     companion object {
@@ -56,23 +73,20 @@
             new: NotificationIconsViewData,
             prev: NotificationIconsViewData
         ): Diff {
-            val added: List<NotificationIconInfo> =
-                new.visibleKeys.filter {
-                    it.notifKey !in prev.visibleKeys.asSequence().map { it.notifKey }
-                }
+            val prevKeys = prev.visibleIcons.asSequence().map { it.notifKey }.toSet()
+            val newKeys = new.visibleIcons.asSequence().map { it.notifKey }.toSet()
+            val added: List<String> = newKeys.mapNotNull { key -> key.takeIf { it !in prevKeys } }
             val removed: List<NotificationIconInfo> =
-                prev.visibleKeys.filter {
-                    it.notifKey !in new.visibleKeys.asSequence().map { it.notifKey }
-                }
+                prev.visibleIcons.filter { it.notifKey !in newKeys }
             val groupsToShow: Set<IconGroupInfo> =
-                new.visibleKeys.asSequence().map { it.groupInfo }.toSet()
-            val replacements: ArrayMap<String, NotificationIconInfo> =
+                new.visibleIcons.asSequence().map { it.groupInfo }.toSet()
+            val replacements: ArrayMap<String, String> =
                 removed
                     .asSequence()
                     .filter { keyToRemove -> keyToRemove.groupInfo in groupsToShow }
                     .groupBy { it.groupInfo.groupKey }
                     .mapValuesNotNullTo(ArrayMap()) { (_, vs) ->
-                        vs.takeIf { it.size == 1 }?.get(0)
+                        vs.takeIf { it.size == 1 }?.get(0)?.notifKey
                     }
             return Diff(added, removed.map { it.notifKey }, replacements)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index 9e5fd95..00e78a4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -141,8 +141,10 @@
     private int mMaxStaticIcons;
     private boolean mDozing;
     private boolean mOnLockScreen;
-    private boolean mOverrideIconColor;
+    private int mSpeedBumpIndex = -1;
 
+    private int mMaxIcons = Integer.MAX_VALUE;
+    private boolean mOverrideIconColor;
     private boolean mIsStaticLayout = true;
     private final HashMap<View, IconState> mIconStates = new HashMap<>();
     private int mDotPadding;
@@ -153,7 +155,6 @@
     private boolean mChangingViewPositions;
     private int mAddAnimationStartIndex = -1;
     private int mCannedAnimationStartIndex = -1;
-    private int mSpeedBumpIndex = -1;
     private int mIconSize;
     private boolean mDisallowNextAnimation;
     private boolean mAnimationsEnabled = true;
@@ -170,6 +171,7 @@
     private View mIsolatedIconForAnimation;
     private int mThemedTextColorPrimary;
     private Runnable mIsolatedIconAnimationEndRunnable;
+    private boolean mUseIncreasedIconScale;
 
     public NotificationIconContainer(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -436,18 +438,24 @@
         if (numIcons == 0) {
             return 0f;
         }
-        final float contentWidth =
-                mIconSize * MathUtils.min(numIcons, mMaxIconsOnLockscreen + 1);
-        return getActualPaddingStart()
-                + contentWidth
-                + getActualPaddingEnd();
+        final float contentWidth;
+        if (NotificationIconContainerRefactor.isEnabled()) {
+            contentWidth = mIconSize * numIcons;
+        } else {
+            contentWidth = mIconSize * MathUtils.min(numIcons, mMaxIconsOnLockscreen + 1);
+        }
+        return getActualPaddingStart() + contentWidth + getActualPaddingEnd();
     }
 
     @VisibleForTesting
     boolean shouldForceOverflow(int i, int speedBumpIndex, float iconAppearAmount,
             int maxVisibleIcons) {
-        return speedBumpIndex != -1 && i >= speedBumpIndex
-                && iconAppearAmount > 0.0f || i >= maxVisibleIcons;
+        if (NotificationIconContainerRefactor.isEnabled()) {
+            return i >= maxVisibleIcons && iconAppearAmount > 0.0f;
+        } else {
+            return speedBumpIndex != -1 && i >= speedBumpIndex
+                    && iconAppearAmount > 0.0f || i >= maxVisibleIcons;
+        }
     }
 
     @VisibleForTesting
@@ -502,9 +510,8 @@
                 firstOverflowIndex = i;
                 mVisualOverflowStart = translationX;
             }
-            final float drawingScale = mOnLockScreen && view instanceof StatusBarIconView
-                    ? ((StatusBarIconView) view).getIconScaleIncreased()
-                    : 1f;
+
+            final float drawingScale = getDrawingScale(view);
             translationX += iconState.iconAppearAmount * view.getWidth() * drawingScale;
         }
         mIsShowingOverflowDot = false;
@@ -554,9 +561,26 @@
         }
     }
 
+    private float getDrawingScale(View view) {
+        final boolean useIncreasedScale = NotificationIconContainerRefactor.isEnabled()
+                ? mUseIncreasedIconScale
+                : mOnLockScreen;
+        return useIncreasedScale && view instanceof StatusBarIconView
+                ? ((StatusBarIconView) view).getIconScaleIncreased()
+                : 1f;
+    }
+
+    public void setUseIncreasedIconScale(boolean useIncreasedIconScale) {
+        if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return;
+        mUseIncreasedIconScale = useIncreasedIconScale;
+    }
+
     private int getMaxVisibleIcons(int childCount) {
-        return mOnLockScreen ? mMaxIconsOnAod :
-                mIsStaticLayout ? mMaxStaticIcons : childCount;
+        if (NotificationIconContainerRefactor.isEnabled()) {
+            return mMaxIcons;
+        } else {
+            return mOnLockScreen ? mMaxIconsOnAod : mIsStaticLayout ? mMaxStaticIcons : childCount;
+        }
     }
 
     private float getLayoutEnd() {
@@ -673,9 +697,15 @@
     }
 
     public void setSpeedBumpIndex(int speedBumpIndex) {
+        NotificationIconContainerRefactor.assertInLegacyMode();
         mSpeedBumpIndex = speedBumpIndex;
     }
 
+    public void setMaxIconsAmount(int maxIcons) {
+        if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return;
+        mMaxIcons = maxIcons;
+    }
+
     public int getIconSize() {
         return mIconSize;
     }
@@ -740,6 +770,7 @@
      * configured to. Depending on these values, the layout of the AOD icons change.
      */
     public void setOnLockScreen(boolean onLockScreen) {
+        NotificationIconContainerRefactor.assertInLegacyMode();
         mOnLockScreen = onLockScreen;
     }
 
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index fff8f78..412aa9b 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -120,6 +120,7 @@
         "io/Util.cpp",
         "io/ZipArchive.cpp",
         "link/AutoVersioner.cpp",
+        "link/FeatureFlagsFilter.cpp",
         "link/ManifestFixer.cpp",
         "link/NoDefaultResourceRemover.cpp",
         "link/PrivateAttributeMover.cpp",
diff --git a/tools/aapt2/link/FeatureFlagsFilter.cpp b/tools/aapt2/link/FeatureFlagsFilter.cpp
new file mode 100644
index 0000000..fdf3f74
--- /dev/null
+++ b/tools/aapt2/link/FeatureFlagsFilter.cpp
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2023 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 "link/FeatureFlagsFilter.h"
+
+#include <string_view>
+
+#include "androidfw/IDiagnostics.h"
+#include "androidfw/Source.h"
+#include "util/Util.h"
+#include "xml/XmlDom.h"
+#include "xml/XmlUtil.h"
+
+using ::aapt::xml::Element;
+using ::aapt::xml::Node;
+using ::aapt::xml::NodeCast;
+
+namespace aapt {
+
+class FlagsVisitor : public xml::Visitor {
+ public:
+  explicit FlagsVisitor(android::IDiagnostics* diagnostics,
+                        const FeatureFlagValues& feature_flag_values,
+                        const FeatureFlagsFilterOptions& options)
+      : diagnostics_(diagnostics), feature_flag_values_(feature_flag_values), options_(options) {
+  }
+
+  void Visit(xml::Element* node) override {
+    std::erase_if(node->children,
+                  [this](std::unique_ptr<xml::Node>& node) { return ShouldRemove(node); });
+    VisitChildren(node);
+  }
+
+  bool HasError() const {
+    return has_error_;
+  }
+
+ private:
+  bool ShouldRemove(std::unique_ptr<xml::Node>& node) {
+    if (const auto* el = NodeCast<Element>(node.get())) {
+      auto* attr = el->FindAttribute(xml::kSchemaAndroid, "featureFlag");
+      if (attr == nullptr) {
+        return false;
+      }
+
+      bool negated = false;
+      std::string_view flag_name = util::TrimWhitespace(attr->value);
+      if (flag_name.starts_with('!')) {
+        negated = true;
+        flag_name = flag_name.substr(1);
+      }
+
+      if (auto it = feature_flag_values_.find(std::string(flag_name));
+          it != feature_flag_values_.end()) {
+        if (it->second.has_value()) {
+          if (options_.remove_disabled_elements) {
+            // Remove if flag==true && attr=="!flag" (negated) OR flag==false && attr=="flag"
+            return *it->second == negated;
+          }
+        } else if (options_.flags_must_have_value) {
+          diagnostics_->Error(android::DiagMessage(node->line_number)
+                              << "attribute 'android:featureFlag' has flag '" << flag_name
+                              << "' without a true/false value from --feature_flags parameter");
+          has_error_ = true;
+          return false;
+        }
+      } else if (options_.fail_on_unrecognized_flags) {
+        diagnostics_->Error(android::DiagMessage(node->line_number)
+                            << "attribute 'android:featureFlag' has flag '" << flag_name
+                            << "' not found in flags from --feature_flags parameter");
+        has_error_ = true;
+        return false;
+      }
+    }
+
+    return false;
+  }
+
+  android::IDiagnostics* diagnostics_;
+  const FeatureFlagValues& feature_flag_values_;
+  const FeatureFlagsFilterOptions& options_;
+  bool has_error_ = false;
+};
+
+bool FeatureFlagsFilter::Consume(IAaptContext* context, xml::XmlResource* doc) {
+  FlagsVisitor visitor(context->GetDiagnostics(), feature_flag_values_, options_);
+  doc->root->Accept(&visitor);
+  return !visitor.HasError();
+}
+
+}  // namespace aapt
diff --git a/tools/aapt2/link/FeatureFlagsFilter.h b/tools/aapt2/link/FeatureFlagsFilter.h
new file mode 100644
index 0000000..1d342a7
--- /dev/null
+++ b/tools/aapt2/link/FeatureFlagsFilter.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2023 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 <optional>
+#include <string>
+#include <unordered_map>
+#include <utility>
+
+#include "android-base/macros.h"
+#include "cmd/Util.h"
+#include "process/IResourceTableConsumer.h"
+
+namespace aapt {
+
+struct FeatureFlagsFilterOptions {
+  // If true, elements whose featureFlag values are false (i.e., disabled feature) will be removed.
+  bool remove_disabled_elements = true;
+
+  // If true, `Consume()` will return false (error) if a flag was found that is not in
+  // `feature_flag_values`.
+  bool fail_on_unrecognized_flags = true;
+
+  // If true, `Consume()` will return false (error) if a flag was found whose value in
+  // `feature_flag_values` is not defined (std::nullopt).
+  bool flags_must_have_value = true;
+};
+
+// Looks for the `android:featureFlag` attribute in each XML element, validates the flag names and
+// values, and removes elements according to the values in `feature_flag_values`. An element will be
+// removed if the flag's given value is FALSE. A "!" before the flag name in the attribute indicates
+// a boolean NOT operation, i.e., an element will be removed if the flag's given value is TRUE. For
+// example, if the XML is the following:
+//
+//   <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+//     <permission android:name="FOO" android:featureFlag="!flag"
+//                 android:protectionLevel="normal" />
+//     <permission android:name="FOO" android:featureFlag="flag"
+//                 android:protectionLevel="dangerous" />
+//   </manifest>
+//
+// If `feature_flag_values` contains {"flag", true}, then the <permission> element with
+// protectionLevel="normal" will be removed, and the <permission> element with
+// protectionLevel="normal" will be kept.
+//
+// The `Consume()` function will return false if there is an invalid flag found (see
+// FeatureFlagsFilterOptions for customizing the filter's validation behavior). Do not use the XML
+// further if there are errors as there may be elements removed already.
+class FeatureFlagsFilter : public IXmlResourceConsumer {
+ public:
+  explicit FeatureFlagsFilter(FeatureFlagValues feature_flag_values,
+                              FeatureFlagsFilterOptions options)
+      : feature_flag_values_(std::move(feature_flag_values)), options_(options) {
+  }
+
+  bool Consume(IAaptContext* context, xml::XmlResource* doc) override;
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(FeatureFlagsFilter);
+
+  const FeatureFlagValues feature_flag_values_;
+  const FeatureFlagsFilterOptions options_;
+};
+
+}  // namespace aapt
diff --git a/tools/aapt2/link/FeatureFlagsFilter_test.cpp b/tools/aapt2/link/FeatureFlagsFilter_test.cpp
new file mode 100644
index 0000000..53086cc
--- /dev/null
+++ b/tools/aapt2/link/FeatureFlagsFilter_test.cpp
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2023 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 "link/FeatureFlagsFilter.h"
+
+#include <string_view>
+
+#include "test/Test.h"
+
+using ::testing::IsNull;
+using ::testing::NotNull;
+
+namespace aapt {
+
+// Returns null if there was an error from FeatureFlagsFilter.
+std::unique_ptr<xml::XmlResource> VerifyWithOptions(std::string_view str,
+                                                    const FeatureFlagValues& feature_flag_values,
+                                                    const FeatureFlagsFilterOptions& options) {
+  std::unique_ptr<xml::XmlResource> doc = test::BuildXmlDom(str);
+  FeatureFlagsFilter filter(feature_flag_values, options);
+  if (filter.Consume(test::ContextBuilder().Build().get(), doc.get())) {
+    return doc;
+  }
+  return {};
+}
+
+// Returns null if there was an error from FeatureFlagsFilter.
+std::unique_ptr<xml::XmlResource> Verify(std::string_view str,
+                                         const FeatureFlagValues& feature_flag_values) {
+  return VerifyWithOptions(str, feature_flag_values, {});
+}
+
+TEST(FeatureFlagsFilterTest, NoFeatureFlagAttributes) {
+  auto doc = Verify(R"EOF(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+      <permission android:name="FOO" />
+    </manifest>)EOF",
+                    {{"flag", false}});
+  ASSERT_THAT(doc, NotNull());
+  auto root = doc->root.get();
+  ASSERT_THAT(root, NotNull());
+  auto maybe_removed = root->FindChild({}, "permission");
+  ASSERT_THAT(maybe_removed, NotNull());
+}
+TEST(FeatureFlagsFilterTest, RemoveElementWithDisabledFlag) {
+  auto doc = Verify(R"EOF(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+      <permission android:name="FOO" android:featureFlag="flag" />
+    </manifest>)EOF",
+                    {{"flag", false}});
+  ASSERT_THAT(doc, NotNull());
+  auto root = doc->root.get();
+  ASSERT_THAT(root, NotNull());
+  auto maybe_removed = root->FindChild({}, "permission");
+  ASSERT_THAT(maybe_removed, IsNull());
+}
+
+TEST(FeatureFlagsFilterTest, RemoveElementWithNegatedEnabledFlag) {
+  auto doc = Verify(R"EOF(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+      <permission android:name="FOO" android:featureFlag="!flag" />
+    </manifest>)EOF",
+                    {{"flag", true}});
+  ASSERT_THAT(doc, NotNull());
+  auto root = doc->root.get();
+  ASSERT_THAT(root, NotNull());
+  auto maybe_removed = root->FindChild({}, "permission");
+  ASSERT_THAT(maybe_removed, IsNull());
+}
+
+TEST(FeatureFlagsFilterTest, KeepElementWithEnabledFlag) {
+  auto doc = Verify(R"EOF(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+      <permission android:name="FOO" android:featureFlag="flag" />
+    </manifest>)EOF",
+                    {{"flag", true}});
+  ASSERT_THAT(doc, NotNull());
+  auto root = doc->root.get();
+  ASSERT_THAT(root, NotNull());
+  auto maybe_removed = root->FindChild({}, "permission");
+  ASSERT_THAT(maybe_removed, NotNull());
+}
+
+TEST(FeatureFlagsFilterTest, SideBySideEnabledAndDisabled) {
+  auto doc = Verify(R"EOF(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+      <permission android:name="FOO" android:featureFlag="!flag"
+                  android:protectionLevel="normal" />
+      <permission android:name="FOO" android:featureFlag="flag"
+                  android:protectionLevel="dangerous" />
+    </manifest>)EOF",
+                    {{"flag", true}});
+  ASSERT_THAT(doc, NotNull());
+  auto root = doc->root.get();
+  ASSERT_THAT(root, NotNull());
+  auto children = root->GetChildElements();
+  ASSERT_EQ(children.size(), 1);
+  auto attr = children[0]->FindAttribute(xml::kSchemaAndroid, "protectionLevel");
+  ASSERT_THAT(attr, NotNull());
+  ASSERT_EQ(attr->value, "dangerous");
+}
+
+TEST(FeatureFlagsFilterTest, RemoveDeeplyNestedElement) {
+  auto doc = Verify(R"EOF(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+      <application>
+        <provider />
+        <activity>
+          <layout android:featureFlag="!flag" />
+        </activity>
+      </application>
+    </manifest>)EOF",
+                    {{"flag", true}});
+  ASSERT_THAT(doc, NotNull());
+  auto root = doc->root.get();
+  ASSERT_THAT(root, NotNull());
+  auto application = root->FindChild({}, "application");
+  ASSERT_THAT(application, NotNull());
+  auto activity = application->FindChild({}, "activity");
+  ASSERT_THAT(activity, NotNull());
+  auto maybe_removed = activity->FindChild({}, "layout");
+  ASSERT_THAT(maybe_removed, IsNull());
+}
+
+TEST(FeatureFlagsFilterTest, KeepDeeplyNestedElement) {
+  auto doc = Verify(R"EOF(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+      <application>
+        <provider />
+        <activity>
+          <layout android:featureFlag="flag" />
+        </activity>
+      </application>
+    </manifest>)EOF",
+                    {{"flag", true}});
+  ASSERT_THAT(doc, NotNull());
+  auto root = doc->root.get();
+  ASSERT_THAT(root, NotNull());
+  auto application = root->FindChild({}, "application");
+  ASSERT_THAT(application, NotNull());
+  auto activity = application->FindChild({}, "activity");
+  ASSERT_THAT(activity, NotNull());
+  auto maybe_removed = activity->FindChild({}, "layout");
+  ASSERT_THAT(maybe_removed, NotNull());
+}
+
+TEST(FeatureFlagsFilterTest, FailOnEmptyFeatureFlagAttribute) {
+  auto doc = Verify(R"EOF(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+      <permission android:name="FOO" android:featureFlag=" " />
+    </manifest>)EOF",
+                    {{"flag", false}});
+  ASSERT_THAT(doc, IsNull());
+}
+
+TEST(FeatureFlagsFilterTest, FailOnFlagWithNoGivenValue) {
+  auto doc = Verify(R"EOF(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+      <permission android:name="FOO" android:featureFlag="flag" />
+    </manifest>)EOF",
+                    {{"flag", std::nullopt}});
+  ASSERT_THAT(doc, IsNull());
+}
+
+TEST(FeatureFlagsFilterTest, FailOnUnrecognizedFlag) {
+  auto doc = Verify(R"EOF(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+      <permission android:name="FOO" android:featureFlag="unrecognized" />
+    </manifest>)EOF",
+                    {{"flag", true}});
+  ASSERT_THAT(doc, IsNull());
+}
+
+TEST(FeatureFlagsFilterTest, FailOnMultipleValidationErrors) {
+  auto doc = Verify(R"EOF(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+      <permission android:name="FOO" android:featureFlag="bar" />
+      <permission android:name="FOO" android:featureFlag="unrecognized" />
+    </manifest>)EOF",
+                    {{"flag", std::nullopt}});
+  ASSERT_THAT(doc, IsNull());
+}
+
+TEST(FeatureFlagsFilterTest, OptionRemoveDisabledElementsIsFalse) {
+  auto doc = VerifyWithOptions(R"EOF(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+      <permission android:name="FOO" android:featureFlag="flag" />
+    </manifest>)EOF",
+                               {{"flag", false}}, {.remove_disabled_elements = false});
+  ASSERT_THAT(doc, NotNull());
+  auto root = doc->root.get();
+  ASSERT_THAT(root, NotNull());
+  auto maybe_removed = root->FindChild({}, "permission");
+  ASSERT_THAT(maybe_removed, NotNull());
+}
+
+TEST(FeatureFlagsFilterTest, OptionFlagsMustHaveValueIsFalse) {
+  auto doc = VerifyWithOptions(R"EOF(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+      <permission android:name="FOO" android:featureFlag="flag" />
+    </manifest>)EOF",
+                               {{"flag", std::nullopt}}, {.flags_must_have_value = false});
+  ASSERT_THAT(doc, NotNull());
+  auto root = doc->root.get();
+  ASSERT_THAT(root, NotNull());
+  auto maybe_removed = root->FindChild({}, "permission");
+  ASSERT_THAT(maybe_removed, NotNull());
+}
+
+TEST(FeatureFlagsFilterTest, OptionFailOnUnrecognizedFlagsIsFalse) {
+  auto doc = VerifyWithOptions(R"EOF(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
+      <permission android:name="FOO" android:featureFlag="unrecognized" />
+    </manifest>)EOF",
+                               {{"flag", true}}, {.fail_on_unrecognized_flags = false});
+  ASSERT_THAT(doc, NotNull());
+  auto root = doc->root.get();
+  ASSERT_THAT(root, NotNull());
+  auto maybe_removed = root->FindChild({}, "permission");
+  ASSERT_THAT(maybe_removed, NotNull());
+}
+
+}  // namespace aapt