Merge "Fix minimum line height for TextView use case" into main
diff --git a/Ravenwood.bp b/Ravenwood.bp
index d13c4d7..93febca4 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -33,6 +33,7 @@
         "@$(location ravenwood/ravenwood-standard-options.txt) " +
 
         "--debug-log $(location hoststubgen_framework-minus-apex.log) " +
+        "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " +
 
         "--out-impl-jar $(location ravenwood.jar) " +
 
@@ -56,6 +57,7 @@
         "hoststubgen_dump.txt",
 
         "hoststubgen_framework-minus-apex.log",
+        "hoststubgen_framework-minus-apex_stats.csv",
     ],
     visibility: ["//visibility:private"],
 }
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index 7284f47..7de6799 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -2354,9 +2354,9 @@
                 if (maxExecutionDelayMillis - windowStart < MIN_ALLOWED_TIME_WINDOW_MILLIS) {
                     if (enforceMinimumTimeWindows
                             && Flags.enforceMinimumTimeWindows()) {
-                        throw new IllegalArgumentException("Jobs with a deadline and"
-                                + " functional constraints cannot have a time window less than "
-                                + MIN_ALLOWED_TIME_WINDOW_MILLIS + " ms."
+                        throw new IllegalArgumentException("Time window too short. Constraints"
+                                + " unlikely to be satisfied. Increase deadline to a reasonable"
+                                + " duration."
                                 + " Job '" + service.flattenToShortString() + "#" + jobId + "'"
                                 + " has delay=" + windowStart
                                 + ", deadline=" + maxExecutionDelayMillis);
diff --git a/core/api/current.txt b/core/api/current.txt
index 285ac9b..9c3196d 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -3323,11 +3323,13 @@
     method @FlaggedApi("android.view.accessibility.a11y_overlay_callbacks") public void attachAccessibilityOverlayToWindow(int, @NonNull android.view.SurfaceControl, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer);
     method public boolean clearCache();
     method public boolean clearCachedSubtree(@NonNull android.view.accessibility.AccessibilityNodeInfo);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void clearTestBrailleDisplayController();
     method public final void disableSelf();
     method public final boolean dispatchGesture(@NonNull android.accessibilityservice.GestureDescription, @Nullable android.accessibilityservice.AccessibilityService.GestureResultCallback, @Nullable android.os.Handler);
     method public android.view.accessibility.AccessibilityNodeInfo findFocus(int);
     method @NonNull public final android.accessibilityservice.AccessibilityButtonController getAccessibilityButtonController();
     method @NonNull public final android.accessibilityservice.AccessibilityButtonController getAccessibilityButtonController(int);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") @NonNull public android.accessibilityservice.BrailleDisplayController getBrailleDisplayController();
     method @NonNull @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public final android.accessibilityservice.FingerprintGestureController getFingerprintGestureController();
     method @Nullable public final android.accessibilityservice.InputMethod getInputMethod();
     method @NonNull public final android.accessibilityservice.AccessibilityService.MagnificationController getMagnificationController();
@@ -3357,6 +3359,7 @@
     method public boolean setCacheEnabled(boolean);
     method public void setGestureDetectionPassthroughRegion(int, @NonNull android.graphics.Region);
     method public final void setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void setTestBrailleDisplayController(@NonNull android.accessibilityservice.BrailleDisplayController);
     method public void setTouchExplorationPassthroughRegion(int, @NonNull android.graphics.Region);
     method public void takeScreenshot(int, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.AccessibilityService.TakeScreenshotCallback);
     method public void takeScreenshotOfWindow(int, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.AccessibilityService.TakeScreenshotCallback);
@@ -3561,6 +3564,25 @@
     field public String[] packageNames;
   }
 
+  @FlaggedApi("android.view.accessibility.braille_display_hid") public interface BrailleDisplayController {
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public void connect(@NonNull android.bluetooth.BluetoothDevice, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public void connect(@NonNull android.bluetooth.BluetoothDevice, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void connect(@NonNull android.hardware.usb.UsbDevice, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void connect(@NonNull android.hardware.usb.UsbDevice, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void disconnect();
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public boolean isConnected();
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void write(@NonNull byte[]) throws java.io.IOException;
+  }
+
+  @FlaggedApi("android.view.accessibility.braille_display_hid") public static interface BrailleDisplayController.BrailleDisplayCallback {
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onConnected(@NonNull byte[]);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onConnectionFailed(int);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onDisconnected();
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onInput(@NonNull byte[]);
+    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final int FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND = 2; // 0x2
+    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final int FLAG_ERROR_CANNOT_ACCESS = 1; // 0x1
+  }
+
   public final class FingerprintGestureController {
     method public boolean isGestureDetectionAvailable();
     method public void registerFingerprintGestureCallback(@NonNull android.accessibilityservice.FingerprintGestureController.FingerprintGestureCallback, @Nullable android.os.Handler);
@@ -36978,6 +37000,7 @@
     field public static final String ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = "android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
     field public static final String ACTION_REQUEST_MANAGE_MEDIA = "android.settings.REQUEST_MANAGE_MEDIA";
     field @FlaggedApi("com.android.media.flags.enable_privileged_routing_for_media_routing_control") public static final String ACTION_REQUEST_MEDIA_ROUTING_CONTROL = "android.settings.REQUEST_MEDIA_ROUTING_CONTROL";
+    field @FlaggedApi("android.provider.backup_tasks_settings_screen") public static final String ACTION_REQUEST_RUN_BACKUP_JOBS = "android.settings.REQUEST_RUN_BACKUP_JOBS";
     field public static final String ACTION_REQUEST_SCHEDULE_EXACT_ALARM = "android.settings.REQUEST_SCHEDULE_EXACT_ALARM";
     field public static final String ACTION_REQUEST_SET_AUTOFILL_SERVICE = "android.settings.REQUEST_SET_AUTOFILL_SERVICE";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String ACTION_SATELLITE_SETTING = "android.settings.SATELLITE_SETTING";
@@ -52220,6 +52243,7 @@
     method public final boolean getClipToOutline();
     method @Nullable public final android.view.contentcapture.ContentCaptureSession getContentCaptureSession();
     method public CharSequence getContentDescription();
+    method @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public final int getContentSensitivity();
     method @UiContext public final android.content.Context getContext();
     method protected android.view.ContextMenu.ContextMenuInfo getContextMenuInfo();
     method public final boolean getDefaultFocusHighlightEnabled();
@@ -52399,6 +52423,7 @@
     method public boolean isAttachedToWindow();
     method public boolean isAutoHandwritingEnabled();
     method public boolean isClickable();
+    method @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public final boolean isContentSensitive();
     method public boolean isContextClickable();
     method public boolean isCredential();
     method public boolean isDirty();
@@ -52603,6 +52628,7 @@
     method public void setClipToOutline(boolean);
     method public void setContentCaptureSession(@Nullable android.view.contentcapture.ContentCaptureSession);
     method public void setContentDescription(CharSequence);
+    method @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public final void setContentSensitivity(int);
     method public void setContextClickable(boolean);
     method public void setDefaultFocusHighlightEnabled(boolean);
     method @Deprecated public void setDrawingCacheBackgroundColor(@ColorInt int);
@@ -52787,6 +52813,9 @@
     field public static final int AUTOFILL_TYPE_NONE = 0; // 0x0
     field public static final int AUTOFILL_TYPE_TEXT = 1; // 0x1
     field public static final int AUTOFILL_TYPE_TOGGLE = 2; // 0x2
+    field @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public static final int CONTENT_SENSITIVITY_AUTO = 0; // 0x0
+    field @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public static final int CONTENT_SENSITIVITY_NOT_SENSITIVE = 2; // 0x2
+    field @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public static final int CONTENT_SENSITIVITY_SENSITIVE = 1; // 0x1
     field public static final int DRAG_FLAG_ACCESSIBILITY_ACTION = 1024; // 0x400
     field public static final int DRAG_FLAG_GLOBAL = 256; // 0x100
     field public static final int DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION = 64; // 0x40
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 50c9c33..aca003d 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -100,7 +100,6 @@
     field public static final String CAMERA_DISABLE_TRANSMIT_LED = "android.permission.CAMERA_DISABLE_TRANSMIT_LED";
     field @FlaggedApi("com.android.internal.camera.flags.camera_hsum_permission") public static final String CAMERA_HEADLESS_SYSTEM_USER = "android.permission.CAMERA_HEADLESS_SYSTEM_USER";
     field public static final String CAMERA_OPEN_CLOSE_LISTENER = "android.permission.CAMERA_OPEN_CLOSE_LISTENER";
-    field @FlaggedApi("com.android.internal.camera.flags.privacy_allowlist") public static final String CAMERA_PRIVACY_ALLOWLIST = "android.permission.CAMERA_PRIVACY_ALLOWLIST";
     field public static final String CAPTURE_AUDIO_HOTWORD = "android.permission.CAPTURE_AUDIO_HOTWORD";
     field public static final String CAPTURE_CONSENTLESS_BUGREPORT_ON_USERDEBUG_BUILD = "android.permission.CAPTURE_CONSENTLESS_BUGREPORT_ON_USERDEBUG_BUILD";
     field public static final String CAPTURE_MEDIA_OUTPUT = "android.permission.CAPTURE_MEDIA_OUTPUT";
@@ -4360,10 +4359,12 @@
   public final class DomainVerificationManager {
     method @Nullable @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) public android.content.pm.verify.domain.DomainVerificationInfo getDomainVerificationInfo(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
     method @NonNull @RequiresPermission(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION) public java.util.SortedSet<android.content.pm.verify.domain.DomainOwner> getOwnersForDomain(@NonNull String);
+    method @FlaggedApi("android.content.pm.relative_reference_intent_filters") @NonNull public java.util.Map<java.lang.String,java.util.List<android.content.UriRelativeFilterGroup>> getUriRelativeFilterGroups(@NonNull String, @NonNull java.util.List<java.lang.String>);
     method @NonNull @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) public java.util.List<java.lang.String> queryValidVerificationPackageNames();
     method @RequiresPermission(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION) public void setDomainVerificationLinkHandlingAllowed(@NonNull String, boolean) throws android.content.pm.PackageManager.NameNotFoundException;
     method @CheckResult @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) public int setDomainVerificationStatus(@NonNull java.util.UUID, @NonNull java.util.Set<java.lang.String>, int) throws android.content.pm.PackageManager.NameNotFoundException;
     method @CheckResult @RequiresPermission(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION) public int setDomainVerificationUserSelection(@NonNull java.util.UUID, @NonNull java.util.Set<java.lang.String>, boolean) throws android.content.pm.PackageManager.NameNotFoundException;
+    method @FlaggedApi("android.content.pm.relative_reference_intent_filters") @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) public void setUriRelativeFilterGroups(@NonNull String, @NonNull java.util.Map<java.lang.String,java.util.List<android.content.UriRelativeFilterGroup>>);
     field public static final int ERROR_DOMAIN_SET_ID_INVALID = 1; // 0x1
     field public static final int ERROR_UNABLE_TO_APPROVE = 3; // 0x3
     field public static final int ERROR_UNKNOWN_DOMAIN = 2; // 0x2
@@ -4657,15 +4658,11 @@
     method @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public void addSensorPrivacyListener(@NonNull android.hardware.SensorPrivacyManager.OnSensorPrivacyChangedListener);
     method @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public void addSensorPrivacyListener(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.SensorPrivacyManager.OnSensorPrivacyChangedListener);
     method @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public boolean areAnySensorPrivacyTogglesEnabled(int);
-    method @FlaggedApi("com.android.internal.camera.flags.privacy_allowlist") @NonNull @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public java.util.Map<java.lang.String,java.lang.Boolean> getCameraPrivacyAllowlist();
-    method @FlaggedApi("com.android.internal.camera.flags.privacy_allowlist") @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public int getSensorPrivacyState(int, int);
-    method @FlaggedApi("com.android.internal.camera.flags.privacy_allowlist") @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public boolean isCameraPrivacyEnabled(@NonNull String);
     method @Deprecated @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public boolean isSensorPrivacyEnabled(int);
     method @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public boolean isSensorPrivacyEnabled(int, int);
     method @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public void removeSensorPrivacyListener(int, @NonNull android.hardware.SensorPrivacyManager.OnSensorPrivacyChangedListener);
     method @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public void removeSensorPrivacyListener(@NonNull android.hardware.SensorPrivacyManager.OnSensorPrivacyChangedListener);
     method @RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY) public void setSensorPrivacy(int, boolean);
-    method @FlaggedApi("com.android.internal.camera.flags.privacy_allowlist") @RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY) public void setSensorPrivacyState(int, int);
   }
 
   public static interface SensorPrivacyManager.OnSensorPrivacyChangedListener {
@@ -4675,7 +4672,6 @@
 
   public static class SensorPrivacyManager.OnSensorPrivacyChangedListener.SensorPrivacyChangedParams {
     method public int getSensor();
-    method @FlaggedApi("com.android.internal.camera.flags.privacy_allowlist") public int getState();
     method public int getToggleType();
     method public boolean isEnabled();
   }
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 1e30a32..19b265d 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -104,6 +104,14 @@
     method @FlaggedApi("android.view.accessibility.motion_event_observing") public void setObservedMotionEventSources(int);
   }
 
+  @FlaggedApi("android.view.accessibility.braille_display_hid") public interface BrailleDisplayController {
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public static void setTestBrailleDisplayData(@NonNull android.accessibilityservice.AccessibilityService, @NonNull java.util.List<android.os.Bundle>);
+    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH = "BUS_BLUETOOTH";
+    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_DESCRIPTOR = "DESCRIPTOR";
+    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_HIDRAW_PATH = "HIDRAW_PATH";
+    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_UNIQUE_ID = "UNIQUE_ID";
+  }
+
 }
 
 package android.animation {
@@ -1483,7 +1491,6 @@
 
   public final class SensorPrivacyManager {
     method @RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY) public void setSensorPrivacy(int, int, boolean);
-    method @FlaggedApi("com.android.internal.camera.flags.privacy_allowlist") @RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY) public void setSensorPrivacyState(int, int, int);
   }
 
   public static class SensorPrivacyManager.Sources {
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java
index e7e3a85..f7d7522 100644
--- a/core/java/android/accessibilityservice/AccessibilityService.java
+++ b/core/java/android/accessibilityservice/AccessibilityService.java
@@ -80,6 +80,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 import java.util.function.IntConsumer;
@@ -850,6 +851,8 @@
     private boolean mInputMethodInitialized = false;
     private final SparseArray<AccessibilityButtonController> mAccessibilityButtonControllers =
             new SparseArray<>(0);
+    private BrailleDisplayController mBrailleDisplayController;
+    private BrailleDisplayController mTestBrailleDisplayController;
 
     private int mGestureStatusCallbackSequence;
 
@@ -3634,4 +3637,56 @@
                 .attachAccessibilityOverlayToWindow(
                         mConnectionId, accessibilityWindowId, sc, executor, callback);
     }
+
+    /**
+     * Returns the {@link BrailleDisplayController} which may be used to communicate with
+     * refreshable Braille displays that provide USB or Bluetooth Braille display HID support.
+     */
+    @FlaggedApi(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @NonNull
+    public BrailleDisplayController getBrailleDisplayController() {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        synchronized (mLock) {
+            if (mTestBrailleDisplayController != null) {
+                return mTestBrailleDisplayController;
+            }
+
+            if (mBrailleDisplayController == null) {
+                mBrailleDisplayController = new BrailleDisplayControllerImpl(this, mLock);
+            }
+            return mBrailleDisplayController;
+        }
+    }
+
+    /**
+     * Set the {@link BrailleDisplayController} implementation that will be returned by
+     * {@link #getBrailleDisplayController}, to allow this accessibility service to test its
+     * interaction with BrailleDisplayController without requiring a real Braille display.
+     *
+     * <p>For full test fidelity, ensure that this test-only implementation follows the same
+     * behavior specified in the documentation for {@link BrailleDisplayController}, including
+     * thrown exceptions.
+     *
+     * @param controller A test-only implementation of {@link BrailleDisplayController}.
+     */
+    @FlaggedApi(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void setTestBrailleDisplayController(@NonNull BrailleDisplayController controller) {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        Objects.requireNonNull(controller);
+        synchronized (mLock) {
+            mTestBrailleDisplayController = controller;
+        }
+    }
+
+    /**
+     * Clears the {@link BrailleDisplayController} previously set by
+     * {@link #setTestBrailleDisplayController}.
+     */
+    @FlaggedApi(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void clearTestBrailleDisplayController() {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        synchronized (mLock) {
+            mTestBrailleDisplayController = null;
+        }
+    }
 }
diff --git a/core/java/android/accessibilityservice/BrailleDisplayController.java b/core/java/android/accessibilityservice/BrailleDisplayController.java
new file mode 100644
index 0000000..5282aa3
--- /dev/null
+++ b/core/java/android/accessibilityservice/BrailleDisplayController.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.accessibilityservice;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.annotation.TestApi;
+import android.bluetooth.BluetoothDevice;
+import android.hardware.usb.UsbDevice;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.view.accessibility.AccessibilityInteractionClient;
+import android.view.accessibility.Flags;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Used to communicate with a Braille display that supports the Braille display HID standard
+ * (usage page 0x41).
+ *
+ * <p>Only one Braille display may be connected at a time.
+ */
+// This interface doesn't actually own resources. Its I/O connections are owned, monitored,
+// and automatically closed by the system after the accessibility service is disconnected.
+@SuppressLint("NotCloseable")
+@FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+public interface BrailleDisplayController {
+
+    /**
+     * Throw {@link IllegalStateException} if this feature's aconfig flag is disabled.
+     *
+     * @hide
+     */
+    static void checkApiFlagIsEnabled() {
+        if (!Flags.brailleDisplayHid()) {
+            throw new IllegalStateException("Flag BRAILLE_DISPLAY_HID not enabled");
+        }
+    }
+
+    /**
+     * Interface provided to {@link BrailleDisplayController} connection methods to
+     * receive callbacks from the system.
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    interface BrailleDisplayCallback {
+        /**
+         * The system cannot access connected HID devices.
+         */
+        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+        int FLAG_ERROR_CANNOT_ACCESS = 1 << 0;
+        /**
+         * A unique Braille display matching the requested properties could not be identified.
+         */
+        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+        int FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND = 1 << 1;
+
+        /** @hide */
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(flag = true, prefix = "FLAG_ERROR_", value = {
+                FLAG_ERROR_CANNOT_ACCESS,
+                FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND,
+        })
+        @interface ErrorCode {
+        }
+
+        /**
+         * Callback to observe a successful Braille display connection.
+         *
+         * <p>The provided HID report descriptor should be used to understand the input bytes
+         * received from the Braille display via {@link #onInput} and to prepare
+         * the output sent to the Braille display via {@link #write}.
+         *
+         * @param hidDescriptor The HID report descriptor for this Braille display.
+         * @see #connect(BluetoothDevice, BrailleDisplayCallback)
+         * @see #connect(UsbDevice, BrailleDisplayCallback)
+         */
+        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+        void onConnected(@NonNull byte[] hidDescriptor);
+
+        /**
+         * Callback to observe a failed Braille display connection.
+         *
+         * @param errorFlags A bitmask of error codes for the connection failure.
+         * @see #connect(BluetoothDevice, BrailleDisplayCallback)
+         * @see #connect(UsbDevice, BrailleDisplayCallback)
+         */
+        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+        void onConnectionFailed(@ErrorCode int errorFlags);
+
+        /**
+         * Callback to observe input bytes from the currently connected Braille display.
+         *
+         * @param input The input bytes from the Braille display, formatted according to the HID
+         *              report descriptor and the HIDRAW kernel driver.
+         */
+        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+        void onInput(@NonNull byte[] input);
+
+        /**
+         * Callback to observe when the currently connected Braille display is disconnected by the
+         * system.
+         */
+        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+        void onDisconnected();
+    }
+
+    /**
+     * Connects to the requested bluetooth Braille display using the Braille
+     * display HID standard (usage page 0x41).
+     *
+     * <p>If successful then the HID report descriptor will be provided to
+     * {@link BrailleDisplayCallback#onConnected}
+     * and the Braille display will start sending incoming input bytes to
+     * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input
+     * then the system will disconnect the Braille display.
+     *
+     * <p>Note that the callbacks will be executed on the main thread using
+     * {@link AccessibilityService#getMainExecutor()}. To specify the execution thread, use
+     * {@link #connect(BluetoothDevice, Executor, BrailleDisplayCallback)}.
+     *
+     * @param bluetoothDevice The Braille display device.
+     * @param callback        Callbacks used to provide connection results.
+     * @see BrailleDisplayCallback#onConnected
+     * @see BrailleDisplayCallback#onConnectionFailed
+     * @throws IllegalStateException if a Braille display is already connected to this controller.
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+    void connect(@NonNull BluetoothDevice bluetoothDevice,
+            @NonNull BrailleDisplayCallback callback);
+
+    /**
+     * Connects to the requested bluetooth Braille display using the Braille
+     * display HID standard (usage page 0x41).
+     *
+     * <p>If successful then the HID report descriptor will be provided to
+     * {@link BrailleDisplayCallback#onConnected}
+     * and the Braille display will start sending incoming input bytes to
+     * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input
+     * then the system will disconnect the Braille display.
+     *
+     * @param bluetoothDevice  The Braille display device.
+     * @param callbackExecutor Executor for executing the provided callbacks.
+     * @param callback         Callbacks used to provide connection results.
+     * @see BrailleDisplayCallback#onConnected
+     * @see BrailleDisplayCallback#onConnectionFailed
+     * @throws IllegalStateException if a Braille display is already connected to this controller.
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+    void connect(@NonNull BluetoothDevice bluetoothDevice,
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull BrailleDisplayCallback callback);
+
+    /**
+     * Connects to the requested USB Braille display using the Braille
+     * display HID standard (usage page 0x41).
+     *
+     * <p>If successful then the HID report descriptor will be provided to
+     * {@link BrailleDisplayCallback#onConnected}
+     * and the Braille display will start sending incoming input bytes to
+     * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input
+     * then the system will disconnect the Braille display.
+     *
+     * <p>The accessibility service app must already have approval to access the USB device
+     * from the standard {@link android.hardware.usb.UsbManager} access approval process.
+     *
+     * <p>Note that the callbacks will be executed on the main thread using
+     * {@link AccessibilityService#getMainExecutor()}. To specify the execution thread, use
+     * {@link #connect(UsbDevice, Executor, BrailleDisplayCallback)}.
+     *
+     * @param usbDevice        The Braille display device.
+     * @param callback         Callbacks used to provide connection results.
+     * @see BrailleDisplayCallback#onConnected
+     * @see BrailleDisplayCallback#onConnectionFailed
+     * @throws SecurityException if the caller does not have USB device approval.
+     * @throws IllegalStateException if a Braille display is already connected to this controller.
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    void connect(@NonNull UsbDevice usbDevice,
+            @NonNull BrailleDisplayCallback callback);
+
+    /**
+     * Connects to the requested USB Braille display using the Braille
+     * display HID standard (usage page 0x41).
+     *
+     * <p>If successful then the HID report descriptor will be provided to
+     * {@link BrailleDisplayCallback#onConnected}
+     * and the Braille display will start sending incoming input bytes to
+     * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input
+     * then the system will disconnect the Braille display.
+     *
+     * <p>The accessibility service app must already have approval to access the USB device
+     * from the standard {@link android.hardware.usb.UsbManager} access approval process.
+     *
+     * @param usbDevice        The Braille display device.
+     * @param callbackExecutor Executor for executing the provided callbacks.
+     * @param callback         Callbacks used to provide connection results.
+     * @see BrailleDisplayCallback#onConnected
+     * @see BrailleDisplayCallback#onConnectionFailed
+     * @throws SecurityException if the caller does not have USB device approval.
+     * @throws IllegalStateException if a Braille display is already connected to this controller.
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    void connect(@NonNull UsbDevice usbDevice,
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull BrailleDisplayCallback callback);
+
+    /**
+     * Returns true if a Braille display is currently connected, otherwise false.
+     *
+     * @see #connect
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    boolean isConnected();
+
+    /**
+     * Writes a HID report to the currently connected Braille display.
+     *
+     * <p>This method returns immediately after dispatching the write request to the system.
+     * If the system experiences an error in writing output (e.g. the Braille display is unplugged
+     * after the system receives the write request but before writing the bytes to the Braille
+     * display) then the system will disconnect the Braille display, which calls
+     * {@link BrailleDisplayCallback#onDisconnected()}.
+     *
+     * @param buffer The bytes to write to the Braille display. These bytes should be formatted
+     *               according to the HID report descriptor and the HIDRAW kernel driver.
+     * @throws IOException              if there is no currently connected Braille display.
+     * @throws IllegalArgumentException if the buffer exceeds the maximum safe payload size for
+     *                                  binder transactions of
+     *                                  {@link IBinder#getSuggestedMaxIpcSizeBytes()}
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    void write(@NonNull byte[] buffer) throws IOException;
+
+    /**
+     * Disconnects from the currently connected Braille display.
+     *
+     * @see #isConnected()
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    void disconnect();
+
+    /**
+     * Provides test Braille display data to be used for automated CTS tests.
+     *
+     * <p>See {@code TEST_BRAILLE_DISPLAY_*} bundle keys.
+     *
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)
+    @TestApi
+    static void setTestBrailleDisplayData(
+            @NonNull AccessibilityService service,
+            @NonNull List<Bundle> brailleDisplays) {
+        checkApiFlagIsEnabled();
+        final IAccessibilityServiceConnection serviceConnection =
+                AccessibilityInteractionClient.getConnection(service.getConnectionId());
+        if (serviceConnection != null) {
+            try {
+                serviceConnection.setTestBrailleDisplayData(brailleDisplays);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /** @hide */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @TestApi
+    String TEST_BRAILLE_DISPLAY_HIDRAW_PATH = "HIDRAW_PATH";
+    /** @hide */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @TestApi
+    String TEST_BRAILLE_DISPLAY_DESCRIPTOR = "DESCRIPTOR";
+    /** @hide */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @TestApi
+    String TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH = "BUS_BLUETOOTH";
+    /** @hide */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @TestApi
+    String TEST_BRAILLE_DISPLAY_UNIQUE_ID = "UNIQUE_ID";
+}
diff --git a/core/java/android/accessibilityservice/BrailleDisplayControllerImpl.java b/core/java/android/accessibilityservice/BrailleDisplayControllerImpl.java
new file mode 100644
index 0000000..cac1dc4
--- /dev/null
+++ b/core/java/android/accessibilityservice/BrailleDisplayControllerImpl.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.accessibilityservice;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.bluetooth.BluetoothDevice;
+import android.hardware.usb.UsbDevice;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.view.accessibility.AccessibilityInteractionClient;
+import android.view.accessibility.Flags;
+
+import com.android.internal.util.FunctionalUtils;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * Default implementation of {@link BrailleDisplayController}.
+ */
+// BrailleDisplayControllerImpl is not an API, but it implements BrailleDisplayController APIs.
+// This @FlaggedApi annotation tells the linter that this method delegates API checks to its
+// callers.
+@FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+final class BrailleDisplayControllerImpl implements BrailleDisplayController {
+
+    private final AccessibilityService mAccessibilityService;
+    private final Object mLock;
+
+    private IBrailleDisplayConnection mBrailleDisplayConnection;
+    private Executor mCallbackExecutor;
+    private BrailleDisplayCallback mCallback;
+
+    BrailleDisplayControllerImpl(AccessibilityService accessibilityService,
+            Object lock) {
+        mAccessibilityService = accessibilityService;
+        mLock = lock;
+    }
+
+    @Override
+    @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+    public void connect(@NonNull BluetoothDevice bluetoothDevice,
+            @NonNull BrailleDisplayCallback callback) {
+        connect(bluetoothDevice, mAccessibilityService.getMainExecutor(), callback);
+    }
+
+    @Override
+    @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+    public void connect(@NonNull BluetoothDevice bluetoothDevice,
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull BrailleDisplayCallback callback) {
+        Objects.requireNonNull(bluetoothDevice);
+        Objects.requireNonNull(callbackExecutor);
+        Objects.requireNonNull(callback);
+        connect(serviceConnection -> serviceConnection.connectBluetoothBrailleDisplay(
+                        bluetoothDevice.getAddress(), new IBrailleDisplayControllerWrapper()),
+                callbackExecutor, callback);
+    }
+
+    @Override
+    public void connect(@NonNull UsbDevice usbDevice,
+            @NonNull BrailleDisplayCallback callback) {
+        connect(usbDevice, mAccessibilityService.getMainExecutor(), callback);
+    }
+
+    @Override
+    public void connect(@NonNull UsbDevice usbDevice,
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull BrailleDisplayCallback callback) {
+        Objects.requireNonNull(usbDevice);
+        Objects.requireNonNull(callbackExecutor);
+        Objects.requireNonNull(callback);
+        connect(serviceConnection -> serviceConnection.connectUsbBrailleDisplay(
+                        usbDevice, new IBrailleDisplayControllerWrapper()),
+                callbackExecutor, callback);
+    }
+
+    /**
+     * Shared implementation for the {@code connect()} API methods.
+     *
+     * <p>Performs a blocking call to system_server to create the connection. Success is
+     * returned through {@link BrailleDisplayCallback#onConnected} while normal connection
+     * errors are returned through {@link BrailleDisplayCallback#onConnectionFailed}. This
+     * connection is implemented using cached data from the HIDRAW driver so it returns
+     * quickly without needing to perform any I/O with the Braille display.
+     *
+     * <p>The AIDL call to system_server is blocking (not posted to a handler thread) so
+     * that runtime exceptions signaling abnormal connection errors from API misuse
+     * (e.g. lacking permissions, providing an invalid BluetoothDevice, calling connect
+     * while already connected) are propagated to the API caller.
+     */
+    private void connect(
+            FunctionalUtils.RemoteExceptionIgnoringConsumer<IAccessibilityServiceConnection>
+                    createConnection,
+            @NonNull Executor callbackExecutor, @NonNull BrailleDisplayCallback callback) {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        if (isConnected()) {
+            throw new IllegalStateException(
+                    "This service already has a connected Braille display");
+        }
+        final IAccessibilityServiceConnection serviceConnection =
+                AccessibilityInteractionClient.getConnection(
+                        mAccessibilityService.getConnectionId());
+        if (serviceConnection == null) {
+            throw new IllegalStateException("Accessibility service is not connected");
+        }
+        synchronized (mLock) {
+            mCallbackExecutor = callbackExecutor;
+            mCallback = callback;
+        }
+        try {
+            createConnection.acceptOrThrow(serviceConnection);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @Override
+    public boolean isConnected() {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        return mBrailleDisplayConnection != null;
+    }
+
+    @Override
+    public void write(@NonNull byte[] buffer) throws IOException {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        Objects.requireNonNull(buffer);
+        if (buffer.length > IBinder.getSuggestedMaxIpcSizeBytes()) {
+            // This same check must be performed in the system to prevent reflection misuse,
+            // but perform it here too to prevent unnecessary IPCs from non-reflection callers.
+            throw new IllegalArgumentException("Invalid write buffer size " + buffer.length);
+        }
+        synchronized (mLock) {
+            if (mBrailleDisplayConnection == null) {
+                throw new IOException("Braille display is not connected");
+            }
+            try {
+                mBrailleDisplayConnection.write(buffer);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    @Override
+    public void disconnect() {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        synchronized (mLock) {
+            try {
+                if (mBrailleDisplayConnection != null) {
+                    mBrailleDisplayConnection.disconnect();
+                }
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            } finally {
+                clearConnectionLocked();
+            }
+        }
+    }
+
+    /**
+     * Implementation of the {@code IBrailleDisplayController} AIDL interface provided to
+     * system_server, which system_server uses to pass messages back to this
+     * {@code BrailleDisplayController}.
+     *
+     * <p>Messages from system_server are routed to the {@link BrailleDisplayCallback} callbacks
+     * implemented by the accessibility service.
+     *
+     * <p>Note: Per API Guidelines 7.5 the Binder identity must be cleared before invoking the
+     * callback executor so that Binder identity checks in the callbacks are performed using the
+     * app's identity.
+     */
+    private final class IBrailleDisplayControllerWrapper extends IBrailleDisplayController.Stub {
+        /**
+         * Called when the system successfully connects to a Braille display.
+         */
+        @Override
+        public void onConnected(IBrailleDisplayConnection connection, byte[] hidDescriptor) {
+            BrailleDisplayController.checkApiFlagIsEnabled();
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    mBrailleDisplayConnection = connection;
+                    mCallbackExecutor.execute(() -> mCallback.onConnected(hidDescriptor));
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        /**
+         * Called when the system is unable to connect to a Braille display.
+         */
+        @Override
+        public void onConnectionFailed(@BrailleDisplayCallback.ErrorCode int errorCode) {
+            BrailleDisplayController.checkApiFlagIsEnabled();
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    mCallbackExecutor.execute(() -> mCallback.onConnectionFailed(errorCode));
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        /**
+         * Called when input is received from the currently connected Braille display.
+         */
+        @Override
+        public void onInput(byte[] input) {
+            BrailleDisplayController.checkApiFlagIsEnabled();
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    // Ignore input that arrives after disconnection.
+                    if (mBrailleDisplayConnection != null) {
+                        mCallbackExecutor.execute(() -> mCallback.onInput(input));
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        /**
+         * Called when the currently connected Braille display is disconnected.
+         */
+        @Override
+        public void onDisconnected() {
+            BrailleDisplayController.checkApiFlagIsEnabled();
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    mCallbackExecutor.execute(mCallback::onDisconnected);
+                    clearConnectionLocked();
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+    }
+
+    private void clearConnectionLocked() {
+        mBrailleDisplayConnection = null;
+    }
+
+}
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
index 96716db..dc5c7f6 100644
--- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
@@ -17,10 +17,12 @@
 package android.accessibilityservice;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
+import android.accessibilityservice.IBrailleDisplayController;
 import android.accessibilityservice.MagnificationConfig;
 import android.content.pm.ParceledListSlice;
 import android.graphics.Bitmap;
 import android.graphics.Region;
+import android.hardware.usb.UsbDevice;
 import android.os.Bundle;
 import android.os.RemoteCallback;
 import android.view.MagnificationSpec;
@@ -160,4 +162,12 @@
     void attachAccessibilityOverlayToDisplay(int interactionId, int displayId, in SurfaceControl sc, IAccessibilityInteractionConnectionCallback callback);
 
     void attachAccessibilityOverlayToWindow(int interactionId, int accessibilityWindowId, in SurfaceControl sc, IAccessibilityInteractionConnectionCallback callback);
+
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+    void connectBluetoothBrailleDisplay(in String bluetoothAddress, in IBrailleDisplayController controller);
+
+    void connectUsbBrailleDisplay(in UsbDevice usbDevice, in IBrailleDisplayController controller);
+
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)")
+    void setTestBrailleDisplayData(in List<Bundle> brailleDisplays);
 }
\ No newline at end of file
diff --git a/core/java/android/accessibilityservice/IBrailleDisplayConnection.aidl b/core/java/android/accessibilityservice/IBrailleDisplayConnection.aidl
new file mode 100644
index 0000000..ec4d7b1
--- /dev/null
+++ b/core/java/android/accessibilityservice/IBrailleDisplayConnection.aidl
@@ -0,0 +1,12 @@
+package android.accessibilityservice;
+
+/**
+ * Interface given to a BrailleDisplayController to talk to a BrailleDisplayConnection
+ * in system_server.
+ *
+ * @hide
+ */
+interface IBrailleDisplayConnection {
+    oneway void disconnect();
+    oneway void write(in byte[] output);
+}
\ No newline at end of file
diff --git a/core/java/android/accessibilityservice/IBrailleDisplayController.aidl b/core/java/android/accessibilityservice/IBrailleDisplayController.aidl
new file mode 100644
index 0000000..7a5d83e
--- /dev/null
+++ b/core/java/android/accessibilityservice/IBrailleDisplayController.aidl
@@ -0,0 +1,17 @@
+package android.accessibilityservice;
+
+import android.accessibilityservice.IBrailleDisplayConnection;
+
+/**
+ * Interface given to a BrailleDisplayConnection to talk to a BrailleDisplayController
+ * in an accessibility service.
+ *
+ * IPCs from system_server to apps must be oneway, so designate this entire interface as oneway.
+ * @hide
+ */
+oneway interface IBrailleDisplayController {
+    void onConnected(in IBrailleDisplayConnection connection, in byte[] hidDescriptor);
+    void onConnectionFailed(int error);
+    void onInput(in byte[] input);
+    void onDisconnected();
+}
\ No newline at end of file
diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java
index 79af65a..d913581 100644
--- a/core/java/android/content/IntentFilter.java
+++ b/core/java/android/content/IntentFilter.java
@@ -559,6 +559,10 @@
             sb.append(" sch=");
             sb.append(mDataSchemes.toString());
         }
+        if (Flags.relativeReferenceIntentFilters() && countUriRelativeFilterGroups() > 0) {
+            sb.append(" grp=");
+            sb.append(mUriRelativeFilterGroups.toString());
+        }
         sb.append(" }");
         return sb.toString();
     }
@@ -1807,13 +1811,7 @@
         if (mUriRelativeFilterGroups == null) {
             return false;
         }
-        for (int i = 0; i < mUriRelativeFilterGroups.size(); i++) {
-            UriRelativeFilterGroup group = mUriRelativeFilterGroups.get(i);
-            if (group.matchData(data)) {
-                return group.getAction() == UriRelativeFilterGroup.ACTION_ALLOW;
-            }
-        }
-        return false;
+        return UriRelativeFilterGroup.matchGroupsToUri(mUriRelativeFilterGroups, data);
     }
 
     /**
diff --git a/core/java/android/content/UriRelativeFilter.java b/core/java/android/content/UriRelativeFilter.java
index 9866cd0..6d33246 100644
--- a/core/java/android/content/UriRelativeFilter.java
+++ b/core/java/android/content/UriRelativeFilter.java
@@ -217,6 +217,15 @@
                 + " }";
     }
 
+    /** @hide */
+    public UriRelativeFilterParcel toParcel() {
+        UriRelativeFilterParcel parcel = new UriRelativeFilterParcel();
+        parcel.uriPart = mUriPart;
+        parcel.patternType = mPatternType;
+        parcel.filter = mFilter;
+        return parcel;
+    }
+
     @Override
     public boolean equals(@Nullable Object o) {
         if (this == o) return true;
@@ -257,4 +266,11 @@
         mPatternType = Integer.parseInt(parser.getAttributeValue(null, PATTERN_STR));
         mFilter = parser.getAttributeValue(null, FILTER_STR);
     }
+
+    /** @hide */
+    public UriRelativeFilter(UriRelativeFilterParcel parcel) {
+        mUriPart = parcel.uriPart;
+        mPatternType = parcel.patternType;
+        mFilter = parcel.filter;
+    }
 }
diff --git a/core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl b/core/java/android/content/UriRelativeFilterGroup.aidl
similarity index 81%
rename from core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl
rename to core/java/android/content/UriRelativeFilterGroup.aidl
index 838e41e..b251054 100644
--- a/core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl
+++ b/core/java/android/content/UriRelativeFilterGroup.aidl
@@ -14,11 +14,6 @@
  * limitations under the License.
  */
 
-package android.hardware;
+package android.content;
 
-/** @hide */
-parcelable CameraPrivacyAllowlistEntry {
-    String packageName;
-    boolean isMandatory;
-}
-
+parcelable UriRelativeFilterGroup;
diff --git a/core/java/android/content/UriRelativeFilterGroup.java b/core/java/android/content/UriRelativeFilterGroup.java
index 72c396a..0e49b4f 100644
--- a/core/java/android/content/UriRelativeFilterGroup.java
+++ b/core/java/android/content/UriRelativeFilterGroup.java
@@ -19,6 +19,7 @@
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.pm.Flags;
 import android.net.Uri;
 import android.os.Parcel;
@@ -36,9 +37,11 @@
 import java.io.IOException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Objects;
 
 /**
@@ -83,6 +86,40 @@
     private final @Action int mAction;
     private final ArraySet<UriRelativeFilter> mUriRelativeFilters = new ArraySet<>();
 
+    /** @hide */
+    public static boolean matchGroupsToUri(List<UriRelativeFilterGroup> groups, Uri uri) {
+        for (int i = 0; i < groups.size(); i++) {
+            if (groups.get(i).matchData(uri)) {
+                return groups.get(i).getAction() == UriRelativeFilterGroup.ACTION_ALLOW;
+            }
+        }
+        return false;
+    }
+
+    /** @hide */
+    public static List<UriRelativeFilterGroup> parcelsToGroups(
+            @Nullable List<UriRelativeFilterGroupParcel> parcels) {
+        List<UriRelativeFilterGroup> groups = new ArrayList<>();
+        if (parcels != null) {
+            for (int i = 0; i < parcels.size(); i++) {
+                groups.add(new UriRelativeFilterGroup(parcels.get(i)));
+            }
+        }
+        return groups;
+    }
+
+    /** @hide */
+    public static List<UriRelativeFilterGroupParcel> groupsToParcels(
+            @Nullable List<UriRelativeFilterGroup> groups) {
+        List<UriRelativeFilterGroupParcel> parcels = new ArrayList<>();
+        if (groups != null) {
+            for (int i = 0; i < groups.size(); i++) {
+                parcels.add(groups.get(i).toParcel());
+            }
+        }
+        return parcels;
+    }
+
     /**
      * New UriRelativeFilterGroup that matches a Intent data.
      *
@@ -205,6 +242,35 @@
         }
     }
 
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        @SuppressWarnings("unchecked")
+        UriRelativeFilterGroup that = (UriRelativeFilterGroup) o;
+        if (mAction != that.mAction) return false;
+        return mUriRelativeFilters.equals(that.mUriRelativeFilters);
+    }
+
+    @Override
+    public int hashCode() {
+        int _hash = 0;
+        _hash = 31 * _hash + mAction;
+        _hash = 31 * _hash + java.util.Objects.hashCode(mUriRelativeFilters);
+        return _hash;
+    }
+
+    /** @hide */
+    public UriRelativeFilterGroupParcel toParcel() {
+        UriRelativeFilterGroupParcel parcel = new UriRelativeFilterGroupParcel();
+        parcel.action = mAction;
+        parcel.filters = new ArrayList<>();
+        for (UriRelativeFilter filter : mUriRelativeFilters) {
+            parcel.filters.add(filter.toParcel());
+        }
+        return parcel;
+    }
+
     /** @hide */
     UriRelativeFilterGroup(@NonNull Parcel src) {
         mAction = src.readInt();
@@ -213,4 +279,12 @@
             mUriRelativeFilters.add(new UriRelativeFilter(src));
         }
     }
+
+    /** @hide */
+    public UriRelativeFilterGroup(UriRelativeFilterGroupParcel parcel) {
+        mAction = parcel.action;
+        for (int i = 0; i < parcel.filters.size(); i++) {
+            mUriRelativeFilters.add(new UriRelativeFilter(parcel.filters.get(i)));
+        }
+    }
 }
diff --git a/core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl b/core/java/android/content/UriRelativeFilterGroupParcel.aidl
similarity index 71%
copy from core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl
copy to core/java/android/content/UriRelativeFilterGroupParcel.aidl
index 838e41e..3679e7f 100644
--- a/core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl
+++ b/core/java/android/content/UriRelativeFilterGroupParcel.aidl
@@ -14,11 +14,15 @@
  * limitations under the License.
  */
 
-package android.hardware;
+package android.content;
 
-/** @hide */
-parcelable CameraPrivacyAllowlistEntry {
-    String packageName;
-    boolean isMandatory;
+import android.content.UriRelativeFilterParcel;
+
+/**
+ * Class for holding UriRelativeFilterGroup data.
+ * @hide
+ */
+parcelable UriRelativeFilterGroupParcel {
+    int action;
+    List<UriRelativeFilterParcel> filters;
 }
-
diff --git a/core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl b/core/java/android/content/UriRelativeFilterParcel.aidl
similarity index 64%
copy from core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl
copy to core/java/android/content/UriRelativeFilterParcel.aidl
index 838e41e..4fb196d 100644
--- a/core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl
+++ b/core/java/android/content/UriRelativeFilterParcel.aidl
@@ -1,11 +1,11 @@
-/**
- * Copyright (c) 2024, The Android Open Source Project
+/*
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      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,
@@ -14,11 +14,14 @@
  * limitations under the License.
  */
 
-package android.hardware;
+package android.content;
 
-/** @hide */
-parcelable CameraPrivacyAllowlistEntry {
-    String packageName;
-    boolean isMandatory;
+/**
+ * Class for holding UriRelativeFilter data.
+ * @hide
+ */
+parcelable UriRelativeFilterParcel {
+    int uriPart;
+    int patternType;
+    String filter;
 }
-
diff --git a/core/java/android/content/pm/verify/domain/DomainVerificationManager.java b/core/java/android/content/pm/verify/domain/DomainVerificationManager.java
index 77bd147..4dcc517 100644
--- a/core/java/android/content/pm/verify/domain/DomainVerificationManager.java
+++ b/core/java/android/content/pm/verify/domain/DomainVerificationManager.java
@@ -17,6 +17,7 @@
 package android.content.pm.verify.domain;
 
 import android.annotation.CheckResult;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -25,15 +26,21 @@
 import android.annotation.SystemService;
 import android.content.Context;
 import android.content.Intent;
+import android.content.UriRelativeFilterGroup;
+import android.content.UriRelativeFilterGroupParcel;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 import android.os.UserHandle;
+import android.util.ArrayMap;
 
 import com.android.internal.util.CollectionUtils;
 
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.SortedSet;
@@ -156,6 +163,74 @@
     }
 
     /**
+     * Update the URI relative filter groups for a package. All previously existing groups
+     * will be cleared before the new groups will be applied.
+     *
+     * @param packageName The name of the package.
+     * @param domainToGroupsMap A map of domains to a list of {@link UriRelativeFilterGroup}s that
+     *                         should apply to them. Groups for each domain will replace any groups
+     *                         provided for that domain in a prior call to this method. Groups will
+     *                         be evaluated in the order they are provided.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT)
+    @FlaggedApi(android.content.pm.Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS)
+    public void setUriRelativeFilterGroups(@NonNull String packageName,
+            @NonNull Map<String, List<UriRelativeFilterGroup>> domainToGroupsMap) {
+        Objects.requireNonNull(packageName);
+        Objects.requireNonNull(domainToGroupsMap);
+        Bundle bundle = new Bundle();
+        for (String domain : domainToGroupsMap.keySet()) {
+            List<UriRelativeFilterGroup> groups = domainToGroupsMap.get(domain);
+            bundle.putParcelableList(domain, UriRelativeFilterGroup.groupsToParcels(groups));
+        }
+        try {
+            mDomainVerificationManager.setUriRelativeFilterGroups(packageName, bundle);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Retrieves a map of a package's verified domains to a list of {@link UriRelativeFilterGroup}s
+     * that applies to them.
+     *
+     * @param packageName The name of the package.
+     * @param domains List of domains for which to retrieve group matches.
+     * @return A map of domains to the lists of {@link UriRelativeFilterGroup}s that apply to them.
+     * @hide
+     */
+    @NonNull
+    @SystemApi
+    @FlaggedApi(android.content.pm.Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS)
+    public Map<String, List<UriRelativeFilterGroup>> getUriRelativeFilterGroups(
+            @NonNull String packageName,
+            @NonNull List<String> domains) {
+        Objects.requireNonNull(packageName);
+        Objects.requireNonNull(domains);
+        if (domains.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        try {
+            Bundle bundle = mDomainVerificationManager.getUriRelativeFilterGroups(packageName,
+                    domains);
+            ArrayMap<String, List<UriRelativeFilterGroup>> map = new ArrayMap<>();
+            if (!bundle.isEmpty()) {
+                for (String domain : bundle.keySet()) {
+                    List<UriRelativeFilterGroupParcel> parcels =
+                            bundle.getParcelableArrayList(domain,
+                                    UriRelativeFilterGroupParcel.class);
+                    map.put(domain, UriRelativeFilterGroup.parcelsToGroups(parcels));
+                }
+            }
+            return map;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Used to iterate all {@link DomainVerificationInfo} values to do cleanup or retries. This is
      * usually a heavy workload and should be done infrequently.
      *
diff --git a/core/java/android/content/pm/verify/domain/IDomainVerificationManager.aidl b/core/java/android/content/pm/verify/domain/IDomainVerificationManager.aidl
index 53205f3..f5af82d 100644
--- a/core/java/android/content/pm/verify/domain/IDomainVerificationManager.aidl
+++ b/core/java/android/content/pm/verify/domain/IDomainVerificationManager.aidl
@@ -20,6 +20,8 @@
 import android.content.pm.verify.domain.DomainSet;
 import android.content.pm.verify.domain.DomainVerificationInfo;
 import android.content.pm.verify.domain.DomainVerificationUserState;
+import android.content.UriRelativeFilterGroup;
+import android.os.Bundle;
 import java.util.List;
 
 /**
@@ -46,4 +48,8 @@
 
     int setDomainVerificationUserSelection(String domainSetId, in DomainSet domains,
             boolean enabled, int userId);
+
+    void setUriRelativeFilterGroups(String packageName, in Bundle domainToGroupsBundle);
+
+    Bundle getUriRelativeFilterGroups(String packageName, in List<String> domains);
 }
diff --git a/core/java/android/hardware/ISensorPrivacyListener.aidl b/core/java/android/hardware/ISensorPrivacyListener.aidl
index 19ae302..2ac21d2 100644
--- a/core/java/android/hardware/ISensorPrivacyListener.aidl
+++ b/core/java/android/hardware/ISensorPrivacyListener.aidl
@@ -25,6 +25,5 @@
     //   frameworks/native/libs/sensorprivacy/aidl/android/hardware/ISensorPrivacyListener.aidl
     // =============== Beginning of transactions used on native side as well ======================
     void onSensorPrivacyChanged(int toggleType, int sensor, boolean enabled);
-    void onSensorPrivacyStateChanged(int toggleType, int sensor, int state);
     // =============== End of transactions used on native side as well ============================
 }
diff --git a/core/java/android/hardware/ISensorPrivacyManager.aidl b/core/java/android/hardware/ISensorPrivacyManager.aidl
index 851ce2a..9cf329c 100644
--- a/core/java/android/hardware/ISensorPrivacyManager.aidl
+++ b/core/java/android/hardware/ISensorPrivacyManager.aidl
@@ -16,7 +16,6 @@
 
 package android.hardware;
 
-import android.hardware.CameraPrivacyAllowlistEntry;
 import android.hardware.ISensorPrivacyListener;
 
 /** @hide */
@@ -46,22 +45,6 @@
     void setToggleSensorPrivacy(int userId, int source, int sensor, boolean enable);
 
     void setToggleSensorPrivacyForProfileGroup(int userId, int source, int sensor, boolean enable);
-
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY)")
-    List<CameraPrivacyAllowlistEntry> getCameraPrivacyAllowlist();
-
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY)")
-    int getToggleSensorPrivacyState(int toggleType, int sensor);
-
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY)")
-    void setToggleSensorPrivacyState(int userId, int source, int sensor, int state);
-
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY)")
-    void setToggleSensorPrivacyStateForProfileGroup(int userId, int source, int sensor, int  state);
-
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY)")
-    boolean isCameraPrivacyEnabled(String packageName);
-
     // =============== End of transactions used on native side as well ============================
 
     void suppressToggleSensorPrivacyReminders(int userId, int sensor, IBinder token,
@@ -70,4 +53,4 @@
     boolean requiresAuthentication();
 
     void showSensorUseDialog(int sensor);
-}
+}
\ No newline at end of file
diff --git a/core/java/android/hardware/SensorPrivacyManager.java b/core/java/android/hardware/SensorPrivacyManager.java
index 4c0a4b9..18c95bfb 100644
--- a/core/java/android/hardware/SensorPrivacyManager.java
+++ b/core/java/android/hardware/SensorPrivacyManager.java
@@ -17,7 +17,6 @@
 package android.hardware;
 
 import android.Manifest;
-import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
@@ -39,11 +38,9 @@
 import android.util.Pair;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.camera.flags.Flags;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 
@@ -218,41 +215,13 @@
         public static final int DISABLED = SensorPrivacyIndividualEnabledSensorProto.DISABLED;
 
         /**
-         * Constant indicating privacy is enabled except for the automotive driver assistance apps
-         * which are helpful for driving.
-         */
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        public static final int AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS =
-                SensorPrivacyIndividualEnabledSensorProto.AUTO_DRIVER_ASSISTANCE_HELPFUL_APPS;
-
-         /**
-         * Constant indicating privacy is enabled except for the automotive driver assistance apps
-         * which are required by car manufacturer for driving.
-         */
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        public static final int AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS =
-                SensorPrivacyIndividualEnabledSensorProto.AUTO_DRIVER_ASSISTANCE_REQUIRED_APPS;
-
-        /**
-         * Constant indicating privacy is enabled except for the automotive driver assistance apps
-         * which are both helpful for driving and also apps required by car manufacturer for
-         * driving.
-         */
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        public static final int AUTOMOTIVE_DRIVER_ASSISTANCE_APPS =
-                SensorPrivacyIndividualEnabledSensorProto.AUTO_DRIVER_ASSISTANCE_APPS;
-
-        /**
          * Types of state which can exist for a sensor privacy toggle
          *
          * @hide
          */
         @IntDef(value = {
                 ENABLED,
-                DISABLED,
-                AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS,
-                AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS,
-                AUTOMOTIVE_DRIVER_ASSISTANCE_APPS
+                DISABLED
         })
         @Retention(RetentionPolicy.SOURCE)
         public @interface StateType {}
@@ -297,19 +266,6 @@
             private int mToggleType;
             private int mSensor;
             private boolean mEnabled;
-            private int mState;
-
-            @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-            private SensorPrivacyChangedParams(int toggleType, int sensor, int state) {
-                mToggleType = toggleType;
-                mSensor = sensor;
-                mState = state;
-                if (state == StateTypes.ENABLED) {
-                    mEnabled = true;
-                } else {
-                    mEnabled = false;
-                }
-            }
 
             private SensorPrivacyChangedParams(int toggleType, int sensor, boolean enabled) {
                 mToggleType = toggleType;
@@ -328,12 +284,6 @@
             public boolean isEnabled() {
                 return mEnabled;
             }
-
-            @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-            public @StateTypes.StateType int getState() {
-                return mState;
-            }
-
         }
     }
 
@@ -369,9 +319,6 @@
     private final ArrayMap<Pair<Integer, OnSensorPrivacyChangedListener>,
             OnSensorPrivacyChangedListener> mLegacyToggleListeners = new ArrayMap<>();
 
-    @GuardedBy("mLock")
-    private ArrayMap<String, Boolean> mCameraPrivacyAllowlist = null;
-
     /** The singleton ISensorPrivacyListener for IPC which will be used to dispatch to local
      * listeners */
     @NonNull
@@ -381,33 +328,12 @@
             synchronized (mLock) {
                 for (int i = 0; i < mToggleListeners.size(); i++) {
                     OnSensorPrivacyChangedListener listener = mToggleListeners.keyAt(i);
-                    if (Flags.privacyAllowlist()) {
-                        int state = enabled ?  StateTypes.ENABLED : StateTypes.DISABLED;
-                        mToggleListeners.valueAt(i).execute(() -> listener
-                                .onSensorPrivacyChanged(new OnSensorPrivacyChangedListener
-                                        .SensorPrivacyChangedParams(toggleType, sensor, state)));
-                    } else {
-                        mToggleListeners.valueAt(i).execute(() -> listener
-                                .onSensorPrivacyChanged(new OnSensorPrivacyChangedListener
-                                        .SensorPrivacyChangedParams(toggleType, sensor, enabled)));
-                    }
-                }
-            }
-        }
-
-        @Override
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        public void onSensorPrivacyStateChanged(int toggleType, int sensor, int state) {
-            synchronized (mLock) {
-                for (int i = 0; i < mToggleListeners.size(); i++) {
-                    OnSensorPrivacyChangedListener listener = mToggleListeners.keyAt(i);
                     mToggleListeners.valueAt(i).execute(() -> listener
                             .onSensorPrivacyChanged(new OnSensorPrivacyChangedListener
-                                    .SensorPrivacyChangedParams(toggleType, sensor, state)));
+                                    .SensorPrivacyChangedParams(toggleType, sensor, enabled)));
                 }
             }
         }
-
     };
 
     /** Whether the singleton ISensorPrivacyListener has been registered */
@@ -723,73 +649,6 @@
     }
 
     /**
-     * Returns sensor privacy state for a specific sensor.
-     *
-     * @return int sensor privacy state.
-     *
-     * @hide
-     */
-    @SystemApi
-    @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    public @StateTypes.StateType int getSensorPrivacyState(@ToggleType int toggleType,
-            @Sensors.Sensor int sensor) {
-        try {
-            return mService.getToggleSensorPrivacyState(toggleType, sensor);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-  /**
-     * Returns if camera privacy is enabled for a specific package.
-     *
-     * @return boolean sensor privacy state.
-     *
-     * @hide
-     */
-    @SystemApi
-    @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    public boolean isCameraPrivacyEnabled(@NonNull String packageName) {
-        try {
-            return mService.isCameraPrivacyEnabled(packageName);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    /**
-     * Returns camera privacy allowlist.
-     *
-     * @return List of automotive driver assistance packages for
-     * privacy allowlisting. The returned map includes the package
-     * name as key and the value is a Boolean which tells if that package
-     * is required by the car manufacturer as mandatory package for driving.
-     *
-     * @hide
-     */
-    @SystemApi
-    @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    public @NonNull Map<String, Boolean>  getCameraPrivacyAllowlist() {
-        synchronized (mLock) {
-            if (mCameraPrivacyAllowlist == null) {
-                mCameraPrivacyAllowlist = new ArrayMap<>();
-                try {
-                    for (CameraPrivacyAllowlistEntry entry :
-                            mService.getCameraPrivacyAllowlist()) {
-                        mCameraPrivacyAllowlist.put(entry.packageName, entry.isMandatory);
-                    }
-                } catch (RemoteException e) {
-                    throw e.rethrowFromSystemServer();
-                }
-            }
-            return mCameraPrivacyAllowlist;
-        }
-    }
-
-    /**
      * Sets sensor privacy to the specified state for an individual sensor.
      *
      * @param sensor the sensor which to change the state for
@@ -818,22 +677,6 @@
      * Sets sensor privacy to the specified state for an individual sensor.
      *
      * @param sensor the sensor which to change the state for
-     * @param state the state to which sensor privacy should be set.
-     *
-     * @hide
-     */
-    @SystemApi
-    @RequiresPermission(Manifest.permission.MANAGE_SENSOR_PRIVACY)
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    public void setSensorPrivacyState(@Sensors.Sensor int sensor,
-            @StateTypes.StateType int state) {
-        setSensorPrivacyState(resolveSourceFromCurrentContext(), sensor, state);
-    }
-
-    /**
-     * Sets sensor privacy to the specified state for an individual sensor.
-     *
-     * @param sensor the sensor which to change the state for
      * @param enable the state to which sensor privacy should be set.
      *
      * @hide
@@ -865,27 +708,6 @@
     }
 
     /**
-     * Sets sensor privacy to the specified state for an individual sensor.
-     *
-     * @param sensor the sensor which to change the state for
-     * @param state the state to which sensor privacy should be set.
-     *
-     * @hide
-     */
-    @TestApi
-    @RequiresPermission(Manifest.permission.MANAGE_SENSOR_PRIVACY)
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    public void setSensorPrivacyState(@Sources.Source int source, @Sensors.Sensor int sensor,
-            @StateTypes.StateType int state) {
-        try {
-            mService.setToggleSensorPrivacyState(mContext.getUserId(), source, sensor, state);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-
-    }
-
-    /**
      * Sets sensor privacy to the specified state for an individual sensor for the profile group of
      * context's user.
      *
@@ -923,28 +745,6 @@
     }
 
     /**
-     * Sets sensor privacy to the specified state for an individual sensor for the profile group of
-     * context's user.
-     *
-     * @param source the source using which the sensor is toggled.
-     * @param sensor the sensor which to change the state for
-     * @param state the state to which sensor privacy should be set.
-     *
-     * @hide
-     */
-    @RequiresPermission(Manifest.permission.MANAGE_SENSOR_PRIVACY)
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    public void setSensorPrivacyStateForProfileGroup(@Sources.Source int source,
-            @Sensors.Sensor int sensor, @StateTypes.StateType int state) {
-        try {
-            mService.setToggleSensorPrivacyStateForProfileGroup(mContext.getUserId(), source,
-                    sensor, state);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    /**
      * Don't show dialogs to turn off sensor privacy for this package.
      *
      * @param suppress Whether to suppress or re-enable.
@@ -1065,12 +865,6 @@
                             boolean enabled) {
                         listener.onAllSensorPrivacyChanged(enabled);
                     }
-
-                    @Override
-                    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-                    public void onSensorPrivacyStateChanged(int toggleType, int sensor,
-                            int state) {
-                    }
                 };
                 mListeners.put(listener, iListener);
             }
diff --git a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
index 7abe821..3b10e0d 100644
--- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
@@ -19,7 +19,9 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.content.ComponentName;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
@@ -40,6 +42,7 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.SystemProperties;
+import android.provider.Settings;
 import android.util.IntArray;
 import android.util.Log;
 import android.util.Pair;
@@ -53,6 +56,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -243,16 +247,19 @@
         private static final String PROXY_SERVICE_NAME =
                 "com.android.cameraextensions.CameraExtensionsProxyService";
 
+        @FlaggedApi(Flags.FLAG_CONCERT_MODE)
+        private static final int FALLBACK_PACKAGE_NAME =
+                com.android.internal.R.string.config_extensionFallbackPackageName;
+        @FlaggedApi(Flags.FLAG_CONCERT_MODE)
+        private static final int FALLBACK_SERVICE_NAME =
+                com.android.internal.R.string.config_extensionFallbackServiceName;
+
         // Singleton instance
         private static final CameraExtensionManagerGlobal GLOBAL_CAMERA_MANAGER =
                 new CameraExtensionManagerGlobal();
         private final Object mLock = new Object();
         private final int PROXY_SERVICE_DELAY_MS = 2000;
-        private InitializerFuture mInitFuture = null;
-        private ServiceConnection mConnection = null;
-        private int mConnectionCount = 0;
-        private ICameraExtensionsProxyService mProxy = null;
-        private boolean mSupportsAdvancedExtensions = false;
+        private ExtensionConnectionManager mConnectionManager = new ExtensionConnectionManager();
 
         // Singleton, don't allow construction
         private CameraExtensionManagerGlobal() {}
@@ -261,17 +268,17 @@
             return GLOBAL_CAMERA_MANAGER;
         }
 
-        private void releaseProxyConnectionLocked(Context ctx) {
-            if (mConnection != null ) {
-                ctx.unbindService(mConnection);
-                mConnection = null;
-                mProxy = null;
-                mConnectionCount = 0;
+        private void releaseProxyConnectionLocked(Context ctx, int extension) {
+            if (mConnectionManager.getConnection(extension) != null) {
+                ctx.unbindService(mConnectionManager.getConnection(extension));
+                mConnectionManager.setConnection(extension, null);
+                mConnectionManager.setProxy(extension, null);
+                mConnectionManager.resetConnectionCount(extension);
             }
         }
 
-        private void connectToProxyLocked(Context ctx) {
-            if (mConnection == null) {
+        private void connectToProxyLocked(Context ctx, int extension, boolean useFallback) {
+            if (mConnectionManager.getConnection(extension) == null) {
                 Intent intent = new Intent();
                 intent.setClassName(PROXY_PACKAGE_NAME, PROXY_SERVICE_NAME);
                 String vendorProxyPackage = SystemProperties.get(
@@ -287,34 +294,55 @@
                       + vendorProxyService);
                   intent.setClassName(vendorProxyPackage, vendorProxyService);
                 }
-                mInitFuture = new InitializerFuture();
-                mConnection = new ServiceConnection() {
+
+                if (Flags.concertMode() && useFallback) {
+                    String packageName = ctx.getResources().getString(FALLBACK_PACKAGE_NAME);
+                    String serviceName = ctx.getResources().getString(FALLBACK_SERVICE_NAME);
+
+                    if (!packageName.isEmpty() && !serviceName.isEmpty()) {
+                        Log.v(TAG,
+                                "Choosing the fallback software implementation package: "
+                                + packageName);
+                        Log.v(TAG,
+                                "Choosing the fallback software implementation service: "
+                                + serviceName);
+                        intent.setClassName(packageName, serviceName);
+                    }
+                }
+
+                InitializerFuture initFuture = new InitializerFuture();
+                ServiceConnection connection = new ServiceConnection() {
                     @Override
                     public void onServiceDisconnected(ComponentName component) {
-                        mConnection = null;
-                        mProxy = null;
+                        mConnectionManager.setConnection(extension, null);
+                        mConnectionManager.setProxy(extension, null);
                     }
 
                     @Override
                     public void onServiceConnected(ComponentName component, IBinder binder) {
-                        mProxy = ICameraExtensionsProxyService.Stub.asInterface(binder);
-                        if (mProxy == null) {
+                        ICameraExtensionsProxyService proxy =
+                                ICameraExtensionsProxyService.Stub.asInterface(binder);
+                        mConnectionManager.setProxy(extension, proxy);
+                        if (mConnectionManager.getProxy(extension) == null) {
                             throw new IllegalStateException("Camera Proxy service is null");
                         }
                         try {
-                            mSupportsAdvancedExtensions = mProxy.advancedExtensionsSupported();
+                            mConnectionManager.setAdvancedExtensionsSupported(extension,
+                                    mConnectionManager.getProxy(extension)
+                                    .advancedExtensionsSupported());
                         } catch (RemoteException e) {
                             Log.e(TAG, "Remote IPC failed!");
                         }
-                        mInitFuture.setStatus(true);
+                        initFuture.setStatus(true);
                     }
                 };
                 ctx.bindService(intent, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT |
                         Context.BIND_ABOVE_CLIENT | Context.BIND_NOT_VISIBLE,
-                        android.os.AsyncTask.THREAD_POOL_EXECUTOR, mConnection);
+                        android.os.AsyncTask.THREAD_POOL_EXECUTOR, connection);
+                mConnectionManager.setConnection(extension, connection);
 
                 try {
-                    mInitFuture.get(PROXY_SERVICE_DELAY_MS, TimeUnit.MILLISECONDS);
+                    initFuture.get(PROXY_SERVICE_DELAY_MS, TimeUnit.MILLISECONDS);
                 } catch (TimeoutException e) {
                     Log.e(TAG, "Timed out while initializing proxy service!");
                 }
@@ -366,64 +394,102 @@
             }
         }
 
-        public boolean registerClient(Context ctx, IBinder token) {
+        public boolean registerClientHelper(Context ctx, IBinder token, int extension,
+                boolean useFallback) {
             synchronized (mLock) {
                 boolean ret = false;
-                connectToProxyLocked(ctx);
-                if (mProxy == null) {
+                connectToProxyLocked(ctx, extension, useFallback);
+                if (mConnectionManager.getProxy(extension) == null) {
                     return false;
                 }
-                mConnectionCount++;
+                mConnectionManager.incrementConnectionCount(extension);
 
                 try {
-                    ret = mProxy.registerClient(token);
+                    ret = mConnectionManager.getProxy(extension).registerClient(token);
                 } catch (RemoteException e) {
                     Log.e(TAG, "Failed to initialize extension! Extension service does "
                             + " not respond!");
                 }
                 if (!ret) {
-                    mConnectionCount--;
+                    mConnectionManager.decrementConnectionCount(extension);
                 }
 
-                if (mConnectionCount <= 0) {
-                    releaseProxyConnectionLocked(ctx);
+                if (mConnectionManager.getConnectionCount(extension) <= 0) {
+                    releaseProxyConnectionLocked(ctx, extension);
                 }
 
                 return ret;
             }
         }
 
-        public void unregisterClient(Context ctx, IBinder token) {
+        @SuppressLint("NonUserGetterCalled")
+        public boolean registerClient(Context ctx, IBinder token, int extension,
+                String cameraId, Map<String, CameraMetadataNative> characteristicsMapNative) {
+            boolean ret = registerClientHelper(ctx, token, extension, false /*useFallback*/);
+
+            if (Flags.concertMode()) {
+                // Check if user enabled fallback impl
+                ContentResolver resolver = ctx.getContentResolver();
+                int userEnabled = Settings.Secure.getInt(resolver,
+                        Settings.Secure.CAMERA_EXTENSIONS_FALLBACK, 1);
+
+                boolean vendorImpl = true;
+                if (ret && (mConnectionManager.getProxy(extension) != null) && (userEnabled == 1)) {
+                    // At this point, we are connected to either CameraExtensionsProxyService or
+                    // the vendor extension proxy service. If the vendor does not support the
+                    // extension, unregisterClient and re-register client with the proxy service
+                    // containing the fallback impl
+                    vendorImpl = isExtensionSupported(cameraId, extension,
+                            characteristicsMapNative);
+                }
+
+                if (!vendorImpl) {
+                    unregisterClient(ctx, token, extension);
+                    ret = registerClientHelper(ctx, token, extension, true /*useFallback*/);
+
+                }
+            }
+
+            return ret;
+        }
+
+        public void unregisterClient(Context ctx, IBinder token, int extension) {
             synchronized (mLock) {
-                if (mProxy != null) {
+                if (mConnectionManager.getProxy(extension) != null) {
                     try {
-                        mProxy.unregisterClient(token);
+                        mConnectionManager.getProxy(extension).unregisterClient(token);
                     } catch (RemoteException e) {
                         Log.e(TAG, "Failed to de-initialize extension! Extension service does"
                                 + " not respond!");
                     } finally {
-                        mConnectionCount--;
-                        if (mConnectionCount <= 0) {
-                            releaseProxyConnectionLocked(ctx);
+                        mConnectionManager.decrementConnectionCount(extension);
+                        if (mConnectionManager.getConnectionCount(extension) <= 0) {
+                            releaseProxyConnectionLocked(ctx, extension);
                         }
                     }
                 }
             }
         }
 
-        public void initializeSession(IInitializeSessionCallback cb) throws RemoteException {
+        public void initializeSession(IInitializeSessionCallback cb, int extension)
+                throws RemoteException {
             synchronized (mLock) {
-                if (mProxy != null) {
-                    mProxy.initializeSession(cb);
+                if (mConnectionManager.getProxy(extension) != null
+                        && !mConnectionManager.isSessionInitialized()) {
+                    mConnectionManager.getProxy(extension).initializeSession(cb);
+                    mConnectionManager.setSessionInitialized(true);
+                } else {
+                    cb.onFailure();
                 }
             }
         }
 
-        public void releaseSession() {
+        public void releaseSession(int extension) {
             synchronized (mLock) {
-                if (mProxy != null) {
+                if (mConnectionManager.getProxy(extension) != null) {
                     try {
-                        mProxy.releaseSession();
+                        mConnectionManager.getProxy(extension).releaseSession();
+                        mConnectionManager.setSessionInitialized(false);
                     } catch (RemoteException e) {
                         Log.e(TAG, "Failed to release session! Extension service does"
                                 + " not respond!");
@@ -432,77 +498,157 @@
             }
         }
 
-        public boolean areAdvancedExtensionsSupported() {
-            return mSupportsAdvancedExtensions;
+        public boolean areAdvancedExtensionsSupported(int extension) {
+            return mConnectionManager.areAdvancedExtensionsSupported(extension);
         }
 
-        public IPreviewExtenderImpl initializePreviewExtension(int extensionType)
+        public IPreviewExtenderImpl initializePreviewExtension(int extension)
                 throws RemoteException {
             synchronized (mLock) {
-                if (mProxy != null) {
-                    return mProxy.initializePreviewExtension(extensionType);
+                if (mConnectionManager.getProxy(extension) != null) {
+                    return mConnectionManager.getProxy(extension)
+                            .initializePreviewExtension(extension);
                 } else {
                     return null;
                 }
             }
         }
 
-        public IImageCaptureExtenderImpl initializeImageExtension(int extensionType)
+        public IImageCaptureExtenderImpl initializeImageExtension(int extension)
                 throws RemoteException {
             synchronized (mLock) {
-                if (mProxy != null) {
-                    return mProxy.initializeImageExtension(extensionType);
+                if (mConnectionManager.getProxy(extension) != null) {
+                    return mConnectionManager.getProxy(extension)
+                            .initializeImageExtension(extension);
                 } else {
                     return null;
                 }
             }
         }
 
-        public IAdvancedExtenderImpl initializeAdvancedExtension(int extensionType)
+        public IAdvancedExtenderImpl initializeAdvancedExtension(int extension)
                 throws RemoteException {
             synchronized (mLock) {
-                if (mProxy != null) {
-                    return mProxy.initializeAdvancedExtension(extensionType);
+                if (mConnectionManager.getProxy(extension) != null) {
+                    return mConnectionManager.getProxy(extension)
+                            .initializeAdvancedExtension(extension);
                 } else {
                     return null;
                 }
             }
         }
+
+        private class ExtensionConnectionManager {
+            // Maps extension to ExtensionConnection
+            private Map<Integer, ExtensionConnection> mConnections = new HashMap<>();
+            private boolean mSessionInitialized = false;
+
+            public ExtensionConnectionManager() {
+                IntArray extensionList = new IntArray(EXTENSION_LIST.length);
+                extensionList.addAll(EXTENSION_LIST);
+                if (Flags.concertMode()) {
+                    extensionList.add(EXTENSION_EYES_FREE_VIDEOGRAPHY);
+                }
+
+                for (int extensionType : extensionList.toArray()) {
+                    mConnections.put(extensionType, new ExtensionConnection());
+                }
+            }
+
+            public ICameraExtensionsProxyService getProxy(@Extension int extension) {
+                return mConnections.get(extension).mProxy;
+            }
+
+            public ServiceConnection getConnection(@Extension int extension) {
+                return mConnections.get(extension).mConnection;
+            }
+
+            public int getConnectionCount(@Extension int extension) {
+                return mConnections.get(extension).mConnectionCount;
+            }
+
+            public boolean areAdvancedExtensionsSupported(@Extension int extension) {
+                return mConnections.get(extension).mSupportsAdvancedExtensions;
+            }
+
+            public boolean isSessionInitialized() {
+                return mSessionInitialized;
+            }
+
+            public void setProxy(@Extension int extension, ICameraExtensionsProxyService proxy) {
+                mConnections.get(extension).mProxy = proxy;
+            }
+
+            public void setConnection(@Extension int extension, ServiceConnection connection) {
+                mConnections.get(extension).mConnection = connection;
+            }
+
+            public void incrementConnectionCount(@Extension int extension) {
+                mConnections.get(extension).mConnectionCount++;
+            }
+
+            public void decrementConnectionCount(@Extension int extension) {
+                mConnections.get(extension).mConnectionCount--;
+            }
+
+            public void resetConnectionCount(@Extension int extension) {
+                mConnections.get(extension).mConnectionCount = 0;
+            }
+
+            public void setAdvancedExtensionsSupported(@Extension int extension,
+                    boolean advancedExtSupported) {
+                mConnections.get(extension).mSupportsAdvancedExtensions = advancedExtSupported;
+            }
+
+            public void setSessionInitialized(boolean initialized) {
+                mSessionInitialized = initialized;
+            }
+
+            private class ExtensionConnection {
+                public ICameraExtensionsProxyService mProxy = null;
+                public ServiceConnection mConnection = null;
+                public int mConnectionCount = 0;
+                public boolean mSupportsAdvancedExtensions = false;
+            }
+        }
     }
 
     /**
      * @hide
      */
-    public static boolean registerClient(Context ctx, IBinder token) {
-        return CameraExtensionManagerGlobal.get().registerClient(ctx, token);
+    public static boolean registerClient(Context ctx, IBinder token, int extension,
+            String cameraId, Map<String, CameraMetadataNative> characteristicsMapNative) {
+        return CameraExtensionManagerGlobal.get().registerClient(ctx, token, extension, cameraId,
+                characteristicsMapNative);
     }
 
     /**
      * @hide
      */
-    public static void unregisterClient(Context ctx, IBinder token) {
-        CameraExtensionManagerGlobal.get().unregisterClient(ctx, token);
+    public static void unregisterClient(Context ctx, IBinder token, int extension) {
+        CameraExtensionManagerGlobal.get().unregisterClient(ctx, token, extension);
     }
 
     /**
      * @hide
      */
-    public static void initializeSession(IInitializeSessionCallback cb) throws RemoteException {
-        CameraExtensionManagerGlobal.get().initializeSession(cb);
+    public static void initializeSession(IInitializeSessionCallback cb, int extension)
+            throws RemoteException {
+        CameraExtensionManagerGlobal.get().initializeSession(cb, extension);
     }
 
     /**
      * @hide
      */
-    public static void releaseSession() {
-        CameraExtensionManagerGlobal.get().releaseSession();
+    public static void releaseSession(int extension) {
+        CameraExtensionManagerGlobal.get().releaseSession(extension);
     }
 
     /**
      * @hide
      */
-    public static boolean areAdvancedExtensionsSupported() {
-        return CameraExtensionManagerGlobal.get().areAdvancedExtensionsSupported();
+    public static boolean areAdvancedExtensionsSupported(int extension) {
+        return CameraExtensionManagerGlobal.get().areAdvancedExtensionsSupported(extension);
     }
 
     /**
@@ -510,7 +656,7 @@
      */
     public static boolean isExtensionSupported(String cameraId, int extensionType,
             Map<String, CameraMetadataNative> characteristicsMap) {
-        if (areAdvancedExtensionsSupported()) {
+        if (areAdvancedExtensionsSupported(extensionType)) {
             try {
                 IAdvancedExtenderImpl extender = initializeAdvancedExtension(extensionType);
                 return extender.isExtensionAvailable(cameraId, characteristicsMap);
@@ -600,24 +746,24 @@
     public @NonNull List<Integer> getSupportedExtensions() {
         ArrayList<Integer> ret = new ArrayList<>();
         final IBinder token = new Binder(TAG + "#getSupportedExtensions:" + mCameraId);
-        boolean success = registerClient(mContext, token);
-        if (!success) {
-            return Collections.unmodifiableList(ret);
-        }
 
         IntArray extensionList = new IntArray(EXTENSION_LIST.length);
         extensionList.addAll(EXTENSION_LIST);
         if (Flags.concertMode()) {
             extensionList.add(EXTENSION_EYES_FREE_VIDEOGRAPHY);
         }
-        try {
-            for (int extensionType : extensionList.toArray()) {
-                if (isExtensionSupported(mCameraId, extensionType, mCharacteristicsMapNative)) {
+
+        for (int extensionType : extensionList.toArray()) {
+            try {
+                boolean success = registerClient(mContext, token, extensionType, mCameraId,
+                        mCharacteristicsMapNative);
+                if (success && isExtensionSupported(mCameraId, extensionType,
+                        mCharacteristicsMapNative)) {
                     ret.add(extensionType);
                 }
+            } finally {
+                unregisterClient(mContext, token, extensionType);
             }
-        } finally {
-            unregisterClient(mContext, token);
         }
 
         return Collections.unmodifiableList(ret);
@@ -643,7 +789,8 @@
     public <T> @Nullable T get(@Extension int extension,
             @NonNull CameraCharacteristics.Key<T> key) {
         final IBinder token = new Binder(TAG + "#get:" + mCameraId);
-        boolean success = registerClient(mContext, token);
+        boolean success = registerClient(mContext, token, extension, mCameraId,
+                mCharacteristicsMapNative);
         if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
@@ -653,7 +800,7 @@
                 throw new IllegalArgumentException("Unsupported extension");
             }
 
-            if (areAdvancedExtensionsSupported() && getKeys(extension).contains(key)) {
+            if (areAdvancedExtensionsSupported(extension) && getKeys(extension).contains(key)) {
                 IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
                 extender.init(mCameraId, mCharacteristicsMapNative);
                 CameraMetadataNative metadata =
@@ -670,7 +817,7 @@
             Log.e(TAG, "Failed to query the extension for the specified key! Extension "
                     + "service does not respond!");
         } finally {
-            unregisterClient(mContext, token);
+            unregisterClient(mContext, token, extension);
         }
         return null;
     }
@@ -691,7 +838,8 @@
     public @NonNull Set<CameraCharacteristics.Key> getKeys(@Extension int extension) {
         final IBinder token =
                 new Binder(TAG + "#getKeys:" + mCameraId);
-        boolean success = registerClient(mContext, token);
+        boolean success = registerClient(mContext, token, extension, mCameraId,
+                mCharacteristicsMapNative);
         if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
@@ -703,7 +851,7 @@
                 throw new IllegalArgumentException("Unsupported extension");
             }
 
-            if (areAdvancedExtensionsSupported()) {
+            if (areAdvancedExtensionsSupported(extension)) {
                 IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
                 extender.init(mCameraId, mCharacteristicsMapNative);
                 CameraMetadataNative metadata =
@@ -732,7 +880,7 @@
             Log.e(TAG, "Failed to query the extension for all available keys! Extension "
                     + "service does not respond!");
         } finally {
-            unregisterClient(mContext, token);
+            unregisterClient(mContext, token, extension);
         }
         return Collections.unmodifiableSet(ret);
     }
@@ -755,7 +903,8 @@
      */
     public boolean isPostviewAvailable(@Extension int extension) {
         final IBinder token = new Binder(TAG + "#isPostviewAvailable:" + mCameraId);
-        boolean success = registerClient(mContext, token);
+        boolean success = registerClient(mContext, token, extension, mCameraId,
+                mCharacteristicsMapNative);
         if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
@@ -765,7 +914,7 @@
                 throw new IllegalArgumentException("Unsupported extension");
             }
 
-            if (areAdvancedExtensionsSupported()) {
+            if (areAdvancedExtensionsSupported(extension)) {
                 IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
                 extender.init(mCameraId, mCharacteristicsMapNative);
                 return extender.isPostviewAvailable();
@@ -779,7 +928,7 @@
             Log.e(TAG, "Failed to query the extension for postview availability! Extension "
                     + "service does not respond!");
         } finally {
-            unregisterClient(mContext, token);
+            unregisterClient(mContext, token, extension);
         }
 
         return false;
@@ -813,7 +962,8 @@
     public List<Size> getPostviewSupportedSizes(@Extension int extension,
             @NonNull Size captureSize, int format) {
         final IBinder token = new Binder(TAG + "#getPostviewSupportedSizes:" + mCameraId);
-        boolean success = registerClient(mContext, token);
+        boolean success = registerClient(mContext, token, extension, mCameraId,
+                mCharacteristicsMapNative);
         if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
@@ -831,7 +981,7 @@
             StreamConfigurationMap streamMap = mCharacteristicsMap.get(mCameraId).get(
                     CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
 
-            if (areAdvancedExtensionsSupported()) {
+            if (areAdvancedExtensionsSupported(extension)) {
                 switch(format) {
                     case ImageFormat.YUV_420_888:
                     case ImageFormat.JPEG:
@@ -879,7 +1029,7 @@
                     + "service does not respond!");
             return Collections.emptyList();
         } finally {
-            unregisterClient(mContext, token);
+            unregisterClient(mContext, token, extension);
         }
     }
 
@@ -917,7 +1067,8 @@
         //       ambiguity is resolved in b/169799538.
 
         final IBinder token = new Binder(TAG + "#getExtensionSupportedSizes:" + mCameraId);
-        boolean success = registerClient(mContext, token);
+        boolean success = registerClient(mContext, token, extension, mCameraId,
+                mCharacteristicsMapNative);
         if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
@@ -929,7 +1080,7 @@
 
             StreamConfigurationMap streamMap = mCharacteristicsMap.get(mCameraId).get(
                     CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
-            if (areAdvancedExtensionsSupported()) {
+            if (areAdvancedExtensionsSupported(extension)) {
                 IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
                 extender.init(mCameraId, mCharacteristicsMapNative);
                 return generateSupportedSizes(
@@ -948,7 +1099,7 @@
                     + " not respond!");
             return new ArrayList<>();
         } finally {
-            unregisterClient(mContext, token);
+            unregisterClient(mContext, token, extension);
         }
     }
 
@@ -978,7 +1129,8 @@
     List<Size> getExtensionSupportedSizes(@Extension int extension, int format) {
         try {
             final IBinder token = new Binder(TAG + "#getExtensionSupportedSizes:" + mCameraId);
-            boolean success = registerClient(mContext, token);
+            boolean success = registerClient(mContext, token, extension, mCameraId,
+                    mCharacteristicsMapNative);
             if (!success) {
                 throw new IllegalArgumentException("Unsupported extensions");
             }
@@ -990,7 +1142,7 @@
 
                 StreamConfigurationMap streamMap = mCharacteristicsMap.get(mCameraId).get(
                         CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
-                if (areAdvancedExtensionsSupported()) {
+                if (areAdvancedExtensionsSupported(extension)) {
                     switch(format) {
                         case ImageFormat.YUV_420_888:
                         case ImageFormat.JPEG:
@@ -1035,7 +1187,7 @@
                     }
                 }
             } finally {
-                unregisterClient(mContext, token);
+                unregisterClient(mContext, token, extension);
             }
         } catch (RemoteException e) {
             Log.e(TAG, "Failed to query the extension supported sizes! Extension service does"
@@ -1073,7 +1225,8 @@
         }
 
         final IBinder token = new Binder(TAG + "#getEstimatedCaptureLatencyRangeMillis:" + mCameraId);
-        boolean success = registerClient(mContext, token);
+        boolean success = registerClient(mContext, token, extension, mCameraId,
+                mCharacteristicsMapNative);
         if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
@@ -1087,7 +1240,7 @@
                     new android.hardware.camera2.extension.Size();
             sz.width = captureOutputSize.getWidth();
             sz.height = captureOutputSize.getHeight();
-            if (areAdvancedExtensionsSupported()) {
+            if (areAdvancedExtensionsSupported(extension)) {
                 IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
                 extender.init(mCameraId, mCharacteristicsMapNative);
                 LatencyRange latencyRange = extender.getEstimatedCaptureLatencyRange(mCameraId,
@@ -1126,7 +1279,7 @@
             Log.e(TAG, "Failed to query the extension capture latency! Extension service does"
                     + " not respond!");
         } finally {
-            unregisterClient(mContext, token);
+            unregisterClient(mContext, token, extension);
         }
 
         return null;
@@ -1143,7 +1296,8 @@
      */
     public boolean isCaptureProcessProgressAvailable(@Extension int extension) {
         final IBinder token = new Binder(TAG + "#isCaptureProcessProgressAvailable:" + mCameraId);
-        boolean success = registerClient(mContext, token);
+        boolean success = registerClient(mContext, token, extension, mCameraId,
+                mCharacteristicsMapNative);
         if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
@@ -1153,7 +1307,7 @@
                 throw new IllegalArgumentException("Unsupported extension");
             }
 
-            if (areAdvancedExtensionsSupported()) {
+            if (areAdvancedExtensionsSupported(extension)) {
                 IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
                 extender.init(mCameraId, mCharacteristicsMapNative);
                 return extender.isCaptureProcessProgressAvailable();
@@ -1167,7 +1321,7 @@
             Log.e(TAG, "Failed to query the extension progress callbacks! Extension service does"
                     + " not respond!");
         } finally {
-            unregisterClient(mContext, token);
+            unregisterClient(mContext, token, extension);
         }
 
         return false;
@@ -1195,7 +1349,8 @@
     @NonNull
     public Set<CaptureRequest.Key> getAvailableCaptureRequestKeys(@Extension int extension) {
         final IBinder token = new Binder(TAG + "#getAvailableCaptureRequestKeys:" + mCameraId);
-        boolean success = registerClient(mContext, token);
+        boolean success = registerClient(mContext, token, extension, mCameraId,
+                mCharacteristicsMapNative);
         if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
@@ -1208,7 +1363,7 @@
             }
 
             CameraMetadataNative captureRequestMeta = null;
-            if (areAdvancedExtensionsSupported()) {
+            if (areAdvancedExtensionsSupported(extension)) {
                 IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
                 extender.init(mCameraId, mCharacteristicsMapNative);
                 captureRequestMeta = extender.getAvailableCaptureRequestKeys(mCameraId);
@@ -1250,7 +1405,7 @@
         } catch (RemoteException e) {
             throw new IllegalStateException("Failed to query the available capture request keys!");
         } finally {
-            unregisterClient(mContext, token);
+            unregisterClient(mContext, token, extension);
         }
 
         return Collections.unmodifiableSet(ret);
@@ -1282,7 +1437,8 @@
     @NonNull
     public Set<CaptureResult.Key> getAvailableCaptureResultKeys(@Extension int extension) {
         final IBinder token = new Binder(TAG + "#getAvailableCaptureResultKeys:" + mCameraId);
-        boolean success = registerClient(mContext, token);
+        boolean success = registerClient(mContext, token, extension, mCameraId,
+                mCharacteristicsMapNative);
         if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
@@ -1294,7 +1450,7 @@
             }
 
             CameraMetadataNative captureResultMeta = null;
-            if (areAdvancedExtensionsSupported()) {
+            if (areAdvancedExtensionsSupported(extension)) {
                 IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
                 extender.init(mCameraId, mCharacteristicsMapNative);
                 captureResultMeta = extender.getAvailableCaptureResultKeys(mCameraId);
@@ -1336,7 +1492,7 @@
         } catch (RemoteException e) {
             throw new IllegalStateException("Failed to query the available capture result keys!");
         } finally {
-            unregisterClient(mContext, token);
+            unregisterClient(mContext, token, extension);
         }
 
         return Collections.unmodifiableSet(ret);
diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
index f6c8f36..b2032fa 100644
--- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
@@ -102,6 +102,8 @@
 
     private boolean mInitialized;
     private boolean mSessionClosed;
+    private int mExtensionType;
+
 
     private final Context mContext;
 
@@ -205,7 +207,7 @@
         CameraAdvancedExtensionSessionImpl ret = new CameraAdvancedExtensionSessionImpl(ctx,
                 extender, cameraDevice, characteristicsMapNative, repeatingRequestSurface,
                 burstCaptureSurface, postviewSurface, config.getStateCallback(),
-                config.getExecutor(), sessionId, token);
+                config.getExecutor(), sessionId, token, config.getExtension());
 
         ret.mStatsAggregator.setClientName(ctx.getOpPackageName());
         ret.mStatsAggregator.setExtensionType(config.getExtension());
@@ -223,7 +225,8 @@
             @Nullable Surface postviewSurface,
             @NonNull StateCallback callback, @NonNull Executor executor,
             int sessionId,
-            @NonNull IBinder token) {
+            @NonNull IBinder token,
+            int extension) {
         mContext = ctx;
         mAdvancedExtender = extender;
         mCameraDevice = cameraDevice;
@@ -242,6 +245,7 @@
         mSessionId = sessionId;
         mToken = token;
         mInterfaceLock = cameraDevice.mInterfaceLock;
+        mExtensionType = extension;
 
         mStatsAggregator = new ExtensionSessionStatsAggregator(mCameraDevice.getId(),
                 /*isAdvanced=*/true);
@@ -583,9 +587,9 @@
             if (mToken != null) {
                 if (mInitialized || (mCaptureSession != null)) {
                     notifyClose = true;
-                    CameraExtensionCharacteristics.releaseSession();
+                    CameraExtensionCharacteristics.releaseSession(mExtensionType);
                 }
-                CameraExtensionCharacteristics.unregisterClient(mContext, mToken);
+                CameraExtensionCharacteristics.unregisterClient(mContext, mToken, mExtensionType);
             }
             mInitialized = false;
             mToken = null;
@@ -654,7 +658,8 @@
             }
 
             try {
-                CameraExtensionCharacteristics.initializeSession(mInitializeHandler);
+                CameraExtensionCharacteristics.initializeSession(
+                        mInitializeHandler, mExtensionType);
             } catch (RemoteException e) {
                 Log.e(TAG, "Failed to initialize session! Extension service does"
                         + " not respond!");
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
index ccb24e7..f03876b 100644
--- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
@@ -2559,13 +2559,16 @@
         boolean initializationFailed = true;
         IBinder token = new Binder(TAG + " : " + mNextSessionId++);
         try {
-            boolean ret = CameraExtensionCharacteristics.registerClient(mContext, token);
+            boolean ret = CameraExtensionCharacteristics.registerClient(mContext, token,
+                    extensionConfiguration.getExtension(), mCameraId,
+                    CameraExtensionUtils.getCharacteristicsMapNative(characteristicsMap));
             if (!ret) {
                 token = null;
                 throw new UnsupportedOperationException("Unsupported extension!");
             }
 
-            if (CameraExtensionCharacteristics.areAdvancedExtensionsSupported()) {
+            if (CameraExtensionCharacteristics.areAdvancedExtensionsSupported(
+                        extensionConfiguration.getExtension())) {
                 mCurrentAdvancedExtensionSession =
                         CameraAdvancedExtensionSessionImpl.createCameraAdvancedExtensionSession(
                                 this, characteristicsMap, mContext, extensionConfiguration,
@@ -2580,7 +2583,8 @@
             throw new CameraAccessException(CameraAccessException.CAMERA_ERROR);
         } finally {
             if (initializationFailed && (token != null)) {
-                CameraExtensionCharacteristics.unregisterClient(mContext, token);
+                CameraExtensionCharacteristics.unregisterClient(mContext, token,
+                        extensionConfiguration.getExtension());
             }
         }
     }
diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
index db7055b..725b413 100644
--- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
@@ -118,6 +118,7 @@
     // In case the client doesn't explicitly enable repeating requests, the framework
     // will do so internally.
     private boolean mInternalRepeatingRequestEnabled = true;
+    private int mExtensionType;
 
     private final Context mContext;
 
@@ -244,7 +245,8 @@
                 sessionId,
                 token,
                 extensionChars.getAvailableCaptureRequestKeys(config.getExtension()),
-                extensionChars.getAvailableCaptureResultKeys(config.getExtension()));
+                extensionChars.getAvailableCaptureResultKeys(config.getExtension()),
+                config.getExtension());
 
         session.mStatsAggregator.setClientName(ctx.getOpPackageName());
         session.mStatsAggregator.setExtensionType(config.getExtension());
@@ -266,7 +268,8 @@
             int sessionId,
             @NonNull IBinder token,
             @NonNull Set<CaptureRequest.Key> requestKeys,
-            @Nullable Set<CaptureResult.Key> resultKeys) {
+            @Nullable Set<CaptureResult.Key> resultKeys,
+            int extension) {
         mContext = ctx;
         mImageExtender = imageExtender;
         mPreviewExtender = previewExtender;
@@ -289,6 +292,7 @@
         mSupportedResultKeys = resultKeys;
         mCaptureResultsSupported = !resultKeys.isEmpty();
         mInterfaceLock = cameraDevice.mInterfaceLock;
+        mExtensionType = extension;
 
         mStatsAggregator = new ExtensionSessionStatsAggregator(mCameraDevice.getId(),
                 /*isAdvanced=*/false);
@@ -881,9 +885,9 @@
             if (mToken != null) {
                 if (mInitialized || (mCaptureSession != null)) {
                     notifyClose = true;
-                    CameraExtensionCharacteristics.releaseSession();
+                    CameraExtensionCharacteristics.releaseSession(mExtensionType);
                 }
-                CameraExtensionCharacteristics.unregisterClient(mContext, mToken);
+                CameraExtensionCharacteristics.unregisterClient(mContext, mToken, mExtensionType);
             }
             mInitialized = false;
             mToken = null;
@@ -1000,7 +1004,8 @@
                 mStatsAggregator.commit(/*isFinal*/false);
                 try {
                     finishPipelineInitialization();
-                    CameraExtensionCharacteristics.initializeSession(mInitializeHandler);
+                    CameraExtensionCharacteristics.initializeSession(
+                            mInitializeHandler, mExtensionType);
                 } catch (RemoteException e) {
                     Log.e(TAG, "Failed to initialize session! Extension service does"
                             + " not respond!");
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 26f46cf..b026ce9 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -668,6 +668,23 @@
             "android.settings.MANAGE_APP_LONG_RUNNING_JOBS";
 
     /**
+     * Activity Action: Show settings to allow configuration of
+     * {@link Manifest.permission#RUN_BACKUP_JOBS} permission.
+     *
+     * Input: Optionally, the Intent's data URI can specify the application package name to
+     * directly invoke the management GUI specific to the package name. For example
+     * "package:com.my.app".
+     * <p>
+     * Output: When a package data uri is passed as input, the activity result is set to
+     * {@link android.app.Activity#RESULT_OK} if the permission was granted to the app. Otherwise,
+     * the result is set to {@link android.app.Activity#RESULT_CANCELED}.
+     */
+    @FlaggedApi(Flags.FLAG_BACKUP_TASKS_SETTINGS_SCREEN)
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_REQUEST_RUN_BACKUP_JOBS =
+            "android.settings.REQUEST_RUN_BACKUP_JOBS";
+
+    /**
      * Activity Action: Show settings to allow configuration of cross-profile access for apps
      *
      * Input: Optionally, the Intent's data URI can specify the application package name to
@@ -11833,6 +11850,7 @@
          * Whether to enable camera extensions software fallback.
          * @hide
          */
+        @Readable
         public static final String CAMERA_EXTENSIONS_FALLBACK = "camera_extensions_fallback";
 
         /**
diff --git a/core/java/android/provider/flags.aconfig b/core/java/android/provider/flags.aconfig
index 0f12b13..ea1ac27 100644
--- a/core/java/android/provider/flags.aconfig
+++ b/core/java/android/provider/flags.aconfig
@@ -13,3 +13,10 @@
     description: "This flag controls new E2EE contact keys API"
     bug: "290696572"
 }
+
+flag {
+    name: "backup_tasks_settings_screen"
+    namespace: "backstage_power"
+    description: "Add a new settings page for the RUN_BACKUP_JOBS permission."
+    bug: "320563660"
+}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index a9f1897..c22986b 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -30,6 +30,7 @@
 import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_UNKNOWN;
 import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_HASH;
 import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_HASH_ERROR_CODE;
+import static android.view.flags.Flags.FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API;
 import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY;
 import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API;
 import static android.view.flags.Flags.enableUseMeasureCacheDuringForceLayout;
@@ -1946,6 +1947,41 @@
     static final int TOOLTIP = 0x40000000;
 
     /** @hide */
+    @IntDef(prefix = { "CONTENT_SENSITIVITY_" }, value = {
+            CONTENT_SENSITIVITY_AUTO,
+            CONTENT_SENSITIVITY_SENSITIVE,
+            CONTENT_SENSITIVITY_NOT_SENSITIVE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ContentSensitivity {}
+
+    /**
+     * Automatically determine whether a view displays sensitive content. For example, available
+     * autofill hints (or some other signal) can be used to determine if this view
+     * displays sensitive content.
+     *
+     * @see #getContentSensitivity()
+     */
+    @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+    public static final int CONTENT_SENSITIVITY_AUTO = 0x0;
+
+    /**
+     * The view displays sensitive content.
+     *
+     * @see #getContentSensitivity()
+     */
+    @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+    public static final int CONTENT_SENSITIVITY_SENSITIVE = 0x1;
+
+    /**
+     * The view doesn't display sensitive content.
+     *
+     * @see #getContentSensitivity()
+     */
+    @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+    public static final int CONTENT_SENSITIVITY_NOT_SENSITIVE = 0x2;
+
+    /** @hide */
     @IntDef(flag = true, prefix = { "FOCUSABLES_" }, value = {
             FOCUSABLES_ALL,
             FOCUSABLES_TOUCH_MODE
@@ -3646,6 +3682,7 @@
      *           1                      PFLAG4_ROTARY_HAPTICS_ENABLED
      *          1                       PFLAG4_ROTARY_HAPTICS_SCROLL_SINCE_LAST_ROTARY_INPUT
      *         1                        PFLAG4_ROTARY_HAPTICS_WAITING_FOR_SCROLL_EVENT
+     *       11                         PFLAG4_CONTENT_SENSITIVITY_MASK
      * |-------|-------|-------|-------|
      */
 
@@ -3762,6 +3799,15 @@
      */
     private static final int PFLAG4_ROTARY_HAPTICS_WAITING_FOR_SCROLL_EVENT = 0x800000;
 
+    private static final int PFLAG4_CONTENT_SENSITIVITY_SHIFT = 24;
+
+    /**
+     * Mask for obtaining the bits which specify how to determine whether a view
+     * displays sensitive content or not.
+     */
+    private static final int PFLAG4_CONTENT_SENSITIVITY_MASK =
+            (CONTENT_SENSITIVITY_AUTO | CONTENT_SENSITIVITY_SENSITIVE
+                    | CONTENT_SENSITIVITY_NOT_SENSITIVE) << PFLAG4_CONTENT_SENSITIVITY_SHIFT;
     /* End of masks for mPrivateFlags4 */
 
     /** @hide */
@@ -10150,6 +10196,54 @@
     }
 
     /**
+     * Sets content sensitivity mode to determine whether this view displays sensitive content.
+     *
+     * @param mode {@link #CONTENT_SENSITIVITY_AUTO}, {@link #CONTENT_SENSITIVITY_NOT_SENSITIVE}
+     *                                            or {@link #CONTENT_SENSITIVITY_SENSITIVE}
+     */
+    @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+    public final void setContentSensitivity(@ContentSensitivity int mode)  {
+        mPrivateFlags4 &= ~PFLAG4_CONTENT_SENSITIVITY_MASK;
+        mPrivateFlags4 |= ((mode << PFLAG4_CONTENT_SENSITIVITY_SHIFT)
+                & PFLAG4_CONTENT_SENSITIVITY_MASK);
+    }
+
+    /**
+     * Gets content sensitivity mode to determine whether this view displays sensitive content.
+     *
+     * <p>See {@link #setContentSensitivity(int)} and
+     * {@link #isContentSensitive()} for more info about this mode.
+     *
+     * @return {@link #CONTENT_SENSITIVITY_AUTO} by default, or value passed to
+     * {@link #setContentSensitivity(int)}.
+     */
+    @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+    public @ContentSensitivity
+    final int getContentSensitivity() {
+        return (mPrivateFlags4 & PFLAG4_CONTENT_SENSITIVITY_MASK)
+                >> PFLAG4_CONTENT_SENSITIVITY_SHIFT;
+    }
+
+    /**
+     * Returns whether this view displays sensitive content, based
+     * on the value explicitly set by {@link #setContentSensitivity(int)}.
+     *
+     * @return whether the view displays sensitive content.
+     *
+     * @see #setContentSensitivity(int)
+     * @see #CONTENT_SENSITIVITY_AUTO
+     * @see #CONTENT_SENSITIVITY_SENSITIVE
+     * @see #CONTENT_SENSITIVITY_NOT_SENSITIVE
+     */
+    @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+    public final boolean isContentSensitive() {
+        if (getContentSensitivity() == CONTENT_SENSITIVITY_SENSITIVE) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
      * Gets the mode for determining whether this view is important for content capture.
      *
      * <p>See {@link #setImportantForContentCapture(int)} and
diff --git a/core/proto/android/hardware/sensorprivacy.proto b/core/proto/android/hardware/sensorprivacy.proto
index e368c6a..9359528 100644
--- a/core/proto/android/hardware/sensorprivacy.proto
+++ b/core/proto/android/hardware/sensorprivacy.proto
@@ -91,9 +91,6 @@
     enum StateType {
         ENABLED = 1;
         DISABLED = 2;
-        AUTO_DRIVER_ASSISTANCE_HELPFUL_APPS = 3;
-        AUTO_DRIVER_ASSISTANCE_REQUIRED_APPS = 4;
-        AUTO_DRIVER_ASSISTANCE_APPS = 5;
     }
 
     // DEPRECATED
@@ -137,4 +134,4 @@
     // Source for which sensor privacy was toggled.
     optional Source source = 1;
 
-}
+}
\ No newline at end of file
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 316001a..a425bb0 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -1730,16 +1730,6 @@
         android:description="@string/permdesc_cameraHeadlessSystemUser"
         android:protectionLevel="signature" />
 
-
-    <!-- @SystemApi Allows camera access of allowlisted driver assistance apps
-         to be controlled separately.
-         <p> Not for use by third-party applications.
-         @FlaggedApi("com.android.internal.camera.flags.privacy_allowlist")
-         @hide
-    -->
-    <permission android:name="android.permission.CAMERA_PRIVACY_ALLOWLIST"
-        android:protectionLevel="signature|privileged" />
-
     <!-- ====================================================================== -->
     <!-- Permissions for accessing the device sensors                           -->
     <!-- ====================================================================== -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index d4e727e..c6bc589 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2141,6 +2141,12 @@
         <item>com.android.location.fused</item>
     </string-array>
 
+   <!-- Package name of the extension software fallback. -->
+    <string name="config_extensionFallbackPackageName" translatable="false"></string>
+
+   <!-- Service name of the extension software fallback. -->
+    <string name="config_extensionFallbackServiceName" translatable="false"></string>
+
     <!-- Package name(s) of Advanced Driver Assistance applications. These packages have additional
     management of access to location, specific to driving assistance use-cases. They must be system
     packages. This configuration is only applicable to devices that declare
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 3df7570..3d19c85 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2154,6 +2154,8 @@
   <java-symbol type="string" name="config_systemImageEditor" />
   <java-symbol type="string" name="config_datause_iface" />
   <java-symbol type="string" name="config_activityRecognitionHardwarePackageName" />
+  <java-symbol type="string" name="config_extensionFallbackPackageName" />
+  <java-symbol type="string" name="config_extensionFallbackServiceName" />
   <java-symbol type="string" name="config_fusedLocationProviderPackageName" />
   <java-symbol type="string" name="config_gnssLocationProviderPackageName" />
   <java-symbol type="string" name="config_geocoderProviderPackageName" />
diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
index 610b8ae..0b0fd66 100644
--- a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
+++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
@@ -19,10 +19,12 @@
 import android.accessibilityservice.AccessibilityService;
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.accessibilityservice.IAccessibilityServiceConnection;
+import android.accessibilityservice.IBrailleDisplayController;
 import android.accessibilityservice.MagnificationConfig;
 import android.annotation.NonNull;
 import android.content.pm.ParceledListSlice;
 import android.graphics.Region;
+import android.hardware.usb.UsbDevice;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteCallback;
@@ -237,4 +239,15 @@
             int accessibilityWindowId,
             SurfaceControl sc,
             IAccessibilityInteractionConnectionCallback callback) {}
+
+    @Override
+    public void connectBluetoothBrailleDisplay(String bluetoothAddress,
+            IBrailleDisplayController controller) {}
+
+    @Override
+    public void connectUsbBrailleDisplay(UsbDevice usbDevice,
+            IBrailleDisplayController controller) {}
+
+    @Override
+    public void setTestBrailleDisplayData(List<Bundle> brailleDisplays) {}
 }
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 0baaff0..d8713f7a 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -575,6 +575,8 @@
         <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
         <!-- Permissions required for CTS test - CtsContactKeysProviderPrivilegedApp -->
         <permission name="android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS"/>
+        <!-- Permission required for CTS test - PackageManagerTest -->
+        <permission name="android.permission.DOMAIN_VERIFICATION_AGENT"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.statementservice">
diff --git a/libs/input/SpriteIcon.h b/libs/input/SpriteIcon.h
index 9e6cc81..0939af4 100644
--- a/libs/input/SpriteIcon.h
+++ b/libs/input/SpriteIcon.h
@@ -27,33 +27,20 @@
  * Icon that a sprite displays, including its hotspot.
  */
 struct SpriteIcon {
-    inline SpriteIcon() : style(PointerIconStyle::TYPE_NULL), hotSpotX(0), hotSpotY(0) {}
-    inline SpriteIcon(const graphics::Bitmap& bitmap, PointerIconStyle style, float hotSpotX,
-                      float hotSpotY, bool drawNativeDropShadow)
+    explicit SpriteIcon() = default;
+    explicit SpriteIcon(const graphics::Bitmap& bitmap, PointerIconStyle style, float hotSpotX,
+                        float hotSpotY, bool drawNativeDropShadow)
           : bitmap(bitmap),
             style(style),
             hotSpotX(hotSpotX),
             hotSpotY(hotSpotY),
             drawNativeDropShadow(drawNativeDropShadow) {}
 
-    graphics::Bitmap bitmap;
-    PointerIconStyle style;
-    float hotSpotX;
-    float hotSpotY;
-    bool drawNativeDropShadow;
-
-    inline SpriteIcon copy() const {
-        return SpriteIcon(bitmap.copy(ANDROID_BITMAP_FORMAT_RGBA_8888), style, hotSpotX, hotSpotY,
-                          drawNativeDropShadow);
-    }
-
-    inline void reset() {
-        bitmap.reset();
-        style = PointerIconStyle::TYPE_NULL;
-        hotSpotX = 0;
-        hotSpotY = 0;
-        drawNativeDropShadow = false;
-    }
+    graphics::Bitmap bitmap{};
+    PointerIconStyle style{PointerIconStyle::TYPE_NULL};
+    float hotSpotX{};
+    float hotSpotY{};
+    bool drawNativeDropShadow{false};
 
     inline bool isValid() const { return bitmap.isValid() && !bitmap.isEmpty(); }
 
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
index e55bbec..9ecbd50 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
@@ -31,6 +31,7 @@
 import java.io.FileOutputStream;
 import java.io.PrintStream;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 public class SettingsStateTest extends AndroidTestCase {
@@ -626,4 +627,121 @@
             assertEquals(VALUE2, settingsState.getSettingLocked(INVALID_STAGED_FLAG_1).getValue());
         }
     }
+
+    public void testsetSettingsLockedKeepTrunkDefault() throws Exception {
+        final PrintStream os = new PrintStream(new FileOutputStream(mSettingsFile));
+        os.print(
+                "<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>"
+                        + "<settings version=\"120\">"
+                        + "  <setting id=\"0\" name=\"test_namespace/flag0\" "
+                            + "value=\"false\" package=\"com.android.flags\" />"
+                        + "  <setting id=\"1\" name=\"test_namespace/flag1\" "
+                            + "value=\"false\" package=\"com.android.flags\" />"
+                        + "  <setting id=\"2\" name=\"test_namespace/com.android.flags.flag3\" "
+                            + "value=\"false\" package=\"com.android.flags\" />"
+                        + "  <setting id=\"3\" "
+                        + "name=\"test_another_namespace/com.android.another.flags.flag0\" "
+                            + "value=\"false\" package=\"com.android.another.flags\" />"
+                        + "</settings>");
+        os.close();
+
+        int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0);
+
+        SettingsState settingsState = new SettingsState(
+                getContext(), mLock, mSettingsFile, configKey,
+                SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
+
+        String prefix = "test_namespace";
+        Map<String, String> keyValues =
+                Map.of("test_namespace/flag0", "true", "test_namespace/flag2", "false");
+        String packageName = "com.android.flags";
+
+        parsed_flags flags = parsed_flags
+                .newBuilder()
+                .addParsedFlag(parsed_flag
+                    .newBuilder()
+                        .setPackage(packageName)
+                        .setName("flag3")
+                        .setNamespace(prefix)
+                        .setDescription("test flag")
+                        .addBug("12345678")
+                        .setState(Aconfig.flag_state.DISABLED)
+                        .setPermission(Aconfig.flag_permission.READ_WRITE))
+                .addParsedFlag(parsed_flag
+                    .newBuilder()
+                        .setPackage("com.android.another.flags")
+                        .setName("flag0")
+                        .setNamespace("test_another_namespace")
+                        .setDescription("test flag")
+                        .addBug("12345678")
+                        .setState(Aconfig.flag_state.DISABLED)
+                        .setPermission(Aconfig.flag_permission.READ_WRITE))
+                .build();
+
+        synchronized (mLock) {
+            settingsState.loadAconfigDefaultValues(
+                    flags.toByteArray(), settingsState.getAconfigDefaultValues());
+            List<String> updates =
+                    settingsState.setSettingsLocked("test_namespace/", keyValues, packageName);
+            assertEquals(3, updates.size());
+
+            SettingsState.Setting s;
+
+            s = settingsState.getSettingLocked("test_namespace/flag0");
+            assertEquals("true", s.getValue());
+
+            s = settingsState.getSettingLocked("test_namespace/flag1");
+            assertNull(s.getValue());
+
+            s = settingsState.getSettingLocked("test_namespace/flag2");
+            assertEquals("false", s.getValue());
+
+            s = settingsState.getSettingLocked("test_namespace/com.android.flags.flag3");
+            assertEquals("false", s.getValue());
+
+            s = settingsState.getSettingLocked(
+                    "test_another_namespace/com.android.another.flags.flag0");
+            assertEquals("false", s.getValue());
+        }
+    }
+
+    public void testsetSettingsLockedNoTrunkDefault() throws Exception {
+        final PrintStream os = new PrintStream(new FileOutputStream(mSettingsFile));
+        os.print(
+                "<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>"
+                        + "<settings version=\"120\">"
+                        + "  <setting id=\"0\" name=\"test_namespace/flag0\" "
+                            + "value=\"false\" package=\"com.android.flags\" />"
+                        + "  <setting id=\"1\" name=\"test_namespace/flag1\" "
+                            + "value=\"false\" package=\"com.android.flags\" />"
+                        + "</settings>");
+        os.close();
+
+        int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0);
+
+        SettingsState settingsState = new SettingsState(
+                getContext(), mLock, mSettingsFile, configKey,
+                SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
+
+        Map<String, String> keyValues =
+                Map.of("test_namespace/flag0", "true", "test_namespace/flag2", "false");
+        String packageName = "com.android.flags";
+
+        synchronized (mLock) {
+            List<String> updates =
+                    settingsState.setSettingsLocked("test_namespace/", keyValues, packageName);
+            assertEquals(3, updates.size());
+
+            SettingsState.Setting s;
+
+            s = settingsState.getSettingLocked("test_namespace/flag0");
+            assertEquals("true", s.getValue());
+
+            s = settingsState.getSettingLocked("test_namespace/flag1");
+            assertNull(s.getValue());
+
+            s = settingsState.getSettingLocked("test_namespace/flag2");
+            assertEquals("false", s.getValue());
+        }
+    }
 }
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 84ef6e5..926e181 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -917,6 +917,9 @@
     <!-- Permissions required for CTS test - GrammaticalInflectionManagerTest -->
     <uses-permission android:name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER" />
 
+    <!-- Permission required for CTS test - CtsPackageManagerTestCases-->
+    <uses-permission android:name="android.permission.DOMAIN_VERIFICATION_AGENT" />
+
     <application
         android:label="@string/app_label"
         android:theme="@android:style/Theme.DeviceDefault.DayNight"
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffect.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffect.kt
new file mode 100644
index 0000000..abe1e3d
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffect.kt
@@ -0,0 +1,377 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.surfaceeffects.loadingeffect
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.graphics.Paint
+import android.graphics.RenderEffect
+import android.view.View
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader
+
+/**
+ * Plays loading effect with the given configuration.
+ *
+ * @param baseType immutable base shader type. This is used for constructing the shader. Reconstruct
+ *   the [LoadingEffect] if the base type needs to be changed.
+ * @param config immutable parameters that are used for drawing the effect.
+ * @param paintCallback triggered every frame when animation is playing. Use this to draw the effect
+ *   with [Canvas.drawPaint].
+ * @param renderEffectCallback triggered every frame when animation is playing. Use this to draw the
+ *   effect with [RenderEffect].
+ * @param animationStateChangedCallback triggered when the [AnimationState] changes. Optional.
+ *
+ * The client is responsible to actually draw the [Paint] or [RenderEffect] returned in the
+ * callback. Note that [View.invalidate] must be called on each callback. There are a few ways to
+ * render the effect:
+ * 1) Use [Canvas.drawPaint]. (Preferred. Significantly cheaper!)
+ * 2) Set [RenderEffect] to the [View]. (Good for chaining effects.)
+ * 3) Use [RenderNode.setRenderEffect]. (This may be least preferred, as 2 should do what you want.)
+ *
+ * <p>First approach is more performant than other ones because [RenderEffect] forces an
+ * intermediate render pass of the View to a texture to feed into it.
+ *
+ * <p>If going with the first approach, your custom [View] would look like as follow:
+ * <pre>{@code
+ *     private var paint: Paint? = null
+ *     // Override [View.onDraw].
+ *     override fun onDraw(canvas: Canvas) {
+ *         // RuntimeShader requires hardwareAcceleration.
+ *         if (!canvas.isHardwareAccelerated) return
+ *
+ *         paint?.let { canvas.drawPaint(it) }
+ *     }
+ *
+ *     // This is called [Callback.onDraw]
+ *     fun draw(paint: Paint) {
+ *         this.paint = paint
+ *
+ *         // Must call invalidate to trigger View#onDraw
+ *         invalidate()
+ *     }
+ * }</pre>
+ *
+ * <p>If going with the second approach, it doesn't require an extra custom [View], and it is as
+ * simple as calling [View.setRenderEffect] followed by [View.invalidate]. You can also chain the
+ * effect with other [RenderEffect].
+ *
+ * <p>Third approach is an option, but it's more of a boilerplate so you would like to stick with
+ * the second option. If you want to go with this option for some reason, below is the example:
+ * <pre>{@code
+ *     // Initialize the shader and paint to use to pass into the [Canvas].
+ *     private val renderNode = RenderNode("LoadingEffect")
+ *
+ *     // Override [View.onDraw].
+ *     override fun onDraw(canvas: Canvas) {
+ *         // RuntimeShader requires hardwareAcceleration.
+ *         if (!canvas.isHardwareAccelerated) return
+ *
+ *         if (renderNode.hasDisplayList()) {
+ *             canvas.drawRenderNode(renderNode)
+ *         }
+ *     }
+ *
+ *     // This is called [Callback.onDraw]
+ *     fun draw(renderEffect: RenderEffect) {
+ *         renderNode.setPosition(0, 0, width, height)
+ *         renderNode.setRenderEffect(renderEffect)
+ *
+ *         val recordingCanvas = renderNode.beginRecording()
+ *         // We need at least 1 drawing instruction.
+ *         recordingCanvas.drawColor(Color.TRANSPARENT)
+ *         renderNode.endRecording()
+ *
+ *         // Must call invalidate to trigger View#onDraw
+ *         invalidate()
+ *     }
+ * }</pre>
+ */
+class LoadingEffect
+private constructor(
+    baseType: TurbulenceNoiseShader.Companion.Type,
+    private val config: TurbulenceNoiseAnimationConfig,
+    private val paintCallback: PaintDrawCallback?,
+    private val renderEffectCallback: RenderEffectDrawCallback?,
+    private val animationStateChangedCallback: AnimationStateChangedCallback? = null
+) {
+    constructor(
+        baseType: TurbulenceNoiseShader.Companion.Type,
+        config: TurbulenceNoiseAnimationConfig,
+        paintCallback: PaintDrawCallback,
+        animationStateChangedCallback: AnimationStateChangedCallback? = null
+    ) : this(
+        baseType,
+        config,
+        paintCallback,
+        renderEffectCallback = null,
+        animationStateChangedCallback
+    )
+    constructor(
+        baseType: TurbulenceNoiseShader.Companion.Type,
+        config: TurbulenceNoiseAnimationConfig,
+        renderEffectCallback: RenderEffectDrawCallback,
+        animationStateChangedCallback: AnimationStateChangedCallback? = null
+    ) : this(
+        baseType,
+        config,
+        paintCallback = null,
+        renderEffectCallback,
+        animationStateChangedCallback
+    )
+
+    private val turbulenceNoiseShader: TurbulenceNoiseShader =
+        TurbulenceNoiseShader(baseType).apply { applyConfig(config) }
+    private var currentAnimator: ValueAnimator? = null
+    private var state: AnimationState = AnimationState.NOT_PLAYING
+        set(value) {
+            if (field != value) {
+                animationStateChangedCallback?.onStateChanged(field, value)
+                field = value
+            }
+        }
+
+    // We create a paint instance only if the client renders it with Paint.
+    private val paint =
+        if (paintCallback != null) {
+            Paint().apply { this.shader = turbulenceNoiseShader }
+        } else {
+            null
+        }
+
+    /** Plays LoadingEffect. */
+    fun play() {
+        if (state != AnimationState.NOT_PLAYING) {
+            return // Ignore if any of the animation is playing.
+        }
+
+        playEaseIn()
+    }
+
+    // TODO(b/237282226): Support force finish.
+    /** Finishes the main animation, which triggers the ease-out animation. */
+    fun finish() {
+        if (state == AnimationState.MAIN) {
+            // Calling Animator#end sets the animation state back to the initial state. Using pause
+            // to avoid visual artifacts.
+            currentAnimator?.pause()
+            currentAnimator = null
+
+            playEaseOut()
+        }
+    }
+
+    /** Updates the noise color dynamically. */
+    fun updateColor(newColor: Int) {
+        turbulenceNoiseShader.setColor(newColor)
+    }
+
+    /**
+     * Retrieves the noise offset x, y, z values. This is useful for replaying the animation
+     * smoothly from the last animation, by passing in the last values to the next animation.
+     */
+    fun getNoiseOffset(): Array<Float> {
+        return arrayOf(
+            turbulenceNoiseShader.noiseOffsetX,
+            turbulenceNoiseShader.noiseOffsetY,
+            turbulenceNoiseShader.noiseOffsetZ
+        )
+    }
+
+    private fun playEaseIn() {
+        if (state != AnimationState.NOT_PLAYING) {
+            return
+        }
+        state = AnimationState.EASE_IN
+
+        val animator = ValueAnimator.ofFloat(0f, 1f)
+        animator.duration = config.easeInDuration.toLong()
+
+        // Animation should start from the initial position to avoid abrupt transition.
+        val initialX = turbulenceNoiseShader.noiseOffsetX
+        val initialY = turbulenceNoiseShader.noiseOffsetY
+        val initialZ = turbulenceNoiseShader.noiseOffsetZ
+
+        animator.addUpdateListener { updateListener ->
+            val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
+            val progress = updateListener.animatedValue as Float
+
+            turbulenceNoiseShader.setNoiseMove(
+                initialX + timeInSec * config.noiseMoveSpeedX,
+                initialY + timeInSec * config.noiseMoveSpeedY,
+                initialZ + timeInSec * config.noiseMoveSpeedZ
+            )
+
+            // TODO: Replace it with a better curve.
+            turbulenceNoiseShader.setOpacity(progress * config.luminosityMultiplier)
+
+            draw()
+        }
+
+        animator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    currentAnimator = null
+                    playMain()
+                }
+            }
+        )
+
+        animator.start()
+        this.currentAnimator = animator
+    }
+
+    private fun playMain() {
+        if (state != AnimationState.EASE_IN) {
+            return
+        }
+        state = AnimationState.MAIN
+
+        val animator = ValueAnimator.ofFloat(0f, 1f)
+        animator.duration = config.maxDuration.toLong()
+
+        // Animation should start from the initial position to avoid abrupt transition.
+        val initialX = turbulenceNoiseShader.noiseOffsetX
+        val initialY = turbulenceNoiseShader.noiseOffsetY
+        val initialZ = turbulenceNoiseShader.noiseOffsetZ
+
+        turbulenceNoiseShader.setOpacity(config.luminosityMultiplier)
+
+        animator.addUpdateListener { updateListener ->
+            val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
+            turbulenceNoiseShader.setNoiseMove(
+                initialX + timeInSec * config.noiseMoveSpeedX,
+                initialY + timeInSec * config.noiseMoveSpeedY,
+                initialZ + timeInSec * config.noiseMoveSpeedZ
+            )
+
+            draw()
+        }
+
+        animator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    currentAnimator = null
+                    playEaseOut()
+                }
+            }
+        )
+
+        animator.start()
+        this.currentAnimator = animator
+    }
+
+    private fun playEaseOut() {
+        if (state != AnimationState.MAIN) {
+            return
+        }
+        state = AnimationState.EASE_OUT
+
+        val animator = ValueAnimator.ofFloat(0f, 1f)
+        animator.duration = config.easeOutDuration.toLong()
+
+        // Animation should start from the initial position to avoid abrupt transition.
+        val initialX = turbulenceNoiseShader.noiseOffsetX
+        val initialY = turbulenceNoiseShader.noiseOffsetY
+        val initialZ = turbulenceNoiseShader.noiseOffsetZ
+
+        animator.addUpdateListener { updateListener ->
+            val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
+            val progress = updateListener.animatedValue as Float
+
+            turbulenceNoiseShader.setNoiseMove(
+                initialX + timeInSec * config.noiseMoveSpeedX,
+                initialY + timeInSec * config.noiseMoveSpeedY,
+                initialZ + timeInSec * config.noiseMoveSpeedZ
+            )
+
+            // TODO: Replace it with a better curve.
+            turbulenceNoiseShader.setOpacity((1f - progress) * config.luminosityMultiplier)
+
+            draw()
+        }
+
+        animator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    currentAnimator = null
+                    state = AnimationState.NOT_PLAYING
+                }
+            }
+        )
+
+        animator.start()
+        this.currentAnimator = animator
+    }
+
+    private fun draw() {
+        paintCallback?.onDraw(paint!!)
+        renderEffectCallback?.onDraw(
+            RenderEffect.createRuntimeShaderEffect(turbulenceNoiseShader, "in_src")
+        )
+    }
+
+    companion object {
+        /**
+         * States of the loading effect animation.
+         *
+         * <p>The state is designed to be follow the order below: [AnimationState.EASE_IN],
+         * [AnimationState.MAIN], [AnimationState.EASE_OUT]. Note that ease in and out don't
+         * necessarily mean the acceleration and deceleration in the animation curve. They simply
+         * mean each stage of the animation. (i.e. Intro, core, and rest)
+         */
+        enum class AnimationState {
+            EASE_IN,
+            MAIN,
+            EASE_OUT,
+            NOT_PLAYING
+        }
+
+        /** Client must implement one of the draw callbacks. */
+        interface PaintDrawCallback {
+            /**
+             * A callback with a [Paint] object that contains shader info, which is triggered every
+             * frame while animation is playing. Note that the [Paint] object here is always the
+             * same instance.
+             */
+            fun onDraw(loadingPaint: Paint)
+        }
+
+        interface RenderEffectDrawCallback {
+            /**
+             * A callback with a [RenderEffect] object that contains shader info, which is triggered
+             * every frame while animation is playing. Note that the [RenderEffect] instance is
+             * different each time to update shader uniforms.
+             */
+            fun onDraw(loadingRenderEffect: RenderEffect)
+        }
+
+        /** Optional callback that is triggered when the animation state changes. */
+        interface AnimationStateChangedCallback {
+            /**
+             * A callback that's triggered when the [AnimationState] changes. Example usage is
+             * performing a cleanup when [AnimationState] becomes [NOT_PLAYING].
+             */
+            fun onStateChanged(oldState: AnimationState, newState: AnimationState) {}
+        }
+
+        private const val MS_TO_SEC = 0.001f
+
+        private val TAG = LoadingEffect::class.java.simpleName
+    }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
index 30108ac..8dd90a8 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
@@ -30,6 +30,7 @@
     companion object {
         private const val UNIFORMS =
             """
+            uniform shader in_src; // Needed to support RenderEffect.
             uniform float in_gridNum;
             uniform vec3 in_noiseMove;
             uniform vec2 in_size;
@@ -114,6 +115,7 @@
         setSize(config.width, config.height)
         setLumaMatteFactors(config.lumaMatteBlendFactor, config.lumaMatteOverallBrightness)
         setInverseNoiseLuminosity(config.shouldInverseNoiseLuminosity)
+        setNoiseMove(config.noiseOffsetX, config.noiseOffsetY, config.noiseOffsetZ)
     }
 
     /** Sets the number of grid for generating noise. */
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
index 9a34d6f..36ab46b4 100644
--- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.bouncer.ui.BouncerDialogFactory
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.communal.widgets.WidgetConfigurator
 import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
 import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint
@@ -104,7 +105,7 @@
         throwComposeUnavailableError()
     }
 
-    override fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View {
+    override fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View {
         throwComposeUnavailableError()
     }
 
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
index 51d2a03..5b6aa09 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -40,6 +40,7 @@
 import com.android.systemui.communal.ui.compose.CommunalContainer
 import com.android.systemui.communal.ui.compose.CommunalHub
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.communal.widgets.WidgetConfigurator
 import com.android.systemui.keyboard.stickykeys.ui.view.createStickyKeyIndicatorView
 import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
@@ -161,7 +162,7 @@
         }
     }
 
-    override fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View {
+    override fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View {
         return ComposeView(context).apply {
             setContent { PlatformTheme { CommunalContainer(viewModel = viewModel) } }
         }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index 92bc1f1..bc85513 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -23,7 +23,9 @@
 import com.android.compose.animation.scene.updateSceneTransitionLayoutState
 import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
+import com.android.systemui.communal.ui.compose.extensions.allowGestures
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.transform
 
@@ -51,7 +53,7 @@
 @Composable
 fun CommunalContainer(
     modifier: Modifier = Modifier,
-    viewModel: BaseCommunalViewModel,
+    viewModel: CommunalViewModel,
 ) {
     val currentScene: SceneKey by
         viewModel.currentScene
@@ -63,6 +65,7 @@
             onChangeScene = { viewModel.onSceneChanged(it.toCommunalSceneKey()) },
             transitions = sceneTransitions,
         )
+    val touchesAllowed by viewModel.touchesAllowed.collectAsState(initial = false)
 
     // This effect exposes the SceneTransitionLayout's observable transition state to the rest of
     // the system, and unsets it when the view is disposed to avoid a memory leak.
@@ -75,7 +78,7 @@
 
     SceneTransitionLayout(
         state = sceneTransitionLayoutState,
-        modifier = modifier.fillMaxSize(),
+        modifier = modifier.fillMaxSize().allowGestures(allowed = touchesAllowed),
         swipeSourceDetector = FixedSizeEdgeDetector(ContainerDimensions.EdgeSwipeSize),
     ) {
         scene(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index f70b6a5..b299ca7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -45,6 +45,7 @@
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.media.controls.ui.MediaHierarchyManager
 import com.android.systemui.media.controls.ui.MediaHost
+import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
 import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
 import com.android.systemui.testKosmos
@@ -102,6 +103,7 @@
                 testScope,
                 kosmos.communalInteractor,
                 kosmos.communalTutorialInteractor,
+                kosmos.shadeInteractor,
                 mediaHost,
                 logcatLogBuffer("CommunalViewModelTest"),
             )
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index 40d2d16..febfd4c 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -28,6 +28,8 @@
 import com.android.systemui.media.controls.ui.MediaHost
 import com.android.systemui.media.controls.ui.MediaHostState
 import com.android.systemui.media.dagger.MediaModule
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.util.kotlin.BooleanFlowOperators.not
 import javax.inject.Inject
 import javax.inject.Named
 import kotlinx.coroutines.CoroutineScope
@@ -51,6 +53,7 @@
     @Application private val scope: CoroutineScope,
     private val communalInteractor: CommunalInteractor,
     tutorialInteractor: CommunalTutorialInteractor,
+    shadeInteractor: ShadeInteractor,
     @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
     @CommunalLog logBuffer: LogBuffer,
 ) : BaseCommunalViewModel(communalInteractor, mediaHost) {
@@ -81,6 +84,9 @@
     override val isPopupOnDismissCtaShowing: Flow<Boolean> =
         _isPopupOnDismissCtaShowing.asStateFlow()
 
+    /** Whether touches should be disabled in communal */
+    val touchesAllowed: Flow<Boolean> = not(shadeInteractor.isAnyFullyExpanded)
+
     init {
         // Initialize our media host for the UMO. This only needs to happen once and must be done
         // before the MediaHierarchyManager attempts to move the UMO to the hub.
@@ -114,6 +120,7 @@
     }
 
     private var delayedHidePopupJob: Job? = null
+
     private fun schedulePopupHiding() {
         cancelDelayedPopupHiding()
         delayedHidePopupJob =
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
index 9a4dfdd..4e23ecd9 100644
--- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.bouncer.ui.BouncerDialogFactory
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.communal.widgets.WidgetConfigurator
 import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
 import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint
@@ -116,7 +117,7 @@
     ): View
 
     /** Creates a container that hosts the communal UI and handles gesture transitions. */
-    fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View
+    fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View
 
     /** Creates a [View] that represents the Lockscreen. */
     fun createLockscreen(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index 1144efe..f95efaa 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -43,6 +43,10 @@
 import android.window.InputTransferToken
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
+import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
+import androidx.constraintlayout.widget.ConstraintSet.START
+import androidx.constraintlayout.widget.ConstraintSet.TOP
+import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT
 import androidx.core.view.isInvisible
 import com.android.keyguard.ClockEventController
 import com.android.keyguard.KeyguardClockSwitch
@@ -393,7 +397,7 @@
             ),
         )
 
-        setUpUdfps(previewContext, rootView)
+        setUpUdfps(previewContext, if (migrateClocksToBlueprint()) keyguardRootView else rootView)
 
         if (keyguardBottomAreaRefactor()) {
             setupShortcuts(keyguardRootView)
@@ -468,15 +472,6 @@
             return
         }
 
-        // Place the UDFPS view in the proper sensor location
-        val fingerprintLayoutParams =
-            FrameLayout.LayoutParams(sensorBounds.width(), sensorBounds.height())
-        fingerprintLayoutParams.setMarginsRelative(
-            sensorBounds.left,
-            sensorBounds.top,
-            sensorBounds.right,
-            sensorBounds.bottom
-        )
         val finger =
             LayoutInflater.from(previewContext)
                 .inflate(
@@ -484,7 +479,31 @@
                     parentView,
                     false,
                 ) as View
-        parentView.addView(finger, fingerprintLayoutParams)
+
+        // Place the UDFPS view in the proper sensor location
+        if (migrateClocksToBlueprint()) {
+            finger.id = R.id.lock_icon_view
+            parentView.addView(finger)
+            val cs = ConstraintSet()
+            cs.clone(parentView as ConstraintLayout)
+            cs.apply {
+                constrainWidth(R.id.lock_icon_view, sensorBounds.width())
+                constrainHeight(R.id.lock_icon_view, sensorBounds.height())
+                connect(R.id.lock_icon_view, TOP, PARENT_ID, TOP, sensorBounds.top)
+                connect(R.id.lock_icon_view, START, PARENT_ID, START, sensorBounds.left)
+            }
+            cs.applyTo(parentView)
+        } else {
+            val fingerprintLayoutParams =
+                FrameLayout.LayoutParams(sensorBounds.width(), sensorBounds.height())
+            fingerprintLayoutParams.setMarginsRelative(
+                sensorBounds.left,
+                sensorBounds.top,
+                sensorBounds.right,
+                sensorBounds.bottom
+            )
+            parentView.addView(finger, fingerprintLayoutParams)
+        }
     }
 
     private fun setUpClock(previewContext: Context, parentView: ViewGroup) {
diff --git a/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt b/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt
index 98c0217..2f0fc51 100644
--- a/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt
@@ -23,7 +23,6 @@
 import android.content.DialogInterface.BUTTON_POSITIVE
 import android.content.Intent
 import android.content.Intent.EXTRA_PACKAGE_NAME
-import android.content.pm.PackageManager
 import android.hardware.SensorPrivacyManager
 import android.hardware.SensorPrivacyManager.EXTRA_ALL_SENSORS
 import android.hardware.SensorPrivacyManager.EXTRA_SENSOR
@@ -32,7 +31,6 @@
 import android.os.Handler
 import android.window.OnBackInvokedDispatcher
 import androidx.annotation.OpenForTesting
-import com.android.internal.camera.flags.Flags
 import com.android.internal.util.FrameworkStatsLog.PRIVACY_TOGGLE_DIALOG_INTERACTION
 import com.android.internal.util.FrameworkStatsLog.PRIVACY_TOGGLE_DIALOG_INTERACTION__ACTION__CANCEL
 import com.android.internal.util.FrameworkStatsLog.PRIVACY_TOGGLE_DIALOG_INTERACTION__ACTION__ENABLE
@@ -92,14 +90,14 @@
             sensor = ALL_SENSORS
             val callback = IndividualSensorPrivacyController.Callback { _, _ ->
                 if (!sensorPrivacyController.isSensorBlocked(MICROPHONE) &&
-                        !isCameraBlocked(sensorUsePackageName)) {
+                        !sensorPrivacyController.isSensorBlocked(CAMERA)) {
                     finish()
                 }
             }
             sensorPrivacyListener = callback
             sensorPrivacyController.addCallback(callback)
             if (!sensorPrivacyController.isSensorBlocked(MICROPHONE) &&
-                    !isCameraBlocked(sensorUsePackageName)) {
+                    !sensorPrivacyController.isSensorBlocked(CAMERA)) {
                 finish()
                 return
             }
@@ -112,22 +110,14 @@
             }
             val callback = IndividualSensorPrivacyController.Callback {
                 whichSensor: Int, isBlocked: Boolean ->
-                if (whichSensor != sensor) {
-                    // Ignore a callback; we're not interested in.
-                } else if ((whichSensor == CAMERA) && !isCameraBlocked(sensorUsePackageName)) {
-                    finish()
-                } else if ((whichSensor == MICROPHONE) && !isBlocked) {
+                if (whichSensor == sensor && !isBlocked) {
                     finish()
                 }
             }
             sensorPrivacyListener = callback
             sensorPrivacyController.addCallback(callback)
 
-            if ((sensor == CAMERA) && !isCameraBlocked(sensorUsePackageName)) {
-                finish()
-                return
-            } else if ((sensor == MICROPHONE) &&
-                    !sensorPrivacyController.isSensorBlocked(MICROPHONE)) {
+            if (!sensorPrivacyController.isSensorBlocked(sensor)) {
                 finish()
                 return
             }
@@ -214,22 +204,6 @@
         recreate()
     }
 
-    private fun isAutomotive(): Boolean {
-        return getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
-    }
-
-    private fun isCameraBlocked(packageName: String): Boolean {
-        if (Flags.privacyAllowlist()) {
-            if (isAutomotive()) {
-                return sensorPrivacyController.isCameraPrivacyEnabled(packageName)
-            } else {
-                return sensorPrivacyController.isSensorBlocked(CAMERA)
-            }
-        } else {
-            return sensorPrivacyController.isSensorBlocked(CAMERA)
-        }
-    }
-
     private fun disableSensorPrivacy() {
         if (sensor == ALL_SENSORS) {
             sensorPrivacyController.setSensorBlocked(DIALOG, MICROPHONE, false)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyController.java
index fb67358..eb08f37 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyController.java
@@ -16,12 +16,9 @@
 
 package com.android.systemui.statusbar.policy;
 
-import android.annotation.FlaggedApi;
 import android.hardware.SensorPrivacyManager.Sensors.Sensor;
 import android.hardware.SensorPrivacyManager.Sources.Source;
 
-import com.android.internal.camera.flags.Flags;
-
 public interface IndividualSensorPrivacyController extends
         CallbackController<IndividualSensorPrivacyController.Callback> {
     void init();
@@ -45,12 +42,6 @@
      */
     boolean requiresAuthentication();
 
-    /**
-     * @return whether camera privacy is enabled for the package.
-     */
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    boolean isCameraPrivacyEnabled(String packageName);
-
     interface Callback {
         void onSensorBlockedChanged(@Sensor int sensor, boolean blocked);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
index 8f768e9..87dfc99 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
@@ -19,9 +19,6 @@
 import static android.hardware.SensorPrivacyManager.Sensors.CAMERA;
 import static android.hardware.SensorPrivacyManager.Sensors.MICROPHONE;
 
-import android.Manifest;
-import android.annotation.FlaggedApi;
-import android.annotation.RequiresPermission;
 import android.hardware.SensorPrivacyManager;
 import android.hardware.SensorPrivacyManager.Sensors.Sensor;
 import android.hardware.SensorPrivacyManager.Sources.Source;
@@ -31,8 +28,6 @@
 
 import androidx.annotation.NonNull;
 
-import com.android.internal.camera.flags.Flags;
-
 import java.util.Set;
 
 public class IndividualSensorPrivacyControllerImpl implements IndividualSensorPrivacyController {
@@ -107,13 +102,6 @@
     }
 
     @Override
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
-    public boolean isCameraPrivacyEnabled(String packageName) {
-        return mSensorPrivacyManager.isCameraPrivacyEnabled(packageName);
-    }
-
-    @Override
     public void addCallback(@NonNull Callback listener) {
         mCallbacks.add(listener);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectTest.kt
new file mode 100644
index 0000000..7c36a85
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectTest.kt
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.surfaceeffects.loadingeffect
+
+import android.graphics.Paint
+import android.graphics.RenderEffect
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.model.SysUiStateTest
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState.EASE_IN
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState.EASE_OUT
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState.MAIN
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState.NOT_PLAYING
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationStateChangedCallback
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.PaintDrawCallback
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.RenderEffectDrawCallback
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class LoadingEffectTest : SysUiStateTest() {
+
+    private val fakeSystemClock = FakeSystemClock()
+    private val fakeExecutor = FakeExecutor(fakeSystemClock)
+
+    @Test
+    fun play_paintCallback_triggersDrawCallback() {
+        var paintFromCallback: Paint? = null
+        val drawCallback =
+            object : PaintDrawCallback {
+                override fun onDraw(loadingPaint: Paint) {
+                    paintFromCallback = loadingPaint
+                }
+            }
+        val loadingEffect =
+            LoadingEffect(
+                baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+                TurbulenceNoiseAnimationConfig(),
+                paintCallback = drawCallback,
+                animationStateChangedCallback = null
+            )
+
+        fakeExecutor.execute {
+            assertThat(paintFromCallback).isNull()
+
+            loadingEffect.play()
+            fakeSystemClock.advanceTime(500L)
+
+            assertThat(paintFromCallback).isNotNull()
+        }
+    }
+
+    @Test
+    fun play_renderEffectCallback_triggersDrawCallback() {
+        var renderEffectFromCallback: RenderEffect? = null
+        val drawCallback =
+            object : RenderEffectDrawCallback {
+                override fun onDraw(loadingRenderEffect: RenderEffect) {
+                    renderEffectFromCallback = loadingRenderEffect
+                }
+            }
+        val loadingEffect =
+            LoadingEffect(
+                baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+                TurbulenceNoiseAnimationConfig(),
+                renderEffectCallback = drawCallback,
+                animationStateChangedCallback = null
+            )
+
+        fakeExecutor.execute {
+            assertThat(renderEffectFromCallback).isNull()
+
+            loadingEffect.play()
+            fakeSystemClock.advanceTime(500L)
+
+            assertThat(renderEffectFromCallback).isNotNull()
+        }
+    }
+
+    @Test
+    fun play_animationStateChangesInOrder() {
+        val config = TurbulenceNoiseAnimationConfig()
+        val expectedStates = arrayOf(NOT_PLAYING, EASE_IN, MAIN, EASE_OUT, NOT_PLAYING)
+        val actualStates = mutableListOf(NOT_PLAYING)
+        val stateChangedCallback =
+            object : AnimationStateChangedCallback {
+                override fun onStateChanged(oldState: AnimationState, newState: AnimationState) {
+                    actualStates.add(newState)
+                }
+            }
+        val drawCallback =
+            object : PaintDrawCallback {
+                override fun onDraw(loadingPaint: Paint) {}
+            }
+        val loadingEffect =
+            LoadingEffect(
+                baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+                config,
+                paintCallback = drawCallback,
+                stateChangedCallback
+            )
+
+        val timeToAdvance =
+            config.easeInDuration + config.maxDuration + config.easeOutDuration + 100
+
+        fakeExecutor.execute {
+            loadingEffect.play()
+
+            fakeSystemClock.advanceTime(timeToAdvance.toLong())
+
+            assertThat(actualStates).isEqualTo(expectedStates)
+        }
+    }
+
+    @Test
+    fun play_alreadyPlaying_playsOnlyOnce() {
+        val config = TurbulenceNoiseAnimationConfig()
+        var numPlay = 0
+        val stateChangedCallback =
+            object : AnimationStateChangedCallback {
+                override fun onStateChanged(oldState: AnimationState, newState: AnimationState) {
+                    if (oldState == NOT_PLAYING && newState == EASE_IN) {
+                        numPlay++
+                    }
+                }
+            }
+        val drawCallback =
+            object : PaintDrawCallback {
+                override fun onDraw(loadingPaint: Paint) {}
+            }
+        val loadingEffect =
+            LoadingEffect(
+                baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+                config,
+                paintCallback = drawCallback,
+                stateChangedCallback
+            )
+
+        fakeExecutor.execute {
+            assertThat(numPlay).isEqualTo(0)
+
+            loadingEffect.play()
+            loadingEffect.play()
+            loadingEffect.play()
+            loadingEffect.play()
+            loadingEffect.play()
+
+            assertThat(numPlay).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun finish_finishesLoadingEffect() {
+        val config = TurbulenceNoiseAnimationConfig(maxDuration = 1000f)
+        val drawCallback =
+            object : PaintDrawCallback {
+                override fun onDraw(loadingPaint: Paint) {}
+            }
+        var isFinished = false
+        val stateChangedCallback =
+            object : AnimationStateChangedCallback {
+                override fun onStateChanged(oldState: AnimationState, newState: AnimationState) {
+                    if (oldState == MAIN && newState == NOT_PLAYING) {
+                        isFinished = true
+                    }
+                }
+            }
+        val loadingEffect =
+            LoadingEffect(
+                baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+                config,
+                paintCallback = drawCallback,
+                stateChangedCallback
+            )
+
+        fakeExecutor.execute {
+            assertThat(isFinished).isFalse()
+
+            loadingEffect.play()
+            fakeSystemClock.advanceTime(config.easeInDuration.toLong() + 500L)
+
+            assertThat(isFinished).isFalse()
+
+            loadingEffect.finish()
+
+            assertThat(isFinished).isTrue()
+        }
+    }
+
+    @Test
+    fun finish_notMainState_hasNoEffect() {
+        val config = TurbulenceNoiseAnimationConfig(maxDuration = 1000f)
+        val drawCallback =
+            object : PaintDrawCallback {
+                override fun onDraw(loadingPaint: Paint) {}
+            }
+        var isFinished = false
+        val stateChangedCallback =
+            object : AnimationStateChangedCallback {
+                override fun onStateChanged(oldState: AnimationState, newState: AnimationState) {
+                    if (oldState == MAIN && newState == NOT_PLAYING) {
+                        isFinished = true
+                    }
+                }
+            }
+        val loadingEffect =
+            LoadingEffect(
+                baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+                config,
+                paintCallback = drawCallback,
+                stateChangedCallback
+            )
+
+        fakeExecutor.execute {
+            assertThat(isFinished).isFalse()
+
+            loadingEffect.finish()
+
+            assertThat(isFinished).isFalse()
+        }
+    }
+
+    @Test
+    fun getNoiseOffset_returnsNoiseOffset() {
+        val expectedNoiseOffset = arrayOf(0.1f, 0.2f, 0.3f)
+        val config =
+            TurbulenceNoiseAnimationConfig(
+                noiseOffsetX = expectedNoiseOffset[0],
+                noiseOffsetY = expectedNoiseOffset[1],
+                noiseOffsetZ = expectedNoiseOffset[2]
+            )
+        val drawCallback =
+            object : PaintDrawCallback {
+                override fun onDraw(loadingPaint: Paint) {}
+            }
+        val loadingEffect =
+            LoadingEffect(
+                baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+                config,
+                paintCallback = drawCallback,
+                animationStateChangedCallback = null
+            )
+
+        assertThat(loadingEffect.getNoiseOffset()).isEqualTo(expectedNoiseOffset)
+    }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index b64c74e..af47ed2 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -42,10 +42,12 @@
 import android.accessibilityservice.AccessibilityTrace;
 import android.accessibilityservice.IAccessibilityServiceClient;
 import android.accessibilityservice.IAccessibilityServiceConnection;
+import android.accessibilityservice.IBrailleDisplayController;
 import android.accessibilityservice.MagnificationConfig;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.app.PendingIntent;
 import android.content.ComponentName;
 import android.content.Context;
@@ -58,6 +60,7 @@
 import android.hardware.HardwareBuffer;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManagerInternal;
+import android.hardware.usb.UsbDevice;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
@@ -2776,4 +2779,23 @@
         t.close();
         mOverlays.clear();
     }
+
+    @Override
+    @SuppressLint("AndroidFrameworkRequiresPermission") // Unsupported in Abstract class
+    public void connectBluetoothBrailleDisplay(String bluetoothAddress,
+            IBrailleDisplayController controller) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void connectUsbBrailleDisplay(UsbDevice usbDevice,
+            IBrailleDisplayController controller) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    @SuppressLint("AndroidFrameworkRequiresPermission") // Unsupported in Abstract class
+    public void setTestBrailleDisplayData(List<Bundle> brailleDisplays) {
+        throw new UnsupportedOperationException();
+    }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
index 5ebe161..b90a66a 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
@@ -26,16 +26,25 @@
 import android.accessibilityservice.AccessibilityService;
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.accessibilityservice.AccessibilityTrace;
+import android.accessibilityservice.BrailleDisplayController;
 import android.accessibilityservice.IAccessibilityServiceClient;
+import android.accessibilityservice.IBrailleDisplayController;
 import android.accessibilityservice.TouchInteractionController;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
 import android.app.PendingIntent;
+import android.bluetooth.BluetoothAdapter;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ParceledListSlice;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
@@ -44,6 +53,7 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.text.TextUtils;
 import android.util.Slog;
 import android.view.Display;
 import android.view.MotionEvent;
@@ -55,6 +65,8 @@
 import com.android.server.wm.WindowManagerInternal;
 
 import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -82,6 +94,9 @@
     final Intent mIntent;
     final ActivityTaskManagerInternal mActivityTaskManagerService;
 
+    private BrailleDisplayConnection mBrailleDisplayConnection;
+    private List<Bundle> mTestBrailleDisplays = null;
+
     private final Handler mMainHandler;
 
     private static final class AccessibilityInputMethodSessionCallback
@@ -448,6 +463,16 @@
         }
     }
 
+    @Override
+    public void resetLocked() {
+        super.resetLocked();
+        if (android.view.accessibility.Flags.brailleDisplayHid()) {
+            if (mBrailleDisplayConnection != null) {
+                mBrailleDisplayConnection.disconnect();
+            }
+        }
+    }
+
     public boolean isAccessibilityButtonAvailableLocked(AccessibilityUserState userState) {
         // If the service does not request the accessibility button, it isn't available
         if (!mRequestAccessibilityButton) {
@@ -640,4 +665,123 @@
             }
         }
     }
+
+    private void checkAccessibilityAccessLocked() {
+        if (!hasRightsToCurrentUserLocked()
+                || !mSecurityPolicy.checkAccessibilityAccess(this)) {
+            throw new SecurityException("Caller does not have accessibility access");
+        }
+    }
+
+    /**
+     * Sets up a BrailleDisplayConnection interface for the requested Bluetooth-connected
+     * Braille display.
+     *
+     * @param bluetoothAddress The address from
+     *                         {@link android.bluetooth.BluetoothDevice#getAddress()}.
+     */
+    @Override
+    @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+    public void connectBluetoothBrailleDisplay(
+            @NonNull String bluetoothAddress, @NonNull IBrailleDisplayController controller) {
+        if (!android.view.accessibility.Flags.brailleDisplayHid()) {
+            throw new IllegalStateException("Flag BRAILLE_DISPLAY_HID not enabled");
+        }
+        Objects.requireNonNull(bluetoothAddress);
+        Objects.requireNonNull(controller);
+        mContext.enforceCallingPermission(Manifest.permission.BLUETOOTH_CONNECT,
+                "Missing BLUETOOTH_CONNECT permission");
+        if (!BluetoothAdapter.checkBluetoothAddress(bluetoothAddress)) {
+            throw new IllegalArgumentException(
+                    bluetoothAddress + " is not a valid Bluetooth address");
+        }
+        synchronized (mLock) {
+            checkAccessibilityAccessLocked();
+            if (mBrailleDisplayConnection != null) {
+                throw new IllegalStateException(
+                        "This service already has a connected Braille display");
+            }
+            BrailleDisplayConnection connection = new BrailleDisplayConnection(mLock, this);
+            if (mTestBrailleDisplays != null) {
+                connection.setTestData(mTestBrailleDisplays);
+            }
+            connection.connectLocked(
+                    bluetoothAddress, BrailleDisplayConnection.BUS_BLUETOOTH, controller);
+        }
+    }
+
+    /**
+     * Sets up a BrailleDisplayConnection interface for the requested USB-connected
+     * Braille display.
+     *
+     * <p>The caller package must already have USB permission for this {@link UsbDevice}.
+     */
+    @SuppressLint("MissingPermission") // system_server has the required MANAGE_USB permission
+    @Override
+    @NonNull
+    public void connectUsbBrailleDisplay(@NonNull UsbDevice usbDevice,
+            @NonNull IBrailleDisplayController controller) {
+        if (!android.view.accessibility.Flags.brailleDisplayHid()) {
+            throw new IllegalStateException("Flag BRAILLE_DISPLAY_HID not enabled");
+        }
+        Objects.requireNonNull(usbDevice);
+        Objects.requireNonNull(controller);
+        final UsbManager usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE);
+        final String usbSerialNumber;
+        final int uid = Binder.getCallingUid();
+        final int pid = Binder.getCallingPid();
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            if (usbManager == null || !usbManager.hasPermission(
+                    usbDevice, mComponentName.getPackageName(), /*pid=*/ pid, /*uid=*/ uid)) {
+                throw new SecurityException(
+                        "Caller does not have permission to access this UsbDevice");
+            }
+            usbSerialNumber = usbDevice.getSerialNumber();
+            if (TextUtils.isEmpty(usbSerialNumber)) {
+                // If the UsbDevice does not report a serial number for locating the HIDRAW
+                // node then notify connection error ERROR_BRAILLE_DISPLAY_NOT_FOUND.
+                try {
+                    controller.onConnectionFailed(BrailleDisplayController.BrailleDisplayCallback
+                            .FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND);
+                } catch (RemoteException e) {
+                    Slog.e(LOG_TAG, "Error calling onConnectionFailed", e);
+                }
+                return;
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+        synchronized (mLock) {
+            checkAccessibilityAccessLocked();
+            if (mBrailleDisplayConnection != null) {
+                throw new IllegalStateException(
+                        "This service already has a connected Braille display");
+            }
+            BrailleDisplayConnection connection = new BrailleDisplayConnection(mLock, this);
+            if (mTestBrailleDisplays != null) {
+                connection.setTestData(mTestBrailleDisplays);
+            }
+            connection.connectLocked(
+                    usbSerialNumber, BrailleDisplayConnection.BUS_USB, controller);
+        }
+    }
+
+    @Override
+    @RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY)
+    public void setTestBrailleDisplayData(List<Bundle> brailleDisplays) {
+        // Enforce that this TestApi is only called by trusted (test) callers.
+        mContext.enforceCallingPermission(Manifest.permission.MANAGE_ACCESSIBILITY,
+                "Missing MANAGE_ACCESSIBILITY permission");
+        mTestBrailleDisplays = brailleDisplays;
+    }
+
+    void onBrailleDisplayConnectedLocked(BrailleDisplayConnection connection) {
+        mBrailleDisplayConnection = connection;
+    }
+
+    // Reset state when the BrailleDisplayConnection object disconnects itself.
+    void onBrailleDisplayDisconnectedLocked() {
+        mBrailleDisplayConnection = null;
+    }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java
new file mode 100644
index 0000000..1f18e15
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java
@@ -0,0 +1,534 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility;
+
+import static android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback.FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND;
+import static android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback.FLAG_ERROR_CANNOT_ACCESS;
+
+import android.accessibilityservice.BrailleDisplayController;
+import android.accessibilityservice.IBrailleDisplayConnection;
+import android.accessibilityservice.IBrailleDisplayController;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.PermissionManuallyEnforced;
+import android.annotation.RequiresNoPermission;
+import android.bluetooth.BluetoothDevice;
+import android.hardware.usb.UsbDevice;
+import android.os.Bundle;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Pair;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * This class represents the connection between {@code system_server} and a connected
+ * Braille Display using the Braille Display HID standard (usage page 0x41).
+ */
+class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub {
+    private static final String LOG_TAG = "BrailleDisplayConnection";
+
+    /**
+     * Represents the connection type of a Braille display.
+     *
+     * <p>The integer values must match the kernel's bus type values because this bus type is
+     * used to locate the correct HIDRAW node using data from the kernel. These values come
+     * from the UAPI header file bionic/libc/kernel/uapi/linux/input.h, which is guaranteed
+     * to stay constant.
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = true, prefix = {"BUS_"}, value = {
+            BUS_UNKNOWN,
+            BUS_USB,
+            BUS_BLUETOOTH
+    })
+    @interface BusType {
+    }
+    static final int BUS_UNKNOWN = -1;
+    static final int BUS_USB = 0x03;
+    static final int BUS_BLUETOOTH = 0x05;
+
+    // Access to this static object must be guarded by a lock that is shared for all instances
+    // of this class: the singular Accessibility system_server lock (mLock).
+    private static final Set<File> sConnectedNodes = new ArraySet<>();
+
+    // Used to guard to AIDL methods from concurrent calls.
+    // Lock must match the one used by AccessibilityServiceConnection, which itself
+    // comes from AccessibilityManagerService.
+    private final Object mLock;
+    private final AccessibilityServiceConnection mServiceConnection;
+
+
+    private File mHidrawNode;
+    private IBrailleDisplayController mController;
+
+    private Thread mInputThread;
+    private OutputStream mOutputStream;
+    private HandlerThread mOutputThread;
+
+    // mScanner is not final because tests may modify this to use a test-only scanner.
+    private BrailleDisplayScanner mScanner;
+
+    BrailleDisplayConnection(@NonNull Object lock,
+            @NonNull AccessibilityServiceConnection serviceConnection) {
+        this.mLock = Objects.requireNonNull(lock);
+        this.mScanner = getDefaultNativeScanner(getDefaultNativeInterface());
+        this.mServiceConnection = Objects.requireNonNull(serviceConnection);
+    }
+
+    /**
+     * Interface to scan for properties of connected Braille displays.
+     *
+     * <p>Helps simplify testing Braille Display APIs using test data without requiring
+     * a real Braille display to be connected to the device, by using a test implementation
+     * of this interface.
+     *
+     * @see #getDefaultNativeScanner
+     * @see #setTestData
+     */
+    @VisibleForTesting
+    interface BrailleDisplayScanner {
+        Collection<Path> getHidrawNodePaths();
+
+        byte[] getDeviceReportDescriptor(@NonNull Path path);
+
+        String getUniqueId(@NonNull Path path);
+
+        @BusType
+        int getDeviceBusType(@NonNull Path path);
+    }
+
+    /**
+     * Finds the Braille display HIDRAW node associated with the provided unique ID.
+     *
+     * <p>If found, saves instance state for this connection and starts a thread to
+     * read from the Braille display.
+     *
+     * @param expectedUniqueId  The expected unique ID of the device to connect, from
+     *                          {@link UsbDevice#getSerialNumber()}
+     *                          or {@link BluetoothDevice#getAddress()}
+     * @param expectedBusType   The expected bus type from {@link BusType}.
+     * @param controller        Interface containing oneway callbacks used to communicate with the
+     *                          {@link android.accessibilityservice.BrailleDisplayController}.
+     */
+    void connectLocked(
+            @NonNull String expectedUniqueId,
+            @BusType int expectedBusType,
+            @NonNull IBrailleDisplayController controller) {
+        Objects.requireNonNull(expectedUniqueId);
+        this.mController = Objects.requireNonNull(controller);
+
+        final List<Pair<File, byte[]>> result = new ArrayList<>();
+        final Collection<Path> hidrawNodePaths = mScanner.getHidrawNodePaths();
+        if (hidrawNodePaths == null) {
+            Slog.w(LOG_TAG, "Unable to access the HIDRAW node directory");
+            sendConnectionErrorLocked(FLAG_ERROR_CANNOT_ACCESS);
+            return;
+        }
+        boolean unableToGetDescriptor = false;
+        // For every present HIDRAW device node:
+        for (Path path : hidrawNodePaths) {
+            final byte[] descriptor = mScanner.getDeviceReportDescriptor(path);
+            if (descriptor == null) {
+                unableToGetDescriptor = true;
+                continue;
+            }
+            final String uniqueId = mScanner.getUniqueId(path);
+            if (isBrailleDisplay(descriptor)
+                    && mScanner.getDeviceBusType(path) == expectedBusType
+                    && expectedUniqueId.equalsIgnoreCase(uniqueId)) {
+                result.add(Pair.create(path.toFile(), descriptor));
+            }
+        }
+
+        // Return success only when exactly one matching device node is found.
+        if (result.size() != 1) {
+            @BrailleDisplayController.BrailleDisplayCallback.ErrorCode int errorCode =
+                    FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND;
+            // If we were unable to get some /dev/hidraw* descriptor then tell the accessibility
+            // service that the device may not have proper access to these device nodes.
+            if (unableToGetDescriptor) {
+                Slog.w(LOG_TAG, "Unable to access some HIDRAW node's descriptor");
+                errorCode |= FLAG_ERROR_CANNOT_ACCESS;
+            } else {
+                Slog.w(LOG_TAG,
+                        "Unable to find a unique Braille display matching the provided device");
+            }
+            sendConnectionErrorLocked(errorCode);
+            return;
+        }
+
+        this.mHidrawNode = result.get(0).first;
+        final byte[] reportDescriptor = result.get(0).second;
+
+        // Only one connection instance should exist for this hidraw node, across
+        // all currently running accessibility services.
+        if (sConnectedNodes.contains(this.mHidrawNode)) {
+            Slog.w(LOG_TAG,
+                    "Unable to find an unused Braille display matching the provided device");
+            sendConnectionErrorLocked(FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND);
+            return;
+        }
+        sConnectedNodes.add(this.mHidrawNode);
+
+        startReadingLocked();
+
+        try {
+            mServiceConnection.onBrailleDisplayConnectedLocked(this);
+            mController.onConnected(this, reportDescriptor);
+        } catch (RemoteException e) {
+            Slog.e(LOG_TAG, "Error calling onConnected", e);
+            disconnect();
+        }
+    }
+
+    private void sendConnectionErrorLocked(
+            @BrailleDisplayController.BrailleDisplayCallback.ErrorCode int errorCode) {
+        try {
+            mController.onConnectionFailed(errorCode);
+        } catch (RemoteException e) {
+            Slog.e(LOG_TAG, "Error calling onConnectionFailed", e);
+        }
+    }
+
+    /** Returns true if this descriptor includes usages for the Braille display usage page 0x41. */
+    private static boolean isBrailleDisplay(byte[] descriptor) {
+        // TODO: b/316036493 - Check that descriptor includes 0x41 reports.
+        return true;
+    }
+
+    /**
+     * Checks that the AccessibilityService that owns this BrailleDisplayConnection
+     * is still connected to the system.
+     *
+     * @throws IllegalStateException if not connected
+     */
+    private void assertServiceIsConnectedLocked() {
+        if (!mServiceConnection.isConnectedLocked()) {
+            throw new IllegalStateException("Accessibility service is not connected");
+        }
+    }
+
+    /**
+     * Disconnects from this Braille display. This object is no longer valid after
+     * this call returns.
+     */
+    @Override
+    // This is a cleanup method, so allow the call even if the calling service was disabled.
+    @RequiresNoPermission
+    public void disconnect() {
+        synchronized (mLock) {
+            closeInputLocked();
+            closeOutputLocked();
+            mServiceConnection.onBrailleDisplayDisconnectedLocked();
+            try {
+                mController.onDisconnected();
+            } catch (RemoteException e) {
+                Slog.e(LOG_TAG, "Error calling onDisconnected");
+            }
+            sConnectedNodes.remove(this.mHidrawNode);
+        }
+    }
+
+    /**
+     * Writes the provided HID bytes to this Braille display.
+     *
+     * <p>Writes are posted to a background thread handler.
+     *
+     * @param buffer The bytes to write to the Braille display. These bytes should be formatted
+     *               according to the report descriptor.
+     */
+    @Override
+    @PermissionManuallyEnforced // by assertServiceIsConnectedLocked()
+    public void write(@NonNull byte[] buffer) {
+        Objects.requireNonNull(buffer);
+        if (buffer.length > IBinder.getSuggestedMaxIpcSizeBytes()) {
+            Slog.e(LOG_TAG, "Requested write of size " + buffer.length
+                    + " which is larger than maximum " + IBinder.getSuggestedMaxIpcSizeBytes());
+            return;
+        }
+        synchronized (mLock) {
+            assertServiceIsConnectedLocked();
+            if (mOutputThread == null) {
+                try {
+                    mOutputStream = new FileOutputStream(mHidrawNode);
+                } catch (FileNotFoundException e) {
+                    Slog.e(LOG_TAG, "Unable to create write stream", e);
+                    disconnect();
+                    return;
+                }
+                mOutputThread = new HandlerThread("BrailleDisplayConnection output thread",
+                        Process.THREAD_PRIORITY_BACKGROUND);
+                mOutputThread.setDaemon(true);
+                mOutputThread.start();
+            }
+            // TODO: b/316035785 - Proactively disconnect a misbehaving Braille display by calling
+            //  disconnect() if the mOutputThread handler queue grows too large.
+            mOutputThread.getThreadHandler().post(() -> {
+                try {
+                    mOutputStream.write(buffer);
+                } catch (IOException e) {
+                    Slog.d(LOG_TAG, "Error writing to connected Braille display", e);
+                    disconnect();
+                }
+            });
+        }
+    }
+
+    /**
+     * Starts reading HID bytes from this Braille display.
+     *
+     * <p>Reads are performed on a background thread.
+     */
+    private void startReadingLocked() {
+        mInputThread = new Thread(() -> {
+            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+            try (InputStream inputStream = new FileInputStream(mHidrawNode)) {
+                final byte[] buffer = new byte[IBinder.getSuggestedMaxIpcSizeBytes()];
+                int readSize;
+                while (!Thread.interrupted()) {
+                    if (!mHidrawNode.exists()) {
+                        disconnect();
+                        break;
+                    }
+                    // Reading from the HIDRAW character device node will block
+                    // until bytes are available.
+                    readSize = inputStream.read(buffer);
+                    if (readSize > 0) {
+                        try {
+                            // Send the input to the AccessibilityService.
+                            mController.onInput(Arrays.copyOfRange(buffer, 0, readSize));
+                        } catch (RemoteException e) {
+                            // Error communicating with the AccessibilityService.
+                            Slog.e(LOG_TAG, "Error calling onInput", e);
+                            disconnect();
+                            break;
+                        }
+                    }
+                }
+            } catch (IOException e) {
+                Slog.d(LOG_TAG, "Error reading from connected Braille display", e);
+                disconnect();
+            }
+        }, "BrailleDisplayConnection input thread");
+        mInputThread.setDaemon(true);
+        mInputThread.start();
+    }
+
+    /** Stop the Input thread. */
+    private void closeInputLocked() {
+        if (mInputThread != null) {
+            mInputThread.interrupt();
+        }
+        mInputThread = null;
+    }
+
+    /** Stop the Output thread and close the Output stream. */
+    private void closeOutputLocked() {
+        if (mOutputThread != null) {
+            mOutputThread.quit();
+        }
+        mOutputThread = null;
+        if (mOutputStream != null) {
+            try {
+                mOutputStream.close();
+            } catch (IOException e) {
+                Slog.e(LOG_TAG, "Unable to close output stream", e);
+            }
+        }
+        mOutputStream = null;
+    }
+
+    /**
+     * Returns a {@link BrailleDisplayScanner} that opens {@link FileInputStream}s to read
+     * from HIDRAW nodes and perform ioctls using the provided {@link NativeInterface}.
+     */
+    @VisibleForTesting
+    BrailleDisplayScanner getDefaultNativeScanner(@NonNull NativeInterface nativeInterface) {
+        Objects.requireNonNull(nativeInterface);
+        return new BrailleDisplayScanner() {
+            private static final Path DEVICE_DIR = Path.of("/dev");
+            private static final String HIDRAW_DEVICE_GLOB = "hidraw*";
+
+            @Override
+            public Collection<Path> getHidrawNodePaths() {
+                final List<Path> result = new ArrayList<>();
+                try (DirectoryStream<Path> hidrawNodePaths = Files.newDirectoryStream(
+                        DEVICE_DIR, HIDRAW_DEVICE_GLOB)) {
+                    for (Path path : hidrawNodePaths) {
+                        result.add(path);
+                    }
+                    return result;
+                } catch (IOException e) {
+                    return null;
+                }
+            }
+
+            private <T> T readFromFileDescriptor(Path path, Function<Integer, T> readFn) {
+                try (FileInputStream stream = new FileInputStream(path.toFile())) {
+                    return readFn.apply(stream.getFD().getInt$());
+                } catch (IOException e) {
+                    return null;
+                }
+            }
+
+            @Override
+            public byte[] getDeviceReportDescriptor(@NonNull Path path) {
+                Objects.requireNonNull(path);
+                return readFromFileDescriptor(path, fd -> {
+                    final int descSize = nativeInterface.getHidrawDescSize(fd);
+                    if (descSize > 0) {
+                        return nativeInterface.getHidrawDesc(fd, descSize);
+                    }
+                    return null;
+                });
+            }
+
+            @Override
+            public String getUniqueId(@NonNull Path path) {
+                Objects.requireNonNull(path);
+                return readFromFileDescriptor(path, nativeInterface::getHidrawUniq);
+            }
+
+            @Override
+            public int getDeviceBusType(@NonNull Path path) {
+                Objects.requireNonNull(path);
+                Integer busType = readFromFileDescriptor(path, nativeInterface::getHidrawBusType);
+                return busType != null ? busType : BUS_UNKNOWN;
+            }
+        };
+    }
+
+    /**
+     * Sets test data to be used by CTS tests.
+     *
+     * <p>Replaces the default {@link BrailleDisplayScanner} object for this connection,
+     * and also returns it to allow unit testing this test-only implementation.
+     *
+     * @see BrailleDisplayController#setTestBrailleDisplayData
+     */
+    BrailleDisplayScanner setTestData(@NonNull List<Bundle> brailleDisplays) {
+        Objects.requireNonNull(brailleDisplays);
+        final Map<Path, Bundle> brailleDisplayMap = new ArrayMap<>();
+        for (Bundle brailleDisplay : brailleDisplays) {
+            Path hidrawNodePath = Path.of(brailleDisplay.getString(
+                    BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH));
+            brailleDisplayMap.put(hidrawNodePath, brailleDisplay);
+        }
+        synchronized (mLock) {
+            mScanner = new BrailleDisplayScanner() {
+                @Override
+                public Collection<Path> getHidrawNodePaths() {
+                    return brailleDisplayMap.keySet();
+                }
+
+                @Override
+                public byte[] getDeviceReportDescriptor(@NonNull Path path) {
+                    return brailleDisplayMap.get(path).getByteArray(
+                            BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR);
+                }
+
+                @Override
+                public String getUniqueId(@NonNull Path path) {
+                    return brailleDisplayMap.get(path).getString(
+                            BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID);
+                }
+
+                @Override
+                public int getDeviceBusType(@NonNull Path path) {
+                    return brailleDisplayMap.get(path).getBoolean(
+                            BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH)
+                            ? BUS_BLUETOOTH : BUS_USB;
+                }
+            };
+            return mScanner;
+        }
+    }
+
+    /**
+     * This interface exists to support unit testing {@link #getDefaultNativeScanner}.
+     */
+    @VisibleForTesting
+    interface NativeInterface {
+        int getHidrawDescSize(int fd);
+
+        byte[] getHidrawDesc(int fd, int descSize);
+
+        String getHidrawUniq(int fd);
+
+        int getHidrawBusType(int fd);
+    }
+
+    /** Native interface that actually calls native HIDRAW ioctls. */
+    private NativeInterface getDefaultNativeInterface() {
+        return new NativeInterface() {
+            @Override
+            public int getHidrawDescSize(int fd) {
+                return nativeGetHidrawDescSize(fd);
+            }
+
+            @Override
+            public byte[] getHidrawDesc(int fd, int descSize) {
+                return nativeGetHidrawDesc(fd, descSize);
+            }
+
+            @Override
+            public String getHidrawUniq(int fd) {
+                return nativeGetHidrawUniq(fd);
+            }
+
+            @Override
+            public int getHidrawBusType(int fd) {
+                return nativeGetHidrawBusType(fd);
+            }
+        };
+    }
+
+    private native int nativeGetHidrawDescSize(int fd);
+
+    private native byte[] nativeGetHidrawDesc(int fd, int descSize);
+
+    private native String nativeGetHidrawUniq(int fd);
+
+    private native int nativeGetHidrawBusType(int fd);
+}
diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java
index 2e14abb..a341b4a 100644
--- a/services/core/java/com/android/server/SystemConfig.java
+++ b/services/core/java/com/android/server/SystemConfig.java
@@ -263,10 +263,6 @@
     // location settings are off, for emergency purposes, as read from the configuration files.
     final ArrayMap<String, ArraySet<String>> mAllowIgnoreLocationSettings = new ArrayMap<>();
 
-    // These are the packages that are allow-listed to be able to access camera when
-    // the camera privacy state is for driver assistance apps only.
-    final ArrayMap<String, Boolean> mAllowlistCameraPrivacy = new ArrayMap<>();
-
     // These are the action strings of broadcasts which are whitelisted to
     // be delivered anonymously even to apps which target O+.
     final ArraySet<String> mAllowImplicitBroadcasts = new ArraySet<>();
@@ -487,10 +483,6 @@
         return mAllowedAssociations;
     }
 
-    public ArrayMap<String, Boolean> getCameraPrivacyAllowlist() {
-        return mAllowlistCameraPrivacy;
-    }
-
     public ArraySet<String> getBugreportWhitelistedPackages() {
         return mBugreportWhitelistedPackages;
     }
@@ -1070,22 +1062,6 @@
                         }
                         XmlUtils.skipCurrentTag(parser);
                     } break;
-                    case "camera-privacy-allowlisted-app" : {
-                        if (allowOverrideAppRestrictions) {
-                            String pkgname = parser.getAttributeValue(null, "package");
-                            boolean isMandatory = XmlUtils.readBooleanAttribute(
-                                    parser, "mandatory", false);
-                            if (pkgname == null) {
-                                Slog.w(TAG, "<" + name + "> without package in "
-                                        + permFile + " at " + parser.getPositionDescription());
-                            } else {
-                                mAllowlistCameraPrivacy.put(pkgname, isMandatory);
-                            }
-                        } else {
-                            logNotAllowedInPartition(name, permFile, parser);
-                        }
-                        XmlUtils.skipCurrentTag(parser);
-                    } break;
                     case "allow-ignore-location-settings": {
                         if (allowOverrideAppRestrictions) {
                             String pkgname = parser.getAttributeValue(null, "package");
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 63ea7b4..5c95d43 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -106,7 +106,6 @@
 import android.content.pm.PermissionInfo;
 import android.content.pm.UserInfo;
 import android.database.ContentObserver;
-import android.hardware.SensorPrivacyManager;
 import android.hardware.camera2.CameraDevice.CAMERA_AUDIO_RESTRICTION;
 import android.net.Uri;
 import android.os.AsyncTask;
@@ -152,7 +151,6 @@
 import com.android.internal.app.IAppOpsService;
 import com.android.internal.app.IAppOpsStartedCallback;
 import com.android.internal.app.MessageSamplingConfig;
-import com.android.internal.camera.flags.Flags;
 import com.android.internal.compat.IPlatformCompat;
 import com.android.internal.os.Clock;
 import com.android.internal.pm.pkg.component.ParsedAttribution;
@@ -225,8 +223,6 @@
      */
     private static final int CURRENT_VERSION = 1;
 
-    private SensorPrivacyManager mSensorPrivacyManager;
-
     // Write at most every 30 minutes.
     static final long WRITE_DELAY = DEBUG ? 1000 : 30*60*1000;
 
@@ -1235,7 +1231,6 @@
                         }
                     }
                 });
-        mSensorPrivacyManager = SensorPrivacyManager.getInstance(mContext);
     }
 
     @VisibleForTesting
@@ -4647,10 +4642,6 @@
         return pmi.isPackageSuspended(packageName, UserHandle.getUserId(uid));
     }
 
-    private boolean isAutomotive() {
-        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
-    }
-
     private boolean isOpRestrictedLocked(int uid, int code, String packageName,
             String attributionTag, int virtualDeviceId, @Nullable RestrictionBypass appBypass,
             boolean isCheckOp) {
@@ -4667,14 +4658,6 @@
             }
         }
 
-        if (Flags.privacyAllowlist()) {
-            if ((code == OP_CAMERA) && isAutomotive()) {
-                if (mSensorPrivacyManager.isCameraPrivacyEnabled(packageName)) {
-                    return true;
-                }
-            }
-        }
-
         int userHandle = UserHandle.getUserId(uid);
         restrictionSetCount = mOpUserRestrictions.size();
 
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerStub.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerStub.java
index 3f00a9d..7d90240 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerStub.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerStub.java
@@ -26,6 +26,7 @@
 import android.content.pm.verify.domain.DomainVerificationManager;
 import android.content.pm.verify.domain.DomainVerificationUserState;
 import android.content.pm.verify.domain.IDomainVerificationManager;
+import android.os.Bundle;
 import android.os.ServiceSpecificException;
 
 import java.util.List;
@@ -41,6 +42,27 @@
         mService = service;
     }
 
+    @Override
+    public void setUriRelativeFilterGroups(@NonNull String packageName,
+            @NonNull Bundle domainToGroupsBundle) {
+        try {
+            mService.setUriRelativeFilterGroups(packageName, domainToGroupsBundle);
+        } catch (Exception e) {
+            throw rethrow(e);
+        }
+    }
+
+    @NonNull
+    @Override
+    public Bundle getUriRelativeFilterGroups(
+            @NonNull String packageName, @NonNull List<String> domains) {
+        try {
+            return mService.getUriRelativeFilterGroups(packageName, domains);
+        } catch (Exception e) {
+            throw rethrow(e);
+        }
+    }
+
     @NonNull
     @Override
     public List<String> queryValidVerificationPackageNames() {
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java
index ac6d795..de464a4 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java
@@ -19,6 +19,8 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
+import android.content.UriRelativeFilter;
+import android.content.UriRelativeFilterGroup;
 import android.content.pm.Signature;
 import android.content.pm.verify.domain.DomainVerificationState;
 import android.os.UserHandle;
@@ -38,7 +40,10 @@
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
 import java.util.UUID;
 import java.util.function.Function;
 
@@ -67,6 +72,13 @@
     public static final String TAG_DOMAIN = "domain";
     public static final String ATTR_NAME = "name";
     public static final String ATTR_STATE = "state";
+    public static final String TAG_URI_RELATIVE_FILTER_GROUPS = "uri-relative-filter-groups";
+    public static final String TAG_URI_RELATIVE_FILTER_GROUP = "uri-relative-filter-group";
+    public static final String ATTR_ACTION = "action";
+    public static final String TAG_URI_RELATIVE_FILTER = "uri-relative-filter";
+    public static final String ATTR_URI_PART = "uri-part";
+    public static final String ATTR_PATTERN_TYPE = "pattern-type";
+    public static final String ATTR_FILTER = "filter";
 
     /**
      * @param pkgNameToSignature Converts package name to a string representation of its signature.
@@ -176,6 +188,7 @@
 
         final ArrayMap<String, Integer> stateMap = new ArrayMap<>();
         final SparseArray<DomainVerificationInternalUserState> userStates = new SparseArray<>();
+        final ArrayMap<String, List<UriRelativeFilterGroup>> groupMap = new ArrayMap<>();
 
         SettingsXml.ChildSection child = section.children();
         while (child.moveToNext()) {
@@ -186,11 +199,47 @@
                 case TAG_USER_STATES:
                     readUserStates(child, userStates);
                     break;
+                case TAG_URI_RELATIVE_FILTER_GROUPS:
+                    readUriRelativeFilterGroups(child, groupMap);
+                    break;
             }
         }
 
         return new DomainVerificationPkgState(packageName, id, hasAutoVerifyDomains, stateMap,
-                userStates, signature);
+                userStates, signature, groupMap);
+    }
+
+    private static void readUriRelativeFilterGroups(@NonNull SettingsXml.ReadSection section,
+            @NonNull ArrayMap<String, List<UriRelativeFilterGroup>> groupMap) {
+        SettingsXml.ChildSection child = section.children();
+        while (child.moveToNext(TAG_DOMAIN)) {
+            String domain = child.getString(ATTR_NAME);
+            groupMap.put(domain, createUriRelativeFilterGroupsFromXml(child));
+        }
+    }
+
+    private static ArrayList<UriRelativeFilterGroup> createUriRelativeFilterGroupsFromXml(
+            @NonNull SettingsXml.ReadSection section) {
+        SettingsXml.ChildSection child = section.children();
+        ArrayList<UriRelativeFilterGroup> groups = new ArrayList<>();
+        while (child.moveToNext(TAG_URI_RELATIVE_FILTER_GROUP)) {
+            UriRelativeFilterGroup group = new UriRelativeFilterGroup(section.getInt(ATTR_ACTION));
+            readUriRelativeFiltersFromXml(child, group);
+            groups.add(group);
+        }
+        return groups;
+    }
+
+    private static void readUriRelativeFiltersFromXml(
+            @NonNull SettingsXml.ReadSection section, UriRelativeFilterGroup group) {
+        SettingsXml.ChildSection child = section.children();
+        while (child.moveToNext(TAG_URI_RELATIVE_FILTER)) {
+            String filter = child.getString(ATTR_FILTER);
+            if (filter != null) {
+                group.addUriRelativeFilter(new UriRelativeFilter(child.getInt(ATTR_URI_PART),
+                        child.getInt(ATTR_PATTERN_TYPE), filter));
+            }
+        }
     }
 
     private static void readUserStates(@NonNull SettingsXml.ReadSection section,
@@ -236,6 +285,7 @@
                              .attribute(ATTR_SIGNATURE, signature)) {
             writeStateMap(parentSection, pkgState.getStateMap());
             writeUserStates(parentSection, userId, pkgState.getUserStates());
+            writeUriRelativeFilterGroupMap(parentSection, pkgState.getUriRelativeFilterGroupMap());
         }
     }
 
@@ -334,6 +384,52 @@
         }
     }
 
+    private static void writeUriRelativeFilterGroupMap(
+            @NonNull SettingsXml.WriteSection parentSection,
+            @NonNull ArrayMap<String, List<UriRelativeFilterGroup>> groupMap) throws IOException {
+        if (groupMap.isEmpty()) {
+            return;
+        }
+        try (SettingsXml.WriteSection section =
+                     parentSection.startSection(TAG_URI_RELATIVE_FILTER_GROUPS)) {
+            for (int i = 0; i < groupMap.size(); i++) {
+                writeUriRelativeFilterGroups(section, groupMap.keyAt(i), groupMap.valueAt(i));
+            }
+        }
+    }
+
+    private static void writeUriRelativeFilterGroups(
+            @NonNull SettingsXml.WriteSection parentSection, @NonNull String domain,
+            @NonNull List<UriRelativeFilterGroup> groups) throws IOException {
+        if (groups.isEmpty()) {
+            return;
+        }
+        try (SettingsXml.WriteSection section =
+                     parentSection.startSection(TAG_DOMAIN)
+                             .attribute(ATTR_NAME, domain)) {
+            for (int i = 0; i < groups.size(); i++) {
+                writeUriRelativeFilterGroup(section, groups.get(i));
+            }
+        }
+    }
+
+    private static void writeUriRelativeFilterGroup(
+            @NonNull SettingsXml.WriteSection parentSection,
+            @NonNull UriRelativeFilterGroup group) throws IOException {
+        try (SettingsXml.WriteSection section =
+                     parentSection.startSection(TAG_URI_RELATIVE_FILTER_GROUP)
+                             .attribute(ATTR_ACTION, group.getAction())) {
+            Iterator<UriRelativeFilter> it = group.getUriRelativeFilters().iterator();
+            while (it.hasNext()) {
+                UriRelativeFilter filter = it.next();
+                section.startSection(TAG_URI_RELATIVE_FILTER)
+                        .attribute(ATTR_URI_PART, filter.getUriPart())
+                        .attribute(ATTR_PATTERN_TYPE, filter.getPatternType())
+                        .attribute(ATTR_FILTER, filter.getFilter()).finish();
+            }
+        }
+    }
+
     public static class ReadResult {
 
         @NonNull
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
index c796b40..305b087 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
@@ -19,14 +19,19 @@
 import static java.util.Collections.emptyList;
 import static java.util.Collections.emptySet;
 
+import android.Manifest;
 import android.annotation.CheckResult;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
 import android.compat.annotation.ChangeId;
 import android.content.Context;
 import android.content.Intent;
+import android.content.UriRelativeFilterGroup;
+import android.content.UriRelativeFilterGroupParcel;
+import android.content.pm.Flags;
 import android.content.pm.IntentFilterVerificationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -38,6 +43,8 @@
 import android.content.pm.verify.domain.DomainVerificationState;
 import android.content.pm.verify.domain.DomainVerificationUserState;
 import android.content.pm.verify.domain.IDomainVerificationManager;
+import android.net.Uri;
+import android.os.Bundle;
 import android.os.UserHandle;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -223,6 +230,72 @@
         mProxy = proxy;
     }
 
+    /**
+     * Update the URI relative filter groups for a package's verified domains. All previously
+     * existing groups will be cleared before the new groups will be applied.
+     */
+    @RequiresPermission(Manifest.permission.DOMAIN_VERIFICATION_AGENT)
+    public void setUriRelativeFilterGroups(@NonNull String packageName,
+            @NonNull Bundle bundle)
+            throws NameNotFoundException {
+        getContext().enforceCallingOrSelfPermission(
+                android.Manifest.permission.DOMAIN_VERIFICATION_AGENT,
+                "Caller " + mConnection.getCallingUid() + " does not hold "
+                        + android.Manifest.permission.DOMAIN_VERIFICATION_AGENT);
+        if (bundle.isEmpty()) {
+            return;
+        }
+        synchronized (mLock) {
+            DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName);
+            if (pkgState == null) {
+                throw DomainVerificationUtils.throwPackageUnavailable(packageName);
+            }
+            Map<String, List<UriRelativeFilterGroup>> domainToGroupsMap =
+                    pkgState.getUriRelativeFilterGroupMap();
+            for (String domain : bundle.keySet()) {
+                ArrayList<UriRelativeFilterGroupParcel> parcels =
+                        bundle.getParcelableArrayList(domain, UriRelativeFilterGroupParcel.class);
+                domainToGroupsMap.put(domain, UriRelativeFilterGroup.parcelsToGroups(parcels));
+            }
+        }
+    }
+
+    /**
+     * Retrieve the current URI relative filter groups for a package's verified domain.
+     */
+    @NonNull
+    public Bundle getUriRelativeFilterGroups(@NonNull String packageName,
+            @NonNull List<String> domains) {
+        Bundle bundle = new Bundle();
+        synchronized (mLock) {
+            DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName);
+            if (pkgState != null) {
+                Map<String, List<UriRelativeFilterGroup>> map =
+                        pkgState.getUriRelativeFilterGroupMap();
+                for (int i = 0; i < domains.size(); i++) {
+                    List<UriRelativeFilterGroup> groups = map.get(domains.get(i));
+                    bundle.putParcelableList(domains.get(i),
+                            UriRelativeFilterGroup.groupsToParcels(groups));
+                }
+            }
+        }
+        return bundle;
+    }
+
+    @NonNull
+    private List<UriRelativeFilterGroup> getUriRelativeFilterGroups(@NonNull String packageName,
+            @NonNull String domain) {
+        List<UriRelativeFilterGroup> groups = Collections.emptyList();
+        synchronized (mLock) {
+            DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName);
+            if (pkgState != null) {
+                groups = pkgState.getUriRelativeFilterGroupMap().getOrDefault(domain,
+                        Collections.emptyList());
+            }
+        }
+        return groups;
+    }
+
     @NonNull
     public List<String> queryValidVerificationPackageNames() {
         mEnforcer.assertApprovedVerifier(mConnection.getCallingUid(), mProxy);
@@ -891,6 +964,8 @@
             }
 
             ArrayMap<String, Integer> oldStateMap = oldPkgState.getStateMap();
+            ArrayMap<String, List<UriRelativeFilterGroup>> oldGroups =
+                    oldPkgState.getUriRelativeFilterGroupMap();
             ArraySet<String> newAutoVerifyDomains =
                     mCollector.collectValidAutoVerifyDomains(newPkg);
             int newDomainsSize = newAutoVerifyDomains.size();
@@ -941,7 +1016,7 @@
 
             mAttachedPkgStates.put(pkgName, newDomainSetId, new DomainVerificationPkgState(
                     pkgName, newDomainSetId, hasAutoVerifyDomains, newStateMap, newUserStates,
-                    null /* signature */));
+                    null /* signature */, oldGroups));
         }
 
         if (sendBroadcast) {
@@ -1572,8 +1647,6 @@
     public Pair<List<ResolveInfo>, Integer> filterToApprovedApp(@NonNull Intent intent,
             @NonNull List<ResolveInfo> infos, @UserIdInt int userId,
             @NonNull Function<String, PackageStateInternal> pkgSettingFunction) {
-        String domain = intent.getData().getHost();
-
         // Collect valid infos
         ArrayMap<ResolveInfo, Integer> infoApprovals = new ArrayMap<>();
         int infosSize = infos.size();
@@ -1586,7 +1659,7 @@
         }
 
         // Find all approval levels
-        int highestApproval = fillMapWithApprovalLevels(infoApprovals, domain, userId,
+        int highestApproval = fillMapWithApprovalLevels(infoApprovals, intent.getData(), userId,
                 pkgSettingFunction);
         if (highestApproval <= APPROVAL_LEVEL_NONE) {
             return Pair.create(emptyList(), highestApproval);
@@ -1623,12 +1696,23 @@
         return Pair.create(finalList, highestApproval);
     }
 
+    private boolean matchUriRelativeFilterGroups(Uri uri, String pkgName) {
+        if (uri.getHost() == null) {
+            return false;
+        }
+        List<UriRelativeFilterGroup> groups = getUriRelativeFilterGroups(pkgName, uri.getHost());
+        if (groups.isEmpty()) {
+            return true;
+        }
+        return UriRelativeFilterGroup.matchGroupsToUri(groups, uri);
+    }
+
     /**
      * @return highest approval level found
      */
     @ApprovalLevel
     private int fillMapWithApprovalLevels(@NonNull ArrayMap<ResolveInfo, Integer> inputMap,
-            @NonNull String domain, @UserIdInt int userId,
+            @NonNull Uri uri, @UserIdInt int userId,
             @NonNull Function<String, PackageStateInternal> pkgSettingFunction) {
         int highestApproval = APPROVAL_LEVEL_NONE;
         int size = inputMap.size();
@@ -1641,12 +1725,13 @@
             ResolveInfo info = inputMap.keyAt(index);
             final String packageName = info.getComponentInfo().packageName;
             PackageStateInternal pkgSetting = pkgSettingFunction.apply(packageName);
-            if (pkgSetting == null) {
+            if (pkgSetting == null || (Flags.relativeReferenceIntentFilters()
+                    && !matchUriRelativeFilterGroups(uri, packageName))) {
                 fillInfoMapForSamePackage(inputMap, packageName, APPROVAL_LEVEL_NONE);
                 continue;
             }
-            int approval = approvalLevelForDomain(pkgSetting, domain, false, userId, DEBUG_APPROVAL,
-                    domain);
+            int approval = approvalLevelForDomain(pkgSetting, uri.getHost(), false, userId,
+                    DEBUG_APPROVAL, uri.getHost());
             highestApproval = Math.max(highestApproval, approval);
             fillInfoMapForSamePackage(inputMap, packageName, approval);
         }
diff --git a/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java b/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java
index d71dbbb..46051fe 100644
--- a/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java
+++ b/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
+import android.content.UriRelativeFilterGroup;
 import android.content.pm.Signature;
 import android.content.pm.verify.domain.DomainVerificationState;
 import android.util.ArrayMap;
@@ -26,6 +27,7 @@
 
 import com.android.internal.util.DataClass;
 
+import java.util.List;
 import java.util.Objects;
 import java.util.UUID;
 
@@ -77,15 +79,30 @@
     @Nullable
     private final String mBackupSignatureHash;
 
+    /**
+     * List of {@link UriRelativeFilterGroup} for filtering domains.
+     */
+    @NonNull
+    private final ArrayMap<String, List<UriRelativeFilterGroup>> mUriRelativeFilterGroupMap;
+
     public DomainVerificationPkgState(@NonNull String packageName, @NonNull UUID id,
             boolean hasAutoVerifyDomains) {
-        this(packageName, id, hasAutoVerifyDomains, new ArrayMap<>(0), new SparseArray<>(0), null);
+        this(packageName, id, hasAutoVerifyDomains, new ArrayMap<>(0), new SparseArray<>(0), null,
+                new ArrayMap<>());
     }
 
     public DomainVerificationPkgState(@NonNull DomainVerificationPkgState pkgState,
             @NonNull UUID id, boolean hasAutoVerifyDomains) {
         this(pkgState.getPackageName(), id, hasAutoVerifyDomains, pkgState.getStateMap(),
-                pkgState.getUserStates(), null);
+                pkgState.getUserStates(), null, new ArrayMap<>());
+    }
+
+    public DomainVerificationPkgState(@NonNull String packageName, @NonNull UUID id,
+            boolean hasAutoVerifyDomains, @NonNull ArrayMap<String, Integer> stateMap,
+            @NonNull SparseArray<DomainVerificationInternalUserState> userStates,
+            @Nullable String backupSignatureHash) {
+        this(packageName, id, hasAutoVerifyDomains, stateMap, userStates, backupSignatureHash,
+                new ArrayMap<>());
     }
 
     @Nullable
@@ -158,6 +175,8 @@
      *
      *   It's assumed the domain verification agent will eventually re-verify this domain
      *   and revoke if necessary.
+     * @param uriRelativeFilterGroupMap
+     *   List of {@link UriRelativeFilterGroup} for filtering domains.
      */
     @DataClass.Generated.Member
     public DomainVerificationPkgState(
@@ -166,7 +185,8 @@
             boolean hasAutoVerifyDomains,
             @NonNull ArrayMap<String,Integer> stateMap,
             @NonNull SparseArray<DomainVerificationInternalUserState> userStates,
-            @Nullable String backupSignatureHash) {
+            @Nullable String backupSignatureHash,
+            @NonNull ArrayMap<String,List<UriRelativeFilterGroup>> uriRelativeFilterGroupMap) {
         this.mPackageName = packageName;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mPackageName);
@@ -181,6 +201,9 @@
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mUserStates);
         this.mBackupSignatureHash = backupSignatureHash;
+        this.mUriRelativeFilterGroupMap = uriRelativeFilterGroupMap;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mUriRelativeFilterGroupMap);
 
         // onConstructed(); // You can define this method to get a callback
     }
@@ -239,6 +262,14 @@
         return mBackupSignatureHash;
     }
 
+    /**
+     * List of {@link UriRelativeFilterGroup} for filtering domains.
+     */
+    @DataClass.Generated.Member
+    public @NonNull ArrayMap<String,List<UriRelativeFilterGroup>> getUriRelativeFilterGroupMap() {
+        return mUriRelativeFilterGroupMap;
+    }
+
     @Override
     @DataClass.Generated.Member
     public String toString() {
@@ -251,7 +282,8 @@
                 "hasAutoVerifyDomains = " + mHasAutoVerifyDomains + ", " +
                 "stateMap = " + mStateMap + ", " +
                 "userStates = " + mUserStates + ", " +
-                "backupSignatureHash = " + mBackupSignatureHash +
+                "backupSignatureHash = " + mBackupSignatureHash + ", " +
+                "uriRelativeFilterGroupMap = " + mUriRelativeFilterGroupMap +
         " }";
     }
 
@@ -273,7 +305,8 @@
                 && mHasAutoVerifyDomains == that.mHasAutoVerifyDomains
                 && Objects.equals(mStateMap, that.mStateMap)
                 && userStatesEquals(that.mUserStates)
-                && Objects.equals(mBackupSignatureHash, that.mBackupSignatureHash);
+                && Objects.equals(mBackupSignatureHash, that.mBackupSignatureHash)
+                && Objects.equals(mUriRelativeFilterGroupMap, that.mUriRelativeFilterGroupMap);
     }
 
     @Override
@@ -289,14 +322,15 @@
         _hash = 31 * _hash + Objects.hashCode(mStateMap);
         _hash = 31 * _hash + userStatesHashCode();
         _hash = 31 * _hash + Objects.hashCode(mBackupSignatureHash);
+        _hash = 31 * _hash + Objects.hashCode(mUriRelativeFilterGroupMap);
         return _hash;
     }
 
     @DataClass.Generated(
-            time = 1617315369614L,
+            time = 1707351734724L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java",
-            inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate @android.annotation.NonNull java.util.UUID mId\nprivate final  boolean mHasAutoVerifyDomains\nprivate final @android.annotation.NonNull android.util.ArrayMap<java.lang.String,java.lang.Integer> mStateMap\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState> mUserStates\nprivate final @android.annotation.Nullable java.lang.String mBackupSignatureHash\npublic @android.annotation.Nullable com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState getUserState(int)\npublic @android.annotation.Nullable com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState getOrCreateUserState(int)\npublic  void removeUser(int)\npublic  void removeAllUsers()\nprivate  int userStatesHashCode()\nprivate  boolean userStatesEquals(android.util.SparseArray<com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState>)\nclass DomainVerificationPkgState extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genToString=true, genEqualsHashCode=true)")
+            inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.util.UUID mId\nprivate final  boolean mHasAutoVerifyDomains\nprivate final @android.annotation.NonNull android.util.ArrayMap<java.lang.String,java.lang.Integer> mStateMap\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState> mUserStates\nprivate final @android.annotation.Nullable java.lang.String mBackupSignatureHash\nprivate final @android.annotation.NonNull android.util.ArrayMap<java.lang.String,java.util.List<android.content.UriRelativeFilterGroup>> mUriRelativeFilterGroupMap\npublic @android.annotation.Nullable com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState getUserState(int)\npublic @android.annotation.Nullable com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState getOrCreateUserState(int)\npublic  void removeUser(int)\npublic  void removeAllUsers()\nprivate  int userStatesHashCode()\nprivate  boolean userStatesEquals(android.util.SparseArray<com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState>)\nclass DomainVerificationPkgState extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genToString=true, genEqualsHashCode=true)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
index 64cfc8d4..59766ec 100644
--- a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
+++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
@@ -45,11 +45,6 @@
 import static android.hardware.SensorPrivacyManager.Sources.QS_TILE;
 import static android.hardware.SensorPrivacyManager.Sources.SETTINGS;
 import static android.hardware.SensorPrivacyManager.Sources.SHELL;
-import static android.hardware.SensorPrivacyManager.StateTypes.AUTOMOTIVE_DRIVER_ASSISTANCE_APPS;
-import static android.hardware.SensorPrivacyManager.StateTypes.AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS;
-import static android.hardware.SensorPrivacyManager.StateTypes.AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS;
-import static android.hardware.SensorPrivacyManager.StateTypes.DISABLED;
-import static android.hardware.SensorPrivacyManager.StateTypes.ENABLED;
 import static android.hardware.SensorPrivacyManager.TOGGLE_TYPE_HARDWARE;
 import static android.hardware.SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE;
 import static android.os.UserHandle.USER_NULL;
@@ -57,9 +52,6 @@
 
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION;
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__ACTION_UNKNOWN;
-import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_APPS;
-import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS;
-import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS;
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_OFF;
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_ON;
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__SENSOR__CAMERA;
@@ -71,11 +63,8 @@
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__SOURCE__SOURCE_UNKNOWN;
 import static com.android.internal.util.FrameworkStatsLog.write;
 
-import android.Manifest;
-import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.RequiresPermission;
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
@@ -98,7 +87,6 @@
 import android.content.res.Configuration;
 import android.database.ContentObserver;
 import android.graphics.drawable.Icon;
-import android.hardware.CameraPrivacyAllowlistEntry;
 import android.hardware.ISensorPrivacyListener;
 import android.hardware.ISensorPrivacyManager;
 import android.hardware.SensorPrivacyManager;
@@ -135,7 +123,6 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.camera.flags.Flags;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.DumpUtils;
@@ -144,7 +131,6 @@
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.server.FgThread;
 import com.android.server.LocalServices;
-import com.android.server.SystemConfig;
 import com.android.server.SystemService;
 import com.android.server.pm.UserManagerInternal;
 
@@ -153,7 +139,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Objects;
 
@@ -169,24 +154,7 @@
             SensorPrivacyService.class.getName() + ".action.disable_sensor_privacy";
 
     public static final int REMINDER_DIALOG_DELAY_MILLIS = 500;
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    private static final int ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS =
-            PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS;
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    private static final int ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS =
-            PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS;
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    private static final int ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_APPS =
-            PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_APPS;
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    private static final int ACTION__TOGGLE_ON =
-            PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_ON;
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    private static final int ACTION__TOGGLE_OFF =
-            PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_OFF;
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    private static final int ACTION__ACTION_UNKNOWN =
-            PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__ACTION_UNKNOWN;
+
     private final Context mContext;
     private final SensorPrivacyServiceImpl mSensorPrivacyServiceImpl;
     private final UserManagerInternal mUserManagerInternal;
@@ -208,9 +176,6 @@
     private CallStateHelper mCallStateHelper;
     private KeyguardManager mKeyguardManager;
 
-    List<CameraPrivacyAllowlistEntry> mCameraPrivacyAllowlist =
-            new ArrayList<CameraPrivacyAllowlistEntry>();
-
     private int mCurrentUser = USER_NULL;
 
     public SensorPrivacyService(Context context) {
@@ -227,15 +192,6 @@
         mPackageManagerInternal = getLocalService(PackageManagerInternal.class);
         mNotificationManager = mContext.getSystemService(NotificationManager.class);
         mSensorPrivacyServiceImpl = new SensorPrivacyServiceImpl();
-        ArrayMap<String, Boolean> cameraPrivacyAllowlist =
-                SystemConfig.getInstance().getCameraPrivacyAllowlist();
-
-        for (Map.Entry<String, Boolean> entry : cameraPrivacyAllowlist.entrySet()) {
-            CameraPrivacyAllowlistEntry ent = new CameraPrivacyAllowlistEntry();
-            ent.packageName = entry.getKey();
-            ent.isMandatory =  entry.getValue();
-            mCameraPrivacyAllowlist.add(ent);
-        }
     }
 
     @Override
@@ -368,15 +324,8 @@
                     mHandler, mHandler::handleSensorPrivacyChanged);
             mSensorPrivacyStateController.setSensorPrivacyListener(
                     mHandler,
-                    (toggleType, userId, sensor, state) -> {
-                        mHandler.handleSensorPrivacyChanged(
-                                userId, toggleType, sensor, state.isEnabled());
-                        if (Flags.privacyAllowlist()) {
-                            mHandler.handleSensorPrivacyChanged(
-                                    userId, toggleType, sensor, state.getState());
-                        }
-                    });
-
+                    (toggleType, userId, sensor, state) -> mHandler.handleSensorPrivacyChanged(
+                            userId, toggleType, sensor, state.isEnabled()));
         }
 
         // If sensor privacy is enabled for a sensor, but the device doesn't support sensor privacy
@@ -451,15 +400,9 @@
          * @param packageName The package name of the app using the sensor
          * @param sensor The sensor that is attempting to be used
          */
-        @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
         private void onSensorUseStarted(int uid, String packageName, int sensor) {
             UserHandle user = UserHandle.of(mCurrentUser);
-
-            if (Flags.privacyAllowlist() && (sensor == CAMERA) && isAutomotive(mContext)) {
-                if (!isCameraPrivacyEnabled(packageName)) {
-                    return;
-                }
-            } else if (!isCombinedToggleSensorPrivacyEnabled(sensor)) {
+            if (!isCombinedToggleSensorPrivacyEnabled(sensor)) {
                 return;
             }
 
@@ -784,12 +727,6 @@
                     == Configuration.UI_MODE_TYPE_TELEVISION;
         }
 
-        private boolean isAutomotive(Context context) {
-            int uiMode = context.getResources().getConfiguration().uiMode;
-            return (uiMode & Configuration.UI_MODE_TYPE_MASK)
-                    == Configuration.UI_MODE_TYPE_CAR;
-        }
-
         /**
          * Sets the sensor privacy to the provided state and notifies all listeners of the new
          * state.
@@ -829,225 +766,6 @@
             setToggleSensorPrivacyUnchecked(TOGGLE_TYPE_SOFTWARE, userId, source, sensor, enable);
         }
 
-
-        @Override
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        @RequiresPermission(Manifest.permission.MANAGE_SENSOR_PRIVACY)
-        public void setToggleSensorPrivacyState(int userId, int source, int sensor, int state) {
-            if (DEBUG) {
-                Log.d(TAG, "callingUid=" + Binder.getCallingUid()
-                        + " callingPid=" + Binder.getCallingPid()
-                        + " setToggleSensorPrivacyState("
-                        + "userId=" + userId
-                        + " source=" + source
-                        + " sensor=" + sensor
-                        + " state=" + state
-                        + ")");
-            }
-            enforceManageSensorPrivacyPermission();
-            if (userId == UserHandle.USER_CURRENT) {
-                userId = mCurrentUser;
-            }
-
-            if (!canChangeToggleSensorPrivacy(userId, sensor)) {
-                return;
-            }
-            if (!supportsSensorToggle(TOGGLE_TYPE_SOFTWARE, sensor)) {
-                // Do not enable sensor privacy if the device doesn't support it.
-                return;
-            }
-
-            setToggleSensorPrivacyStateUnchecked(TOGGLE_TYPE_SOFTWARE, userId, source, sensor,
-                    state);
-        }
-
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        private void setToggleSensorPrivacyStateUnchecked(int toggleType, int userId, int source,
-                int sensor, int state) {
-            if (DEBUG) {
-                Log.d(TAG, "callingUid=" + Binder.getCallingUid()
-                        + " callingPid=" + Binder.getCallingPid()
-                        + " setToggleSensorPrivacyStateUnchecked("
-                        + "userId=" + userId
-                        + " source=" + source
-                        + " sensor=" + sensor
-                        + " state=" + state
-                        + ")");
-            }
-            long[] lastChange = new long[1];
-            mSensorPrivacyStateController.atomic(() -> {
-                SensorState sensorState = mSensorPrivacyStateController
-                        .getState(toggleType, userId, sensor);
-                lastChange[0] = sensorState.getLastChange();
-                mSensorPrivacyStateController.setState(
-                        toggleType, userId, sensor, state, mHandler,
-                        changeSuccessful -> {
-                            if (changeSuccessful) {
-                                if (userId == mUserManagerInternal.getProfileParentId(userId)) {
-                                    mHandler.sendMessage(PooledLambda.obtainMessage(
-                                            SensorPrivacyServiceImpl::logSensorPrivacyStateToggle,
-                                            this,
-                                            source, sensor, state, lastChange[0], false));
-                                }
-                            }
-                        });
-            });
-        }
-
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        private void logSensorPrivacyStateToggle(int source, int sensor, int state,
-                long lastChange, boolean onShutDown) {
-            long logMins = Math.max(0, (getCurrentTimeMillis() - lastChange) / (1000 * 60));
-
-            int logAction = ACTION__ACTION_UNKNOWN;
-            if (!onShutDown) {
-                switch(state) {
-                    case ENABLED :
-                        logAction = ACTION__TOGGLE_OFF;
-                        break;
-                    case DISABLED :
-                        logAction = ACTION__TOGGLE_ON;
-                        break;
-                    case AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS :
-                        logAction = ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS;
-                        break;
-                    case AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS :
-                        logAction = ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS;
-                        break;
-                    case AUTOMOTIVE_DRIVER_ASSISTANCE_APPS :
-                        logAction = ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_APPS;
-                        break;
-                    default :
-                        logAction = ACTION__ACTION_UNKNOWN;
-                        break;
-                }
-            }
-
-            int logSensor = PRIVACY_SENSOR_TOGGLE_INTERACTION__SENSOR__SENSOR_UNKNOWN;
-            switch(sensor) {
-                case CAMERA:
-                    logSensor = PRIVACY_SENSOR_TOGGLE_INTERACTION__SENSOR__CAMERA;
-                    break;
-                case MICROPHONE:
-                    logSensor = PRIVACY_SENSOR_TOGGLE_INTERACTION__SENSOR__MICROPHONE;
-                    break;
-                default:
-                    logSensor = PRIVACY_SENSOR_TOGGLE_INTERACTION__SENSOR__SENSOR_UNKNOWN;
-                    break;
-            }
-
-            int logSource = PRIVACY_SENSOR_TOGGLE_INTERACTION__SOURCE__SOURCE_UNKNOWN;
-            switch(source) {
-                case QS_TILE :
-                    logSource = PRIVACY_SENSOR_TOGGLE_INTERACTION__SOURCE__QS_TILE;
-                    break;
-                case DIALOG :
-                    logSource = PRIVACY_SENSOR_TOGGLE_INTERACTION__SOURCE__DIALOG;
-                    break;
-                case SETTINGS:
-                    logSource = PRIVACY_SENSOR_TOGGLE_INTERACTION__SOURCE__SETTINGS;
-                    break;
-                default:
-                    logSource = PRIVACY_SENSOR_TOGGLE_INTERACTION__SOURCE__SOURCE_UNKNOWN;
-                    break;
-            }
-
-            if (DEBUG || DEBUG_LOGGING) {
-                Log.d(TAG, "Logging sensor toggle interaction:" + " logSensor=" + logSensor
-                        + " logAction=" + logAction + " logSource=" + logSource + " logMins="
-                        + logMins);
-            }
-            write(PRIVACY_SENSOR_TOGGLE_INTERACTION, logSensor, logAction, logSource, logMins);
-
-        }
-
-        @Override
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        @RequiresPermission(Manifest.permission.MANAGE_SENSOR_PRIVACY)
-        public void setToggleSensorPrivacyStateForProfileGroup(int userId, int source, int sensor,
-                int  state) {
-            enforceManageSensorPrivacyPermission();
-            if (userId == UserHandle.USER_CURRENT) {
-                userId = mCurrentUser;
-            }
-            int parentId = mUserManagerInternal.getProfileParentId(userId);
-            forAllUsers(userId2 -> {
-                if (parentId == mUserManagerInternal.getProfileParentId(userId2)) {
-                    setToggleSensorPrivacyState(userId2, source, sensor, state);
-                }
-            });
-        }
-
-        @Override
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
-        public List<CameraPrivacyAllowlistEntry> getCameraPrivacyAllowlist() {
-            enforceObserveSensorPrivacyPermission();
-            return mCameraPrivacyAllowlist;
-        }
-
-        @Override
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
-        public boolean isCameraPrivacyEnabled(String packageName) {
-            if (DEBUG) {
-                Log.d(TAG, "callingUid=" + Binder.getCallingUid()
-                        + " callingPid=" + Binder.getCallingPid()
-                        + " isCameraPrivacyEnabled("
-                        + "packageName=" + packageName
-                        + ")");
-            }
-            enforceObserveSensorPrivacyPermission();
-
-            int state =  mSensorPrivacyStateController.getState(TOGGLE_TYPE_SOFTWARE, mCurrentUser,
-                    CAMERA).getState();
-            if (state == ENABLED) {
-                return true;
-            } else if (state == DISABLED) {
-                return false;
-            } else if (state == AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS) {
-                for (CameraPrivacyAllowlistEntry entry : mCameraPrivacyAllowlist) {
-                    if ((packageName.equals(entry.packageName)) && !entry.isMandatory) {
-                        return false;
-                    }
-                }
-                return true;
-            } else if (state == AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS) {
-                for (CameraPrivacyAllowlistEntry entry : mCameraPrivacyAllowlist) {
-                    if ((packageName.equals(entry.packageName)) && entry.isMandatory) {
-                        return false;
-                    }
-                }
-                return true;
-            } else if (state == AUTOMOTIVE_DRIVER_ASSISTANCE_APPS) {
-                for (CameraPrivacyAllowlistEntry entry : mCameraPrivacyAllowlist) {
-                    if (packageName.equals(entry.packageName)) {
-                        return false;
-                    }
-                }
-                return true;
-            }
-            return false;
-        }
-
-        @Override
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
-        public int getToggleSensorPrivacyState(int toggleType, int sensor) {
-            if (DEBUG) {
-                Log.d(TAG, "callingUid=" + Binder.getCallingUid()
-                        + " callingPid=" + Binder.getCallingPid()
-                        + " getToggleSensorPrivacyState("
-                        + "toggleType=" + toggleType
-                        + " sensor=" + sensor
-                        + ")");
-            }
-            enforceObserveSensorPrivacyPermission();
-
-            return mSensorPrivacyStateController.getState(toggleType, mCurrentUser, sensor)
-                    .getState();
-        }
-
         private void setToggleSensorPrivacyUnchecked(int toggleType, int userId, int source,
                 int sensor, boolean enable) {
             if (DEBUG) {
@@ -1181,23 +899,16 @@
          * Enforces the caller contains the necessary permission to change the state of sensor
          * privacy.
          */
-        @RequiresPermission(Manifest.permission.MANAGE_SENSOR_PRIVACY)
         private void enforceManageSensorPrivacyPermission() {
-            if (mContext.checkCallingOrSelfPermission(
-                    android.Manifest.permission.MANAGE_SENSOR_PRIVACY) == PERMISSION_GRANTED) {
-                return;
-            }
-
-            String message = "Changing sensor privacy requires the following permission: "
-                    + MANAGE_SENSOR_PRIVACY;
-            throw new SecurityException(message);
+            enforcePermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY,
+                    "Changing sensor privacy requires the following permission: "
+                            + MANAGE_SENSOR_PRIVACY);
         }
 
         /**
          * Enforces the caller contains the necessary permission to observe changes to the sate of
          * sensor privacy.
          */
-        @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
         private void enforceObserveSensorPrivacyPermission() {
             String systemUIPackage = mContext.getString(R.string.config_systemUi);
             int systemUIAppId = UserHandle.getAppId(mPackageManagerInternal
@@ -1206,13 +917,15 @@
                 // b/221782106, possible race condition with role grant might bootloop device.
                 return;
             }
-            if (mContext.checkCallingOrSelfPermission(
-                    android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) == PERMISSION_GRANTED) {
+            enforcePermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY,
+                    "Observing sensor privacy changes requires the following permission: "
+                            + android.Manifest.permission.OBSERVE_SENSOR_PRIVACY);
+        }
+
+        private void enforcePermission(String permission, String message) {
+            if (mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) {
                 return;
             }
-
-            String message = "Observing sensor privacy changes requires the following permission: "
-                    + android.Manifest.permission.OBSERVE_SENSOR_PRIVACY;
             throw new SecurityException(message);
         }
 
@@ -1580,13 +1293,11 @@
         }
 
         @Override
-        @RequiresPermission(Manifest.permission.MANAGE_SENSOR_PRIVACY)
         public void onShellCommand(FileDescriptor in, FileDescriptor out,
                 FileDescriptor err, String[] args, ShellCallback callback,
                 ResultReceiver resultReceiver) {
             (new ShellCommand() {
                 @Override
-                @RequiresPermission(Manifest.permission.MANAGE_SENSOR_PRIVACY)
                 public int onCommand(String cmd) {
                     if (cmd == null) {
                         return handleDefaultCommands(cmd);
@@ -1616,45 +1327,6 @@
                             setToggleSensorPrivacy(userId, SHELL, sensor, false);
                         }
                         break;
-                        case "automotive_driver_assistance_apps" : {
-                            if (Flags.privacyAllowlist()) {
-                                int sensor = sensorStrToId(getNextArgRequired());
-                                if ((!isAutomotive(mContext)) || (sensor != CAMERA)) {
-                                    pw.println("Command not valid for this sensor");
-                                    return -1;
-                                }
-
-                                setToggleSensorPrivacyState(userId, SHELL, sensor,
-                                        AUTOMOTIVE_DRIVER_ASSISTANCE_APPS);
-                            }
-                        }
-                        break;
-                        case "automotive_driver_assistance_helpful_apps" : {
-                            if (Flags.privacyAllowlist()) {
-                                int sensor = sensorStrToId(getNextArgRequired());
-                                if ((!isAutomotive(mContext)) || (sensor != CAMERA)) {
-                                    pw.println("Command not valid for this sensor");
-                                    return -1;
-                                }
-
-                                setToggleSensorPrivacyState(userId, SHELL, sensor,
-                                        AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS);
-                            }
-                        }
-                        break;
-                        case "automotive_driver_assistance_required_apps" : {
-                            if (Flags.privacyAllowlist()) {
-                                int sensor = sensorStrToId(getNextArgRequired());
-                                if ((!isAutomotive(mContext)) || (sensor != CAMERA)) {
-                                    pw.println("Command not valid for this sensor");
-                                    return -1;
-                                }
-
-                                setToggleSensorPrivacyState(userId, SHELL, sensor,
-                                        AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS);
-                            }
-                        }
-                        break;
                         default:
                             return handleDefaultCommands(cmd);
                     }
@@ -1677,24 +1349,6 @@
                     pw.println("  disable USER_ID SENSOR");
                     pw.println("    Disable privacy for a certain sensor.");
                     pw.println("");
-                    if (Flags.privacyAllowlist()) {
-                        if (isAutomotive(mContext)) {
-                            pw.println("  automotive_driver_assistance_apps USER_ID SENSOR");
-                            pw.println("    Disable privacy for automotive apps which help you"
-                                    + " drive and apps which are required by OEM");
-                            pw.println("");
-                            pw.println("  automotive_driver_assistance_helpful_apps "
-                                    + "USER_ID SENSOR");
-                            pw.println("    Disable privacy for automotive apps which "
-                                    + "help you drive.");
-                            pw.println("");
-                            pw.println("  automotive_driver_assistance_required_apps "
-                                    + "USER_ID SENSOR");
-                            pw.println("    Disable privacy for automotive apps which are "
-                                    + "required by OEM.");
-                            pw.println("");
-                        }
-                    }
                 }
             }).exec(this, in, out, err, args, callback, resultReceiver);
         }
@@ -1803,38 +1457,6 @@
             mSensorPrivacyServiceImpl.showSensorStateChangedActivity(sensor, toggleType);
         }
 
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        public void handleSensorPrivacyChanged(int userId, int toggleType, int sensor,
-                int state) {
-            if (userId == mCurrentUser) {
-                mSensorPrivacyServiceImpl.setGlobalRestriction(sensor,
-                        mSensorPrivacyServiceImpl.isCombinedToggleSensorPrivacyEnabled(sensor));
-            }
-
-            if (userId != mCurrentUser) {
-                return;
-            }
-            synchronized (mListenerLock) {
-                try {
-                    final int count = mToggleSensorListeners.beginBroadcast();
-                    for (int i = 0; i < count; i++) {
-                        ISensorPrivacyListener listener = mToggleSensorListeners.getBroadcastItem(
-                                i);
-                        try {
-                            listener.onSensorPrivacyStateChanged(toggleType, sensor, state);
-                        } catch (RemoteException e) {
-                            Log.e(TAG, "Caught an exception notifying listener " + listener + ": ",
-                                    e);
-                        }
-                    }
-                } finally {
-                    mToggleSensorListeners.finishBroadcast();
-                }
-            }
-
-            mSensorPrivacyServiceImpl.showSensorStateChangedActivity(sensor, toggleType);
-        }
-
         public void removeSuppressPackageReminderToken(Pair<Integer, UserHandle> key,
                 IBinder token) {
             sendMessage(PooledLambda.obtainMessage(
diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateController.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateController.java
index 0e29222..9694958 100644
--- a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateController.java
+++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateController.java
@@ -16,11 +16,9 @@
 
 package com.android.server.sensorprivacy;
 
-import android.annotation.FlaggedApi;
 import android.os.Handler;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.camera.flags.Flags;
 import com.android.internal.util.dump.DualDumpOutputStream;
 import com.android.internal.util.function.pooled.PooledLambda;
 
@@ -53,14 +51,6 @@
         }
     }
 
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    void setState(int toggleType, int userId, int sensor, int state, Handler callbackHandler,
-            SetStateResultCallback callback) {
-        synchronized (mLock) {
-            setStateLocked(toggleType, userId, sensor, state, callbackHandler, callback);
-        }
-    }
-
     void setSensorPrivacyListener(Handler handler,
             SensorPrivacyListener listener) {
         synchronized (mLock) {
@@ -138,11 +128,6 @@
             Handler callbackHandler, SetStateResultCallback callback);
 
     @GuardedBy("mLock")
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    abstract void setStateLocked(int toggleType, int userId, int sensor, int state,
-            Handler callbackHandler, SetStateResultCallback callback);
-
-    @GuardedBy("mLock")
     abstract void setSensorPrivacyListenerLocked(Handler handler,
             SensorPrivacyListener listener);
 
diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateControllerImpl.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateControllerImpl.java
index 2d96aeb..3dcb4cf 100644
--- a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateControllerImpl.java
+++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateControllerImpl.java
@@ -16,12 +16,8 @@
 
 package com.android.server.sensorprivacy;
 
-import static android.hardware.SensorPrivacyManager.StateTypes.DISABLED;
-
-import android.annotation.FlaggedApi;
 import android.os.Handler;
 
-import com.android.internal.camera.flags.Flags;
 import com.android.internal.util.dump.DualDumpOutputStream;
 import com.android.internal.util.function.pooled.PooledLambda;
 
@@ -89,33 +85,6 @@
         sendSetStateCallback(callbackHandler, callback, false);
     }
 
-    @Override
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    void setStateLocked(int toggleType, int userId, int sensor, int state,
-            Handler callbackHandler, SetStateResultCallback callback) {
-        // Changing the SensorState's mEnabled updates the timestamp of its last change.
-        // A nonexistent state -> unmuted should not set the timestamp.
-        SensorState lastState = mPersistedState.getState(toggleType, userId, sensor);
-        if (lastState == null) {
-            if (state == DISABLED) {
-                sendSetStateCallback(callbackHandler, callback, false);
-                return;
-            } else {
-                SensorState sensorState = new SensorState(state);
-                mPersistedState.setState(toggleType, userId, sensor, sensorState);
-                notifyStateChangeLocked(toggleType, userId, sensor, sensorState);
-                sendSetStateCallback(callbackHandler, callback, true);
-                return;
-            }
-        }
-        if (lastState.setState(state)) {
-            notifyStateChangeLocked(toggleType, userId, sensor, lastState);
-            sendSetStateCallback(callbackHandler, callback, true);
-            return;
-        }
-        sendSetStateCallback(callbackHandler, callback, false);
-    }
-
     private void notifyStateChangeLocked(int toggleType, int userId, int sensor,
             SensorState sensorState) {
         if (mListenerHandler != null && mListener != null) {
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index dfa9dce..3607ddd 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -34,6 +34,7 @@
         "tvinput/BufferProducerThread.cpp",
         "tvinput/JTvInputHal.cpp",
         "tvinput/TvInputHal_hidl.cpp",
+        "com_android_server_accessibility_BrailleDisplayConnection.cpp",
         "com_android_server_adb_AdbDebuggingManager.cpp",
         "com_android_server_am_BatteryStatsService.cpp",
         "com_android_server_biometrics_SurfaceToNativeHandleConverter.cpp",
diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS
index df7fb99..b999305f 100644
--- a/services/core/jni/OWNERS
+++ b/services/core/jni/OWNERS
@@ -15,6 +15,7 @@
 per-file com_android_server_SystemClock* = file:/services/core/java/com/android/server/timedetector/OWNERS
 per-file com_android_server_Usb* = file:/services/usb/OWNERS
 per-file com_android_server_Vibrator* = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file com_android_server_accessibility_* = file:/services/accessibility/OWNERS
 per-file com_android_server_hdmi_* = file:/core/java/android/hardware/hdmi/OWNERS
 per-file com_android_server_lights_* = file:/services/core/java/com/android/server/lights/OWNERS
 per-file com_android_server_location_* = file:/location/java/android/location/OWNERS
diff --git a/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp b/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp
new file mode 100644
index 0000000..9a509a7
--- /dev/null
+++ b/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <core_jni_helpers.h>
+#include <jni.h>
+#include <linux/hidraw.h>
+#include <linux/input.h>
+#include <nativehelper/JNIHelp.h>
+#include <sys/ioctl.h>
+
+/*
+ * This file defines simple wrappers around the kernel UAPI HIDRAW driver's ioctl() commands.
+ * See kernel example samples/hidraw/hid-example.c
+ *
+ * All methods expect an open file descriptor int from Java.
+ */
+
+namespace android {
+
+namespace {
+
+// Max size we allow for the result from HIDIOCGRAWUNIQ (Bluetooth address or USB serial number).
+// Copied from linux/hid.h struct hid_device->uniq char array size; the ioctl implementation
+// writes at most this many bytes to the provided buffer.
+constexpr int UNIQ_SIZE_MAX = 64;
+
+} // anonymous namespace
+
+static jint com_android_server_accessibility_BrailleDisplayConnection_getHidrawDescSize(
+        JNIEnv* env, jobject thiz, int fd) {
+    int size = 0;
+    if (ioctl(fd, HIDIOCGRDESCSIZE, &size) < 0) {
+        return -1;
+    }
+    return size;
+}
+
+static jbyteArray com_android_server_accessibility_BrailleDisplayConnection_getHidrawDesc(
+        JNIEnv* env, jobject thiz, int fd, int descSize) {
+    struct hidraw_report_descriptor desc;
+    desc.size = descSize;
+    if (ioctl(fd, HIDIOCGRDESC, &desc) < 0) {
+        return nullptr;
+    }
+    jbyteArray result = env->NewByteArray(descSize);
+    if (result != nullptr) {
+        env->SetByteArrayRegion(result, 0, descSize, (jbyte*)desc.value);
+    }
+    // Local ref is not deleted because it is returned to Java
+    return result;
+}
+
+static jstring com_android_server_accessibility_BrailleDisplayConnection_getHidrawUniq(JNIEnv* env,
+                                                                                       jobject thiz,
+                                                                                       int fd) {
+    char buf[UNIQ_SIZE_MAX];
+    if (ioctl(fd, HIDIOCGRAWUNIQ(UNIQ_SIZE_MAX), buf) < 0) {
+        return nullptr;
+    }
+    // Local ref is not deleted because it is returned to Java
+    return env->NewStringUTF(buf);
+}
+
+static jint com_android_server_accessibility_BrailleDisplayConnection_getHidrawBusType(JNIEnv* env,
+                                                                                       jobject thiz,
+                                                                                       int fd) {
+    struct hidraw_devinfo info;
+    if (ioctl(fd, HIDIOCGRAWINFO, &info) < 0) {
+        return -1;
+    }
+    return info.bustype;
+}
+
+static const JNINativeMethod gMethods[] = {
+        {"nativeGetHidrawDescSize", "(I)I",
+         (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawDescSize},
+        {"nativeGetHidrawDesc", "(II)[B",
+         (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawDesc},
+        {"nativeGetHidrawUniq", "(I)Ljava/lang/String;",
+         (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawUniq},
+        {"nativeGetHidrawBusType", "(I)I",
+         (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawBusType},
+};
+
+int register_com_android_server_accessibility_BrailleDisplayConnection(JNIEnv* env) {
+    return RegisterMethodsOrDie(env, "com/android/server/accessibility/BrailleDisplayConnection",
+                                gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 5d1eb49..0936888 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -70,6 +70,7 @@
 int register_com_android_server_display_DisplayControl(JNIEnv* env);
 int register_com_android_server_SystemClockTime(JNIEnv* env);
 int register_android_server_display_smallAreaDetectionController(JNIEnv* env);
+int register_com_android_server_accessibility_BrailleDisplayConnection(JNIEnv* env);
 };
 
 using namespace android;
@@ -132,5 +133,6 @@
     register_com_android_server_display_DisplayControl(env);
     register_com_android_server_SystemClockTime(env);
     register_android_server_display_smallAreaDetectionController(env);
+    register_com_android_server_accessibility_BrailleDisplayConnection(env);
     return JNI_VERSION_1_4;
 }
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
index a8100af..66e0717 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
@@ -18,6 +18,10 @@
 
 import android.content.Context
 import android.content.Intent
+import android.content.UriRelativeFilter
+import android.content.UriRelativeFilterGroup
+import android.content.UriRelativeFilterGroupParcel
+import android.content.pm.Flags
 import android.content.pm.PackageManager
 import android.content.pm.verify.domain.DomainOwner
 import android.content.pm.verify.domain.DomainVerificationInfo
@@ -25,8 +29,10 @@
 import android.content.pm.verify.domain.DomainVerificationUserState
 import android.content.pm.verify.domain.IDomainVerificationManager
 import android.os.Build
-import android.os.PatternMatcher
+import android.os.Bundle
+import android.os.PatternMatcher.PATTERN_LITERAL
 import android.os.Process
+import android.platform.test.annotations.RequiresFlagsEnabled
 import android.util.ArraySet
 import android.util.SparseArray
 import com.android.internal.pm.parsing.pkg.AndroidPackageInternal
@@ -68,6 +74,63 @@
         private val DOMAIN_4 = "four.$DOMAIN_BASE"
     }
 
+    @RequiresFlagsEnabled(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS)
+    @Test
+    fun updateUriRelativeFilterGroups() {
+        val pkgWithDomains = mockPkgState(PKG_ONE, UUID_ONE, listOf(DOMAIN_1, DOMAIN_2))
+        val service = makeService(pkgWithDomains).apply {
+            addPackages(pkgWithDomains)
+        }
+
+        val bundle = service.getUriRelativeFilterGroups(PKG_ONE, listOf(DOMAIN_1, DOMAIN_2))
+        assertThat(bundle.keySet()).containsExactlyElementsIn(listOf(DOMAIN_1, DOMAIN_2))
+        assertThat(bundle.getParcelableArrayList(DOMAIN_1, UriRelativeFilterGroup::class.java))
+            .isEmpty()
+        assertThat(bundle.getParcelableArrayList(DOMAIN_2, UriRelativeFilterGroup::class.java))
+            .isEmpty()
+
+        val pathGroup = UriRelativeFilterGroup(UriRelativeFilterGroup.ACTION_ALLOW)
+        pathGroup.addUriRelativeFilter(
+            UriRelativeFilter(UriRelativeFilter.PATH, PATTERN_LITERAL, "path")
+        )
+        val queryGroup = UriRelativeFilterGroup(UriRelativeFilterGroup.ACTION_BLOCK)
+        queryGroup.addUriRelativeFilter(
+            UriRelativeFilter(UriRelativeFilter.QUERY, PATTERN_LITERAL, "query")
+        )
+        val fragmentGroup = UriRelativeFilterGroup(UriRelativeFilterGroup.ACTION_ALLOW)
+        fragmentGroup.addUriRelativeFilter(
+            UriRelativeFilter(UriRelativeFilter.FRAGMENT, PATTERN_LITERAL, "fragment")
+        )
+
+        assertGroups(service, arrayListOf(pathGroup))
+        assertGroups(service, arrayListOf(queryGroup, pathGroup))
+        assertGroups(service, arrayListOf(queryGroup, fragmentGroup, pathGroup))
+    }
+
+    private fun assertGroups(
+        service: DomainVerificationService,
+        groups: List<UriRelativeFilterGroup>
+    ) {
+        val bundle = Bundle()
+        bundle.putParcelableList(DOMAIN_1, UriRelativeFilterGroup.groupsToParcels(groups))
+        service.setUriRelativeFilterGroups(PKG_ONE, bundle)
+        val fetchedBundle = service.getUriRelativeFilterGroups(PKG_ONE, listOf(DOMAIN_1))
+        assertThat(fetchedBundle.keySet()).containsExactlyElementsIn(bundle.keySet())
+        assertThat(
+            UriRelativeFilterGroup.parcelsToGroups(
+                fetchedBundle.getParcelableArrayList(
+                    DOMAIN_1,
+                    UriRelativeFilterGroupParcel::class.java)
+            )
+        ).containsExactlyElementsIn(
+            UriRelativeFilterGroup.parcelsToGroups(
+                bundle.getParcelableArrayList(
+                    DOMAIN_1,
+                    UriRelativeFilterGroupParcel::class.java)
+            )
+        ).inOrder()
+    }
+
     @Test
     fun queryValidVerificationPackageNames() {
         val pkgWithDomains = mockPkgState(PKG_ONE, UUID_ONE, listOf(DOMAIN_1, DOMAIN_2))
@@ -484,6 +547,7 @@
         DomainVerificationService(mockThrowOnUnmocked {
             // Assume the test has every permission necessary
             whenever(enforcePermission(anyString(), anyInt(), anyInt(), anyString()))
+            whenever(enforceCallingOrSelfPermission(anyString(), anyString()))
             whenever(checkPermission(anyString(), anyInt(), anyInt())) {
                 PackageManager.PERMISSION_GRANTED
             }
@@ -539,7 +603,7 @@
                                     addCategory(Intent.CATEGORY_DEFAULT)
                                     addDataScheme("http")
                                     addDataScheme("https")
-                                    addDataPath("/sub", PatternMatcher.PATTERN_LITERAL)
+                                    addDataPath("/sub", PATTERN_LITERAL)
                                     addDataAuthority(it, null)
                                 }
                             }
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt
index 65b99c5..4fa4190 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt
@@ -16,7 +16,12 @@
 
 package com.android.server.pm.test.verify.domain
 
+import android.content.UriRelativeFilter
+import android.content.UriRelativeFilter.PATH
+import android.content.UriRelativeFilterGroup
+import android.content.UriRelativeFilterGroup.ACTION_ALLOW
 import android.content.pm.verify.domain.DomainVerificationState
+import android.os.PatternMatcher.PATTERN_LITERAL
 import android.os.UserHandle
 import android.util.ArrayMap
 import android.util.SparseArray
@@ -157,7 +162,7 @@
 
     @Test
     fun writeStateSignatureIfFunctionReturnsNull() {
-        val (attached, pending, restored) = mockWriteValues  { "SIGNATURE_$it" }
+        val (attached, pending, restored) = mockWriteValues { "SIGNATURE_$it" }
         val file = tempFolder.newFile().writeXml {
             DomainVerificationPersistence.writeToXml(it, attached, pending, restored,
                     UserHandle.USER_ALL) { null }
@@ -313,6 +318,9 @@
                     addHosts(setOf("$packageName-user.com"))
                     isLinkHandlingAllowed = true
                 }
+                val group = UriRelativeFilterGroup(ACTION_ALLOW)
+                group.addUriRelativeFilter(UriRelativeFilter(PATH, PATTERN_LITERAL, "test"))
+                uriRelativeFilterGroupMap.put("example.com", listOf(group))
             }
 
     private fun pkgName(id: Int) = "$PKG_PREFIX.pkg$id"
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
index ef80b59..f86cb7b 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
@@ -21,9 +21,12 @@
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
+import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -31,10 +34,14 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.Manifest;
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.accessibilityservice.AccessibilityTrace;
+import android.accessibilityservice.BrailleDisplayController;
 import android.accessibilityservice.GestureDescription;
 import android.accessibilityservice.IAccessibilityServiceClient;
+import android.accessibilityservice.IBrailleDisplayConnection;
+import android.accessibilityservice.IBrailleDisplayController;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -43,6 +50,9 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.hardware.display.DisplayManager;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
+import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -62,7 +72,9 @@
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 import java.util.Arrays;
@@ -94,18 +106,30 @@
     AccessibilityServiceInfo mServiceInfo;
     @Mock ResolveInfo mMockResolveInfo;
     @Mock AccessibilitySecurityPolicy mMockSecurityPolicy;
-    @Mock AccessibilityWindowManager mMockA11yWindowManager;
-    @Mock ActivityTaskManagerInternal mMockActivityTaskManagerInternal;
-    @Mock AbstractAccessibilityServiceConnection.SystemSupport mMockSystemSupport;
-    @Mock AccessibilityTrace mMockA11yTrace;
-    @Mock WindowManagerInternal mMockWindowManagerInternal;
-    @Mock SystemActionPerformer mMockSystemActionPerformer;
-    @Mock KeyEventDispatcher mMockKeyEventDispatcher;
+    @Mock
+    AccessibilityWindowManager mMockA11yWindowManager;
+    @Mock
+    ActivityTaskManagerInternal mMockActivityTaskManagerInternal;
+    @Mock
+    AbstractAccessibilityServiceConnection.SystemSupport mMockSystemSupport;
+    @Mock
+    AccessibilityTrace mMockA11yTrace;
+    @Mock
+    WindowManagerInternal mMockWindowManagerInternal;
+    @Mock
+    SystemActionPerformer mMockSystemActionPerformer;
+    @Mock
+    KeyEventDispatcher mMockKeyEventDispatcher;
     @Mock
     MagnificationProcessor mMockMagnificationProcessor;
-    @Mock IBinder mMockIBinder;
-    @Mock IAccessibilityServiceClient mMockServiceClient;
-    @Mock MotionEventInjector mMockMotionEventInjector;
+    @Mock
+    IBinder mMockIBinder;
+    @Mock
+    IAccessibilityServiceClient mMockServiceClient;
+    @Mock
+    IBrailleDisplayController mMockBrailleDisplayController;
+    @Mock
+    MotionEventInjector mMockMotionEventInjector;
 
     MessageCapturingHandler mHandler = new MessageCapturingHandler(null);
 
@@ -134,6 +158,7 @@
                 mMockWindowManagerInternal, mMockSystemActionPerformer,
                 mMockA11yWindowManager, mMockActivityTaskManagerInternal);
         when(mMockSecurityPolicy.canPerformGestures(mConnection)).thenReturn(true);
+        when(mMockSecurityPolicy.checkAccessibilityAccess(mConnection)).thenReturn(true);
     }
 
     @After
@@ -291,6 +316,119 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectBluetoothBrailleDisplay() throws Exception {
+        final String macAddress = "00:11:22:33:AA:BB";
+        final byte[] descriptor = {0x05, 0x41};
+        Bundle bd = new Bundle();
+        bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, "/dev/null");
+        bd.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, descriptor);
+        bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, macAddress);
+        bd.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, true);
+        mConnection.setTestBrailleDisplayData(List.of(bd));
+
+        mConnection.connectBluetoothBrailleDisplay(macAddress, mMockBrailleDisplayController);
+
+        ArgumentCaptor<IBrailleDisplayConnection> connection =
+                ArgumentCaptor.forClass(IBrailleDisplayConnection.class);
+        verify(mMockBrailleDisplayController).onConnected(connection.capture(), eq(descriptor));
+        // Cleanup the connection.
+        connection.getValue().disconnect();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectBluetoothBrailleDisplay_throwsForMissingBluetoothConnectPermission() {
+        doThrow(SecurityException.class).when(mMockContext)
+                .enforceCallingPermission(eq(Manifest.permission.BLUETOOTH_CONNECT), any());
+
+        assertThrows(SecurityException.class,
+                () -> mConnection.connectBluetoothBrailleDisplay("unused",
+                        mMockBrailleDisplayController));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectBluetoothBrailleDisplay_throwsForNullMacAddress() {
+        assertThrows(NullPointerException.class,
+                () -> mConnection.connectBluetoothBrailleDisplay(null,
+                        mMockBrailleDisplayController));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectBluetoothBrailleDisplay_throwsForMisformattedMacAddress() {
+        assertThrows(IllegalArgumentException.class,
+                () -> mConnection.connectBluetoothBrailleDisplay("12:34",
+                        mMockBrailleDisplayController));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectUsbBrailleDisplay() throws Exception {
+        final String serialNumber = "myUsbDevice";
+        final byte[] descriptor = {0x05, 0x41};
+        Bundle bd = new Bundle();
+        bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, "/dev/null");
+        bd.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, descriptor);
+        bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, serialNumber);
+        bd.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, false);
+        mConnection.setTestBrailleDisplayData(List.of(bd));
+        UsbDevice usbDevice = Mockito.mock(UsbDevice.class);
+        when(usbDevice.getSerialNumber()).thenReturn(serialNumber);
+        UsbManager usbManager = Mockito.mock(UsbManager.class);
+        when(mMockContext.getSystemService(Context.USB_SERVICE)).thenReturn(usbManager);
+        when(usbManager.hasPermission(eq(usbDevice), eq(COMPONENT_NAME.getPackageName()),
+                anyInt(), anyInt())).thenReturn(true);
+
+        mConnection.connectUsbBrailleDisplay(usbDevice, mMockBrailleDisplayController);
+
+        ArgumentCaptor<IBrailleDisplayConnection> connection =
+                ArgumentCaptor.forClass(IBrailleDisplayConnection.class);
+        verify(mMockBrailleDisplayController).onConnected(connection.capture(), eq(descriptor));
+        // Cleanup the connection.
+        connection.getValue().disconnect();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectUsbBrailleDisplay_throwsForMissingUsbPermission() {
+        UsbManager usbManager = Mockito.mock(UsbManager.class);
+        when(mMockContext.getSystemService(Context.USB_SERVICE)).thenReturn(usbManager);
+        when(usbManager.hasPermission(notNull(), eq(COMPONENT_NAME.getPackageName()),
+                anyInt(), anyInt())).thenReturn(false);
+
+        assertThrows(SecurityException.class,
+                () -> mConnection.connectUsbBrailleDisplay(Mockito.mock(UsbDevice.class),
+                        mMockBrailleDisplayController));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectUsbBrailleDisplay_throwsForNullDevice() {
+        assertThrows(NullPointerException.class,
+                () -> mConnection.connectUsbBrailleDisplay(null, mMockBrailleDisplayController));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectUsbBrailleDisplay_callsOnConnectionFailedForEmptySerialNumber()
+            throws Exception {
+        UsbManager usbManager = Mockito.mock(UsbManager.class);
+        when(mMockContext.getSystemService(Context.USB_SERVICE)).thenReturn(usbManager);
+        when(usbManager.hasPermission(notNull(), eq(COMPONENT_NAME.getPackageName()),
+                anyInt(), anyInt())).thenReturn(true);
+        UsbDevice usbDevice = Mockito.mock(UsbDevice.class);
+        when(usbDevice.getSerialNumber()).thenReturn("");
+
+        mConnection.connectUsbBrailleDisplay(usbDevice, mMockBrailleDisplayController);
+
+        verify(mMockBrailleDisplayController).onConnectionFailed(
+                BrailleDisplayController.BrailleDisplayCallback
+                        .FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND);
+    }
+
+    @Test
     @RequiresFlagsEnabled(Flags.FLAG_RESETTABLE_DYNAMIC_PROPERTIES)
     public void binderDied_resetA11yServiceInfo() {
         final int flag = AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS;
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java
new file mode 100644
index 0000000..7c278ce
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.accessibilityservice.BrailleDisplayController;
+import android.os.Bundle;
+import android.testing.DexmakerShareClassLoaderRule;
+
+import com.google.common.truth.Expect;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.nio.file.Path;
+import java.util.List;
+
+/**
+ * Tests for internal details of {@link BrailleDisplayConnection}.
+ *
+ * <p>Prefer adding new tests in CTS where possible.
+ */
+public class BrailleDisplayConnectionTest {
+    private static final Path NULL_PATH = Path.of("/dev/null");
+
+    private BrailleDisplayConnection mBrailleDisplayConnection;
+    @Mock
+    private BrailleDisplayConnection.NativeInterface mNativeInterface;
+    @Mock
+    private AccessibilityServiceConnection mServiceConnection;
+
+    @Rule
+    public final Expect expect = Expect.create();
+
+    // To mock package-private class
+    @Rule
+    public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
+            new DexmakerShareClassLoaderRule();
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mBrailleDisplayConnection = new BrailleDisplayConnection(new Object(), mServiceConnection);
+    }
+
+    @Test
+    public void defaultNativeScanner_getReportDescriptor_returnsDescriptor() {
+        int descriptorSize = 4;
+        byte[] descriptor = {0xB, 0xE, 0xE, 0xF};
+        when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(descriptorSize);
+        when(mNativeInterface.getHidrawDesc(anyInt(), eq(descriptorSize))).thenReturn(descriptor);
+
+        BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+        assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isEqualTo(descriptor);
+    }
+
+    @Test
+    public void defaultNativeScanner_getReportDescriptor_invalidSize_returnsNull() {
+        when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(0);
+
+        BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+        assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isNull();
+    }
+
+    @Test
+    public void defaultNativeScanner_getUniqueId_returnsUniq() {
+        String macAddress = "12:34:56:78";
+        when(mNativeInterface.getHidrawUniq(anyInt())).thenReturn(macAddress);
+
+        BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+        assertThat(scanner.getUniqueId(NULL_PATH)).isEqualTo(macAddress);
+    }
+
+    @Test
+    public void defaultNativeScanner_getDeviceBusType_busUsb() {
+        when(mNativeInterface.getHidrawBusType(anyInt()))
+                .thenReturn(BrailleDisplayConnection.BUS_USB);
+
+        BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+        assertThat(scanner.getDeviceBusType(NULL_PATH))
+                .isEqualTo(BrailleDisplayConnection.BUS_USB);
+    }
+
+    @Test
+    public void defaultNativeScanner_getDeviceBusType_busBluetooth() {
+        when(mNativeInterface.getHidrawBusType(anyInt()))
+                .thenReturn(BrailleDisplayConnection.BUS_BLUETOOTH);
+
+        BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+        assertThat(scanner.getDeviceBusType(NULL_PATH))
+                .isEqualTo(BrailleDisplayConnection.BUS_BLUETOOTH);
+    }
+
+    // BrailleDisplayConnection#setTestData() is used to enable CTS testing with
+    // test Braille display data, but its own implementation should also be tested
+    // so that issues in this helper don't cause confusing failures in CTS.
+    @Test
+    public void setTestData_scannerReturnsTestData() {
+        Bundle bd1 = new Bundle(), bd2 = new Bundle();
+
+        Path path1 = Path.of("/dev/path1"), path2 = Path.of("/dev/path2");
+        bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, path1.toString());
+        bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, path2.toString());
+        byte[] desc1 = {0xB, 0xE}, desc2 = {0xE, 0xF};
+        bd1.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc1);
+        bd2.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc2);
+        String uniq1 = "uniq1", uniq2 = "uniq2";
+        bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq1);
+        bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq2);
+        int bus1 = BrailleDisplayConnection.BUS_USB, bus2 = BrailleDisplayConnection.BUS_BLUETOOTH;
+        bd1.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH,
+                bus1 == BrailleDisplayConnection.BUS_BLUETOOTH);
+        bd2.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH,
+                bus2 == BrailleDisplayConnection.BUS_BLUETOOTH);
+
+        BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                mBrailleDisplayConnection.setTestData(List.of(bd1, bd2));
+
+        expect.that(scanner.getHidrawNodePaths()).containsExactly(path1, path2);
+        expect.that(scanner.getDeviceReportDescriptor(path1)).isEqualTo(desc1);
+        expect.that(scanner.getDeviceReportDescriptor(path2)).isEqualTo(desc2);
+        expect.that(scanner.getUniqueId(path1)).isEqualTo(uniq1);
+        expect.that(scanner.getUniqueId(path2)).isEqualTo(uniq2);
+        expect.that(scanner.getDeviceBusType(path1)).isEqualTo(bus1);
+        expect.that(scanner.getDeviceBusType(path2)).isEqualTo(bus2);
+    }
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
index 97e09b8..06eeb47c 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
@@ -49,6 +49,7 @@
 class HostStubGen(val options: HostStubGenOptions) {
     fun run() {
         val errors = HostStubGenErrors()
+        val stats = HostStubGenStats()
 
         // Load all classes.
         val allClasses = loadClassStructures(options.inJar.get)
@@ -80,7 +81,14 @@
                 options.enableClassChecker.get,
                 allClasses,
                 errors,
+                stats,
         )
+
+        // Dump statistics, if specified.
+        options.statsFile.ifSet {
+            PrintWriter(it).use { pw -> stats.dump(pw) }
+            log.i("Dump file created at $it")
+        }
     }
 
     /**
@@ -237,6 +245,7 @@
             enableChecker: Boolean,
             classes: ClassNodes,
             errors: HostStubGenErrors,
+            stats: HostStubGenStats,
             ) {
         log.i("Converting %s into [stub: %s, impl: %s] ...", inJar, outStubJar, outImplJar)
         log.i("ASM CheckClassAdapter is %s", if (enableChecker) "enabled" else "disabled")
@@ -254,7 +263,8 @@
                         while (inEntries.hasMoreElements()) {
                             val entry = inEntries.nextElement()
                             convertSingleEntry(inZip, entry, stubOutStream, implOutStream,
-                                    filter, packageRedirector, enableChecker, classes, errors)
+                                    filter, packageRedirector, enableChecker, classes, errors,
+                                    stats)
                         }
                         log.i("Converted all entries.")
                     }
@@ -287,6 +297,7 @@
             enableChecker: Boolean,
             classes: ClassNodes,
             errors: HostStubGenErrors,
+            stats: HostStubGenStats,
             ) {
         log.d("Entry: %s", entry.name)
         log.withIndent {
@@ -300,7 +311,7 @@
             // If it's a class, convert it.
             if (name.endsWith(".class")) {
                 processSingleClass(inZip, entry, stubOutStream, implOutStream, filter,
-                        packageRedirector, enableChecker, classes, errors)
+                        packageRedirector, enableChecker, classes, errors, stats)
                 return
             }
 
@@ -354,6 +365,7 @@
             enableChecker: Boolean,
             classes: ClassNodes,
             errors: HostStubGenErrors,
+            stats: HostStubGenStats,
             ) {
         val classInternalName = entry.name.replaceFirst("\\.class$".toRegex(), "")
         val classPolicy = filter.getPolicyForClass(classInternalName)
@@ -370,7 +382,7 @@
                     stubOutStream.putNextEntry(newEntry)
                     convertClass(classInternalName, /*forImpl=*/false, bis,
                             stubOutStream, filter, packageRedirector, enableChecker, classes,
-                            errors)
+                            errors, stats)
                     stubOutStream.closeEntry()
                 }
             }
@@ -383,7 +395,7 @@
                     implOutStream.putNextEntry(newEntry)
                     convertClass(classInternalName, /*forImpl=*/true, bis,
                             implOutStream, filter, packageRedirector, enableChecker, classes,
-                            errors)
+                            errors, stats)
                     implOutStream.closeEntry()
                 }
             }
@@ -403,6 +415,7 @@
             enableChecker: Boolean,
             classes: ClassNodes,
             errors: HostStubGenErrors,
+            stats: HostStubGenStats,
             ) {
         val cr = ClassReader(input)
 
@@ -420,6 +433,7 @@
                 enablePostTrace = options.enablePostTrace.get,
                 enableNonStubMethodCallDetection = options.enableNonStubMethodCallDetection.get,
                 errors = errors,
+                stats = stats,
         )
         outVisitor = BaseAdapter.getVisitor(classInternalName, classes, outVisitor, filter,
                 packageRedirector, forImpl, visitorOptions)
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
index d2ead18..9f5d524 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
@@ -108,6 +108,8 @@
         var enablePostTrace: SetOnce<Boolean> = SetOnce(false),
 
         var enableNonStubMethodCallDetection: SetOnce<Boolean> = SetOnce(false),
+
+        var statsFile: SetOnce<String?> = SetOnce(null),
 ) {
     companion object {
 
@@ -252,6 +254,8 @@
                         "--verbose-log" -> setLogFile(LogLevel.Verbose, nextArg())
                         "--debug-log" -> setLogFile(LogLevel.Debug, nextArg())
 
+                        "--stats-file" -> ret.statsFile.setNextStringArg()
+
                         else -> throw ArgumentsException("Unknown option: $arg")
                     }
                 } catch (e: SetOnce.SetMoreThanOnceException) {
@@ -387,6 +391,7 @@
               enablePreTrace=$enablePreTrace,
               enablePostTrace=$enablePostTrace,
               enableNonStubMethodCallDetection=$enableNonStubMethodCallDetection,
+              statsFile=$statsFile,
             }
             """.trimIndent()
     }
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt
new file mode 100644
index 0000000..fe4072f
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.hoststubgen
+
+import com.android.hoststubgen.asm.toHumanReadableClassName
+import com.android.hoststubgen.filters.FilterPolicyWithReason
+import java.io.PrintWriter
+
+open class HostStubGenStats {
+    data class Stats(
+            var supported: Int = 0,
+            var total: Int = 0,
+            val children: MutableMap<String, Stats> = mutableMapOf<String, Stats>(),
+    )
+
+    private val stats = mutableMapOf<String, Stats>()
+
+    fun onVisitPolicyForMethod(fullClassName: String, policy: FilterPolicyWithReason) {
+        if (policy.isIgnoredForStats) return
+
+        val packageName = resolvePackageName(fullClassName)
+        val className = resolveClassName(fullClassName)
+
+        val packageStats = stats.getOrPut(packageName) { Stats() }
+        val classStats = packageStats.children.getOrPut(className) { Stats() }
+
+        if (policy.policy.isSupported) {
+            packageStats.supported += 1
+            classStats.supported += 1
+        }
+        packageStats.total += 1
+        classStats.total += 1
+    }
+
+    fun dump(pw: PrintWriter) {
+        pw.printf("PackageName,ClassName,SupportedMethods,TotalMethods\n")
+        stats.forEach { (packageName, packageStats) ->
+            if (packageStats.supported > 0) {
+                packageStats.children.forEach { (className, classStats) ->
+                    pw.printf("%s,%s,%d,%d\n", packageName, className,
+                            classStats.supported, classStats.total)
+                }
+            }
+        }
+    }
+
+    private fun resolvePackageName(fullClassName: String): String {
+        val start = fullClassName.lastIndexOf('/')
+        return fullClassName.substring(0, start).toHumanReadableClassName()
+    }
+
+    private fun resolveClassName(fullClassName: String): String {
+        val start = fullClassName.lastIndexOf('/')
+        val end = fullClassName.indexOf('$')
+        if (end == -1) {
+            return fullClassName.substring(start + 1)
+        } else {
+            return fullClassName.substring(start + 1, end)
+        }
+    }
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt
index 9317996..4d21106 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt
@@ -111,6 +111,16 @@
             }
         }
 
+    /** Returns whether a policy is considered supported. */
+    val isSupported: Boolean
+        get() {
+            return when (this) {
+                // TODO: handle native method with no substitution as being unsupported
+                Stub, StubClass, Keep, KeepClass, SubstituteAndStub, SubstituteAndKeep -> true
+                else -> false
+            }
+        }
+
     fun getSubstitutionBasePolicy(): FilterPolicy {
         return when (this) {
             SubstituteAndKeep -> Keep
@@ -136,4 +146,4 @@
     fun withReason(reason: String): FilterPolicyWithReason {
         return FilterPolicyWithReason(this, reason)
     }
-}
\ No newline at end of file
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
index b64a2f5..53eb5a8 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
@@ -63,4 +63,15 @@
     override fun toString(): String {
         return "[$policy - reason: $reason]"
     }
-}
\ No newline at end of file
+
+    /** Returns whether this policy should be ignored for stats. */
+    val isIgnoredForStats: Boolean
+        get() {
+            return reason.contains("anonymous-inner-class")
+                    || reason.contains("is-annotation")
+                    || reason.contains("is-enum")
+                    || reason.contains("is-synthetic-method")
+                    || reason.contains("special-class")
+                    || reason.contains("substitute-from")
+        }
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
index ea7d1d0..78b13fd 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
@@ -119,14 +119,14 @@
             if (cn.isEnum()) {
                 mn?.let { mn ->
                     if (isAutoGeneratedEnumMember(mn)) {
-                        return memberPolicy.withReason(classPolicy.reason).wrapReason("enum")
+                        return memberPolicy.withReason(classPolicy.reason).wrapReason("is-enum")
                     }
                 }
             }
 
             // Keep (or stub) all members of annotations.
             if (cn.isAnnotation()) {
-                return memberPolicy.withReason(classPolicy.reason).wrapReason("annotation")
+                return memberPolicy.withReason(classPolicy.reason).wrapReason("is-annotation")
             }
 
             mn?.let {
@@ -134,7 +134,7 @@
                     // For synthetic methods (such as lambdas), let's just inherit the class's
                     // policy.
                     return memberPolicy.withReason(classPolicy.reason).wrapReason(
-                            "synthetic method")
+                            "is-synthetic-method")
                 }
             }
         }
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
index 7fdd944..6ad83fb 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
@@ -132,23 +132,24 @@
                                             throw ParseException(
                                                     "Policy for AIDL classes already defined")
                                         }
-                                        aidlPolicy = policy.withReason("$FILTER_REASON (AIDL)")
+                                        aidlPolicy = policy.withReason(
+                                                "$FILTER_REASON (special-class AIDL)")
                                     }
                                     SpecialClass.FeatureFlags -> {
                                         if (featureFlagsPolicy != null) {
                                             throw ParseException(
                                                     "Policy for feature flags already defined")
                                         }
-                                        featureFlagsPolicy =
-                                                policy.withReason("$FILTER_REASON (feature flags)")
+                                        featureFlagsPolicy = policy.withReason(
+                                                "$FILTER_REASON (special-class feature flags)")
                                     }
                                     SpecialClass.Sysprops -> {
                                         if (syspropsPolicy != null) {
                                             throw ParseException(
                                                     "Policy for sysprops already defined")
                                         }
-                                        syspropsPolicy =
-                                                policy.withReason("$FILTER_REASON (sysprops)")
+                                        syspropsPolicy = policy.withReason(
+                                                "$FILTER_REASON (special-class sysprops)")
                                     }
                                 }
                             }
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
index 21cfd4b..c20aa8b 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
@@ -16,6 +16,7 @@
 package com.android.hoststubgen.visitors
 
 import com.android.hoststubgen.HostStubGenErrors
+import com.android.hoststubgen.HostStubGenStats
 import com.android.hoststubgen.LogLevel
 import com.android.hoststubgen.asm.ClassNodes
 import com.android.hoststubgen.asm.UnifiedVisitor
@@ -50,6 +51,7 @@
      */
     data class Options (
             val errors: HostStubGenErrors,
+            val stats: HostStubGenStats,
             val enablePreTrace: Boolean,
             val enablePostTrace: Boolean,
             val enableNonStubMethodCallDetection: Boolean,
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
index 416b782..beca945 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
@@ -141,6 +141,11 @@
             substituted: Boolean,
             superVisitor: MethodVisitor?,
     ): MethodVisitor? {
+        // Record statistics about visiting this method when visible.
+        if ((access and Opcodes.ACC_PRIVATE) == 0) {
+            options.stats.onVisitPolicyForMethod(currentClassName, policy)
+        }
+
         // Inject method log, if needed.
         var innerVisitor = superVisitor