Merge "Add a flag to guard new MessageQueue test apis" into main
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 5c0b227..b7a82cd 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -5293,6 +5293,11 @@
     method @NonNull public android.hardware.contexthub.HubServiceInfo build();
   }
 
+  @FlaggedApi("android.chre.flags.offload_api") public interface IHubEndpointDiscoveryCallback {
+    method public void onEndpointsStarted(@NonNull java.util.List<android.hardware.contexthub.HubDiscoveryInfo>);
+    method public void onEndpointsStopped(@NonNull java.util.List<android.hardware.contexthub.HubDiscoveryInfo>);
+  }
+
   @FlaggedApi("android.chre.flags.offload_api") public interface IHubEndpointLifecycleCallback {
     method public void onSessionClosed(@NonNull android.hardware.contexthub.HubEndpointSession, int);
     method @NonNull public android.hardware.contexthub.HubEndpointSessionResult onSessionOpenRequest(@NonNull android.hardware.contexthub.HubEndpointInfo, @Nullable android.hardware.contexthub.HubServiceInfo);
@@ -6315,11 +6320,16 @@
     method @Deprecated public int registerCallback(@NonNull android.hardware.location.ContextHubManager.Callback);
     method @Deprecated public int registerCallback(android.hardware.location.ContextHubManager.Callback, android.os.Handler);
     method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void registerEndpoint(@NonNull android.hardware.contexthub.HubEndpoint);
+    method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void registerEndpointDiscoveryCallback(long, @NonNull android.hardware.contexthub.IHubEndpointDiscoveryCallback);
+    method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void registerEndpointDiscoveryCallback(long, @NonNull android.hardware.contexthub.IHubEndpointDiscoveryCallback, @NonNull java.util.concurrent.Executor);
+    method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void registerEndpointDiscoveryCallback(@NonNull String, @NonNull android.hardware.contexthub.IHubEndpointDiscoveryCallback);
+    method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void registerEndpointDiscoveryCallback(@NonNull String, @NonNull android.hardware.contexthub.IHubEndpointDiscoveryCallback, @NonNull java.util.concurrent.Executor);
     method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public int sendMessage(int, int, @NonNull android.hardware.location.ContextHubMessage);
     method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public int unloadNanoApp(int);
     method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public android.hardware.location.ContextHubTransaction<java.lang.Void> unloadNanoApp(@NonNull android.hardware.location.ContextHubInfo, long);
     method @Deprecated public int unregisterCallback(@NonNull android.hardware.location.ContextHubManager.Callback);
     method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void unregisterEndpoint(@NonNull android.hardware.contexthub.HubEndpoint);
+    method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void unregisterEndpointDiscoveryCallback(@NonNull android.hardware.contexthub.IHubEndpointDiscoveryCallback);
     field public static final int AUTHORIZATION_DENIED = 0; // 0x0
     field public static final int AUTHORIZATION_DENIED_GRACE_PERIOD = 1; // 0x1
     field public static final int AUTHORIZATION_GRANTED = 2; // 0x2
diff --git a/core/java/android/companion/CompanionDeviceService.java b/core/java/android/companion/CompanionDeviceService.java
index 5ad2348..db080fc 100644
--- a/core/java/android/companion/CompanionDeviceService.java
+++ b/core/java/android/companion/CompanionDeviceService.java
@@ -249,7 +249,7 @@
 
     // TODO(b/315163162) Add @Deprecated keyword after 24Q2 cut.
     /**
-     * Called by system whenever a device associated with this app is connected.
+     * Called by the system when an associated device is nearby or connected.
      *
      * @param associationInfo A record for the companion device.
      */
@@ -262,7 +262,7 @@
 
     // TODO(b/315163162) Add @Deprecated keyword after 24Q2 cut.
     /**
-     * Called by system whenever a device associated with this app is disconnected.
+     * Called by the system when an associated device is out of range or disconnected.
      *
      * @param associationInfo A record for the companion device.
      */
@@ -274,7 +274,7 @@
     }
 
     /**
-     * Called by the system during device events.
+     * Called by the system when an associated device's presence state changes.
      *
      * @see CompanionDeviceManager#startObservingDevicePresence(ObservingDevicePresenceRequest)
      */
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 02eed1a..3d2d487 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -7720,7 +7720,6 @@
     @IntDef(flag = true, prefix = { "EXTENDED_FLAG_" }, value = {
             EXTENDED_FLAG_FILTER_MISMATCH,
             EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN,
-            EXTENDED_FLAG_NESTED_INTENT_KEYS_COLLECTED,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ExtendedFlags {}
@@ -7741,13 +7740,6 @@
      */
     public static final int EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN = 1 << 1;
 
-    /**
-     * This flag indicates this intent called {@link #collectExtraIntentKeys()}.
-     *
-     * @hide
-     */
-    public static final int EXTENDED_FLAG_NESTED_INTENT_KEYS_COLLECTED = 1 << 2;
-
     // ---------------------------------------------------------------------
     // ---------------------------------------------------------------------
     // toUri() and parseUri() options.
@@ -12336,8 +12328,7 @@
     }
 
     private void collectNestedIntentKeysRecur(Set<Intent> visited) {
-        addExtendedFlags(EXTENDED_FLAG_NESTED_INTENT_KEYS_COLLECTED);
-        if (mExtras != null && !mExtras.isEmpty()) {
+        if (mExtras != null && !mExtras.isParcelled() && !mExtras.isEmpty()) {
             for (String key : mExtras.keySet()) {
                 Object value = mExtras.get(key);
 
diff --git a/core/java/android/hardware/contexthub/IContextHubEndpointDiscoveryCallback.aidl b/core/java/android/hardware/contexthub/IContextHubEndpointDiscoveryCallback.aidl
new file mode 100644
index 0000000..85775c0
--- /dev/null
+++ b/core/java/android/hardware/contexthub/IContextHubEndpointDiscoveryCallback.aidl
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.contexthub;
+
+import android.hardware.contexthub.HubEndpointInfo;
+
+/**
+ * @hide
+ */
+oneway interface IContextHubEndpointDiscoveryCallback {
+    /**
+     * Called when endpoint(s) start.
+     * @param hubEndpointInfoList The list of endpoints that started.
+     */
+    void onEndpointsStarted(in HubEndpointInfo[] hubEndpointInfoList);
+
+    /**
+     * Called when endpoint(s) stopped.
+     * @param hubEndpointInfoList The list of endpoints that started.
+     */
+    void onEndpointsStopped(in HubEndpointInfo[] hubEndpointInfoList);
+}
diff --git a/core/java/android/hardware/contexthub/IHubEndpointDiscoveryCallback.java b/core/java/android/hardware/contexthub/IHubEndpointDiscoveryCallback.java
new file mode 100644
index 0000000..0b77ddb
--- /dev/null
+++ b/core/java/android/hardware/contexthub/IHubEndpointDiscoveryCallback.java
@@ -0,0 +1,48 @@
+/*
+ * 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.hardware.contexthub;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.chre.flags.Flags;
+
+import java.util.List;
+
+/**
+ * Interface for listening to updates about endpoint availability.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_OFFLOAD_API)
+public interface IHubEndpointDiscoveryCallback {
+    /**
+     * Called when a list of hub endpoints have started.
+     *
+     * @param discoveryInfoList The list containing hub discovery information.
+     */
+    void onEndpointsStarted(@NonNull List<HubDiscoveryInfo> discoveryInfoList);
+
+    /**
+     * Called when a list of hub endpoints have stopped.
+     *
+     * @param discoveryInfoList The list containing hub discovery information.
+     */
+    // TODO(b/375487784): Add endpoint stop reason
+    void onEndpointsStopped(@NonNull List<HubDiscoveryInfo> discoveryInfoList);
+}
diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java
index 426cd69f..5e8a187 100644
--- a/core/java/android/hardware/location/ContextHubManager.java
+++ b/core/java/android/hardware/location/ContextHubManager.java
@@ -38,6 +38,8 @@
 import android.hardware.contexthub.HubEndpoint;
 import android.hardware.contexthub.HubEndpointInfo;
 import android.hardware.contexthub.HubServiceInfo;
+import android.hardware.contexthub.IContextHubEndpointDiscoveryCallback;
+import android.hardware.contexthub.IHubEndpointDiscoveryCallback;
 import android.hardware.contexthub.IHubEndpointLifecycleCallback;
 import android.os.Handler;
 import android.os.HandlerExecutor;
@@ -49,7 +51,9 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
 
 /**
@@ -202,6 +206,10 @@
     private Callback mCallback;
     private Handler mCallbackHandler;
 
+    /** A map of endpoint discovery callbacks currently registered */
+    private Map<IHubEndpointDiscoveryCallback, IContextHubEndpointDiscoveryCallback>
+            mDiscoveryCallbacks = new ConcurrentHashMap<>();
+
     /**
      * @deprecated Use {@code mCallback} instead.
      */
@@ -694,8 +702,6 @@
     @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @NonNull
     public List<HubDiscoveryInfo> findEndpoints(long endpointId) {
-        // TODO(b/379323274): Consider improving these getters to avoid racing with nano app load
-        //  timing.
         try {
             List<HubEndpointInfo> endpointInfos = mService.findEndpoints(endpointId);
             List<HubDiscoveryInfo> results = new ArrayList<>(endpointInfos.size());
@@ -720,8 +726,6 @@
     @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @NonNull
     public List<HubDiscoveryInfo> findEndpoints(@NonNull String serviceDescriptor) {
-        // TODO(b/379323274): Consider improving these getters to avoid racing with nano app load
-        //  timing.
         if (serviceDescriptor.isBlank()) {
             throw new IllegalArgumentException("Invalid service descriptor: " + serviceDescriptor);
         }
@@ -744,6 +748,188 @@
     }
 
     /**
+     * Creates an interface to invoke endpoint discovery callbacks to send down to the service.
+     *
+     * @param callback the callback to invoke at the client process
+     * @param executor the executor to invoke callbacks for this client
+     * @return the callback interface
+     */
+    private IContextHubEndpointDiscoveryCallback createDiscoveryCallback(
+            IHubEndpointDiscoveryCallback callback,
+            Executor executor,
+            @Nullable String serviceDescriptor) {
+        return new IContextHubEndpointDiscoveryCallback.Stub() {
+            @Override
+            public void onEndpointsStarted(HubEndpointInfo[] hubEndpointInfoList) {
+                if (hubEndpointInfoList.length == 0) {
+                    Log.w(TAG, "onEndpointsStarted: received empty discovery list");
+                    return;
+                }
+                executor.execute(
+                        () -> {
+                            // TODO(b/380293951): Refactor
+                            List<HubDiscoveryInfo> discoveryList =
+                                    new ArrayList<>(hubEndpointInfoList.length);
+                            for (HubEndpointInfo info : hubEndpointInfoList) {
+                                if (serviceDescriptor != null) {
+                                    for (HubServiceInfo sInfo : info.getServiceInfoCollection()) {
+                                        if (sInfo.getServiceDescriptor()
+                                                .equals(serviceDescriptor)) {
+                                            discoveryList.add(new HubDiscoveryInfo(info, sInfo));
+                                        }
+                                    }
+                                } else {
+                                    discoveryList.add(new HubDiscoveryInfo(info));
+                                }
+                            }
+                            if (discoveryList.isEmpty()) {
+                                Log.w(TAG, "onEndpointsStarted: no matching service descriptor");
+                            } else {
+                                callback.onEndpointsStarted(discoveryList);
+                            }
+                        });
+            }
+
+            @Override
+            public void onEndpointsStopped(HubEndpointInfo[] hubEndpointInfoList) {
+                if (hubEndpointInfoList.length == 0) {
+                    Log.w(TAG, "onEndpointsStopped: received empty discovery list");
+                    return;
+                }
+                executor.execute(
+                        () -> {
+                            List<HubDiscoveryInfo> discoveryList =
+                                    new ArrayList<>(hubEndpointInfoList.length);
+                            for (HubEndpointInfo info : hubEndpointInfoList) {
+                                if (serviceDescriptor != null) {
+                                    for (HubServiceInfo sInfo : info.getServiceInfoCollection()) {
+                                        if (sInfo.getServiceDescriptor()
+                                                .equals(serviceDescriptor)) {
+                                            discoveryList.add(new HubDiscoveryInfo(info, sInfo));
+                                        }
+                                    }
+                                } else {
+                                    discoveryList.add(new HubDiscoveryInfo(info));
+                                }
+                            }
+                            if (discoveryList.isEmpty()) {
+                                Log.w(TAG, "onEndpointsStopped: no matching service descriptor");
+                            } else {
+                                callback.onEndpointsStopped(discoveryList);
+                            }
+                        });
+            }
+        };
+    }
+
+    /**
+     * Equivalent to {@link #registerEndpointDiscoveryCallback(long, IHubEndpointDiscoveryCallback,
+     * Executor)} with the default executor in the main thread.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+    @FlaggedApi(Flags.FLAG_OFFLOAD_API)
+    public void registerEndpointDiscoveryCallback(
+            long endpointId, @NonNull IHubEndpointDiscoveryCallback callback) {
+        registerEndpointDiscoveryCallback(
+                endpointId, callback, new HandlerExecutor(Handler.getMain()));
+    }
+
+    /**
+     * Registers a callback to be notified when the hub endpoint with the corresponding endpoint ID
+     * has started or stopped.
+     *
+     * @param endpointId The identifier of the hub endpoint.
+     * @param callback The callback to be invoked.
+     * @param executor The executor to invoke the callback on.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+    @FlaggedApi(Flags.FLAG_OFFLOAD_API)
+    public void registerEndpointDiscoveryCallback(
+            long endpointId,
+            @NonNull IHubEndpointDiscoveryCallback callback,
+            @NonNull Executor executor) {
+        Objects.requireNonNull(callback, "callback cannot be null");
+        Objects.requireNonNull(executor, "executor cannot be null");
+        IContextHubEndpointDiscoveryCallback iCallback =
+                createDiscoveryCallback(callback, executor, null);
+        try {
+            mService.registerEndpointDiscoveryCallbackId(endpointId, iCallback);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+
+        mDiscoveryCallbacks.put(callback, iCallback);
+    }
+
+    /**
+     * Equivalent to {@link #registerEndpointDiscoveryCallback(String,
+     * IHubEndpointDiscoveryCallback, Executor)} with the default executor in the main thread.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+    @FlaggedApi(Flags.FLAG_OFFLOAD_API)
+    public void registerEndpointDiscoveryCallback(
+            @NonNull String serviceDescriptor, @NonNull IHubEndpointDiscoveryCallback callback) {
+        registerEndpointDiscoveryCallback(
+                serviceDescriptor, callback, new HandlerExecutor(Handler.getMain()));
+    }
+
+    /**
+     * Registers a callback to be notified when the hub endpoint with the corresponding service
+     * descriptor has started or stopped.
+     *
+     * @param serviceDescriptor The service descriptor of the hub endpoint.
+     * @param callback The callback to be invoked.
+     * @param executor The executor to invoke the callback on.
+     * @throws IllegalArgumentException if the serviceDescriptor is empty.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+    @FlaggedApi(Flags.FLAG_OFFLOAD_API)
+    public void registerEndpointDiscoveryCallback(
+            @NonNull String serviceDescriptor,
+            @NonNull IHubEndpointDiscoveryCallback callback,
+            @NonNull Executor executor) {
+        Objects.requireNonNull(serviceDescriptor, "serviceDescriptor cannot be null");
+        Objects.requireNonNull(callback, "callback cannot be null");
+        Objects.requireNonNull(executor, "executor cannot be null");
+        if (serviceDescriptor.isBlank()) {
+            throw new IllegalArgumentException("Invalid service descriptor: " + serviceDescriptor);
+        }
+
+        IContextHubEndpointDiscoveryCallback iCallback =
+                createDiscoveryCallback(callback, executor, serviceDescriptor);
+        try {
+            mService.registerEndpointDiscoveryCallbackDescriptor(serviceDescriptor, iCallback);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+
+        mDiscoveryCallbacks.put(callback, iCallback);
+    }
+
+    /**
+     * Unregisters a previously registered endpoint discovery callback.
+     *
+     * @param callback The callback previously registered.
+     * @throws IllegalArgumentException If the callback was not previously registered.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+    @FlaggedApi(Flags.FLAG_OFFLOAD_API)
+    public void unregisterEndpointDiscoveryCallback(
+            @NonNull IHubEndpointDiscoveryCallback callback) {
+        Objects.requireNonNull(callback, "callback cannot be null");
+        IContextHubEndpointDiscoveryCallback iCallback = mDiscoveryCallbacks.remove(callback);
+        if (iCallback == null) {
+            throw new IllegalArgumentException("Callback not previously registered");
+        }
+
+        try {
+            mService.unregisterEndpointDiscoveryCallback(iCallback);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Set a callback to receive messages from the context hub
      *
      * @param callback Callback object
diff --git a/core/java/android/hardware/location/IContextHubService.aidl b/core/java/android/hardware/location/IContextHubService.aidl
index f9f41244..f14aadc 100644
--- a/core/java/android/hardware/location/IContextHubService.aidl
+++ b/core/java/android/hardware/location/IContextHubService.aidl
@@ -21,6 +21,7 @@
 import android.hardware.contexthub.HubEndpointInfo;
 import android.hardware.contexthub.IContextHubEndpoint;
 import android.hardware.contexthub.IContextHubEndpointCallback;
+import android.hardware.contexthub.IContextHubEndpointDiscoveryCallback;
 import android.hardware.location.ContextHubInfo;
 import android.hardware.location.ContextHubMessage;
 import android.hardware.location.HubInfo;
@@ -137,4 +138,16 @@
     // Register an endpoint with the context hub
     @EnforcePermission("ACCESS_CONTEXT_HUB")
     IContextHubEndpoint registerEndpoint(in HubEndpointInfo pendingEndpointInfo, in IContextHubEndpointCallback callback);
+
+    // Register an endpoint discovery callback (id)
+    @EnforcePermission("ACCESS_CONTEXT_HUB")
+    void registerEndpointDiscoveryCallbackId(long endpointId, in IContextHubEndpointDiscoveryCallback callback);
+
+    // Register an endpoint discovery callback (descriptor)
+    @EnforcePermission("ACCESS_CONTEXT_HUB")
+    void registerEndpointDiscoveryCallbackDescriptor(String serviceDescriptor, in IContextHubEndpointDiscoveryCallback callback);
+
+    // Unregister an endpoint with the context hub
+    @EnforcePermission("ACCESS_CONTEXT_HUB")
+    void unregisterEndpointDiscoveryCallback(in IContextHubEndpointDiscoveryCallback callback);
 }
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 55ba4af..a653e0a 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -58,7 +58,7 @@
     is_fixed_read_only: true
     namespace: "permissions"
     description: "enable enhanced confirmation incall apis"
-    bug: "310220212"
+    bug: "364535720"
 }
 
 flag {
@@ -67,7 +67,7 @@
     is_fixed_read_only: true
     namespace: "permissions"
     description: "enable the blocking of certain app installs during an unknown call"
-    bug: "310220212"
+    bug: "364535720"
 }
 
 flag {
diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig
index 357aba3..6c92991 100644
--- a/core/java/android/security/responsible_apis_flags.aconfig
+++ b/core/java/android/security/responsible_apis_flags.aconfig
@@ -96,21 +96,6 @@
 }
 
 flag {
-    name: "prevent_intent_redirect_throw_exception_if_nested_keys_not_collected"
-    namespace: "responsible_apis"
-    description: "Prevent intent redirect attacks by throwing exception if the intent does not collect nested keys"
-    bug: "361143368"
-}
-
-flag {
-    name: "prevent_intent_redirect_collect_nested_keys_on_server_if_not_collected"
-    namespace: "responsible_apis"
-    description: "Prevent intent redirect attacks by collecting nested keys on server if not yet collected"
-    bug: "361143368"
-    is_fixed_read_only: true
-}
-
-flag {
     name: "enable_intent_matching_flags"
     is_exported: true
     namespace: "permissions"
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 80d39d1..595eb26 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -9333,7 +9333,11 @@
         Set<Integer> bitmapIdSet = getBitmapIdsUsedByActions(new HashSet<>());
         int result = 0;
         for (int bitmapId: bitmapIdSet) {
-            result += mBitmapCache.getBitmapForId(bitmapId).getAllocationByteCount();
+            Bitmap currentBitmap = mBitmapCache.getBitmapForId(bitmapId);
+            if (currentBitmap == null) {
+                continue;
+            }
+            result += currentBitmap.getAllocationByteCount();
         }
 
         return result;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index a13e334..7c2a30d 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -848,10 +848,10 @@
     <protected-broadcast android:name="android.app.action.CONSOLIDATED_NOTIFICATION_POLICY_CHANGED" />
     <protected-broadcast android:name="android.intent.action.MAIN_USER_LOCKSCREEN_KNOWLEDGE_FACTOR_CHANGED" />
     <protected-broadcast android:name="com.android.uwb.uwbcountrycode.GEOCODE_RETRY" />
-    <protected-broadcast android:name="android.telephony.action.ACTION_SATELLITE_SUBSCRIBER_ID_LIST_CHANGED" />
+    <protected-broadcast android:name="android.telephony.satellite.action.SATELLITE_SUBSCRIBER_ID_LIST_CHANGED" />
     <protected-broadcast android:name="android.service.ondeviceintelligence.MODEL_LOADED" />
     <protected-broadcast android:name="android.service.ondeviceintelligence.MODEL_UNLOADED" />
-    <protected-broadcast android:name="android.telephony.action.ACTION_SATELLITE_START_NON_EMERGENCY_SESSION" />
+    <protected-broadcast android:name="android.telephony.satellite.action.SATELLITE_START_NON_EMERGENCY_SESSION" />
 
 
     <!-- ====================================================================== -->
@@ -9351,6 +9351,17 @@
             </intent-filter>
         </service>
 
+        <service android:name="com.android.ecm.EnhancedConfirmationCallTrackerService"
+            android:permission="android.permission.BIND_INCALL_SERVICE"
+            android:featureFlag="android.permission.flags.enhanced_confirmation_in_call_apis_enabled"
+            android:exported="true">
+            <meta-data android:name="android.telecom.INCLUDE_SELF_MANAGED_CALLS"
+                android:value="true" />
+            <intent-filter>
+                <action android:name="android.telecom.InCallService"/>
+            </intent-filter>
+        </service>
+
         <service android:name="com.android.server.companion.datatransfer.contextsync.CallMetadataSyncConnectionService"
                  android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
                  android:exported="true">
diff --git a/core/res/res/drawable-watch-v36/dialog_alert_button_background_negative.xml b/core/res/res/drawable-watch-v36/dialog_alert_button_background_negative.xml
index b6b8eac3..0314bbe 100644
--- a/core/res/res/drawable-watch-v36/dialog_alert_button_background_negative.xml
+++ b/core/res/res/drawable-watch-v36/dialog_alert_button_background_negative.xml
@@ -18,7 +18,7 @@
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
     <solid android:color="@color/btn_material_filled_tonal_background_color"/>
-    <corners android:radius="@dimen/config_bottomDialogCornerRadius" />
+    <corners android:radius="@dimen/config_wearMaterial3_bottomDialogCornerRadius" />
     <size
         android:width="@dimen/dialog_btn_negative_width"
         android:height="@dimen/dialog_btn_negative_height" />
diff --git a/core/res/res/layout-watch-v36/alert_dialog_icon_button_wear_material3.xml b/core/res/res/layout-watch-v36/alert_dialog_icon_button_wear_material3.xml
new file mode 100644
index 0000000..407ec7a
--- /dev/null
+++ b/core/res/res/layout-watch-v36/alert_dialog_icon_button_wear_material3.xml
@@ -0,0 +1,123 @@
+<!--
+  ~ 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.
+  -->
+
+<!-- This layout is the AlertDialog template. It overrides the system layout with the same name.
+    Make sure to include all the existing id of the overridden alert_dialog_material.-->
+<com.android.internal.widget.WatchListDecorLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/parentPanel"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <ScrollView
+        android:id="@+id/scrollView"
+        android:fillViewport="true"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+        <LinearLayout
+            android:orientation="vertical"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+            <!-- Top Panel -->
+            <FrameLayout
+                android:paddingLeft="?dialogPreferredPadding"
+                android:paddingRight="?dialogPreferredPadding"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:id="@+id/topPanel"
+                android:minHeight="@dimen/dialog_list_padding_top_no_title">
+                <include android:id="@+id/title_template"
+                         android:layout_width="match_parent"
+                         android:layout_height="wrap_content"
+                         layout="@layout/alert_dialog_title_material"/>
+            </FrameLayout>
+
+            <!-- Content Panel -->
+            <FrameLayout android:id="@+id/contentPanel"
+                         android:layout_width="match_parent"
+                         android:layout_height="wrap_content"
+                         android:clipToPadding="false">
+                <TextView android:id="@+id/message"
+                          android:layout_width="match_parent"
+                          android:layout_height="wrap_content"
+                          android:gravity="center_horizontal|top"
+                          android:textAppearance="@style/TextAppearance.DeviceDefault.Body1"
+                          android:paddingStart="?dialogPreferredPadding"
+                          android:paddingEnd="?dialogPreferredPadding"
+                          android:paddingTop="8dip"
+                          android:paddingBottom="8dip"/>
+            </FrameLayout>
+
+            <!-- Custom Panel, to replace content panel if needed -->
+            <FrameLayout android:id="@+id/customPanel"
+                         android:layout_width="match_parent"
+                         android:layout_height="match_parent"
+                         android:minHeight="64dp">
+                <FrameLayout android:id="@+android:id/custom"
+                             android:layout_width="match_parent"
+                             android:layout_height="wrap_content" />
+            </FrameLayout>
+
+            <!-- Button Panel -->
+            <FrameLayout
+                android:id="@+id/buttonPanel"
+                android:minHeight="@dimen/dialog_list_padding_bottom_no_buttons"
+                android:layout_weight="1"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center">
+                <LinearLayout
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="bottom"
+                    android:orientation="horizontal"
+                    android:paddingBottom="?dialogPreferredPadding"
+                    style="?android:attr/buttonBarStyle"
+                    android:measureWithLargestChild="true">
+                    <Button android:id="@+id/button2"
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:layout_gravity="center"
+                            android:gravity="center"
+                            android:layout_weight="1"
+                            style="@style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog.WearMaterial3.Negative" />
+                    <Button android:id="@+id/button3"
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:layout_gravity="center"
+                            android:gravity="center"
+                            android:layout_weight="1"
+                            style="?android:attr/buttonBarButtonStyle"/>
+                    <FrameLayout
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content">
+                        <Button android:id="@+id/button1"
+                                android:layout_width="match_parent"
+                                android:layout_height="match_parent"
+                                android:layout_gravity="center"
+                                android:gravity="center"
+                                android:layout_weight="1"
+                                style="@style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog.WearMaterial3.Confirm" />
+                        <!-- This works as background. -->
+                        <ImageView
+                            android:layout_width="match_parent"
+                            android:layout_height="match_parent"
+                            android:src="@drawable/dialog_alert_button_positive"/>
+                    </FrameLayout>
+                </LinearLayout>
+            </FrameLayout>
+        </LinearLayout>
+    </ScrollView>
+</com.android.internal.widget.WatchListDecorLayout>
diff --git a/core/res/res/layout-watch-v36/alert_dialog_material.xml b/core/res/res/layout-watch-v36/alert_dialog_material.xml
index 900102f..8f75456 100644
--- a/core/res/res/layout-watch-v36/alert_dialog_material.xml
+++ b/core/res/res/layout-watch-v36/alert_dialog_material.xml
@@ -82,9 +82,8 @@
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:layout_gravity="bottom"
-                    android:orientation="horizontal"
+                    android:orientation="vertical"
                     android:paddingBottom="?dialogPreferredPadding"
-                    style="?android:attr/buttonBarStyle"
                     android:measureWithLargestChild="true">
                     <Button android:id="@+id/button2"
                             android:layout_width="wrap_content"
@@ -92,7 +91,7 @@
                             android:layout_gravity="center"
                             android:gravity="center"
                             android:layout_weight="1"
-                            style="@style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog.Negative" />
+                            style="@*android:style/Widget.DeviceDefault.Button.WearMaterial3"/>
                     <Button android:id="@+id/button3"
                             android:layout_width="wrap_content"
                             android:layout_height="wrap_content"
@@ -100,22 +99,13 @@
                             android:gravity="center"
                             android:layout_weight="1"
                             style="?android:attr/buttonBarButtonStyle"/>
-                    <FrameLayout
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content">
-                        <Button android:id="@+id/button1"
-                                android:layout_width="match_parent"
-                                android:layout_height="match_parent"
-                                android:layout_gravity="center"
-                                android:gravity="center"
-                                android:layout_weight="1"
-                                style="@style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog.Confirm"/>
-                        <!-- This works as background. -->
-                        <ImageView
+                    <Button android:id="@+id/button1"
                             android:layout_width="match_parent"
                             android:layout_height="match_parent"
-                            android:src="@drawable/dialog_alert_button_positive"/>
-                    </FrameLayout>
+                            android:layout_gravity="center"
+                            android:gravity="center"
+                            android:layout_weight="1"
+                            style="@*android:style/Widget.DeviceDefault.Button.Filled"/>
                 </LinearLayout>
             </FrameLayout>
         </LinearLayout>
diff --git a/core/res/res/values-watch-v36/config.xml b/core/res/res/values-watch-v36/config.xml
index 1143ae3..679dc70 100644
--- a/core/res/res/values-watch-v36/config.xml
+++ b/core/res/res/values-watch-v36/config.xml
@@ -16,5 +16,5 @@
 
 <resources>
     <dimen name="config_wearMaterial3_buttonCornerRadius">26dp</dimen>
-    <dimen name="config_bottomDialogCornerRadius">18dp</dimen>
+    <dimen name="config_wearMaterial3_bottomDialogCornerRadius">18dp</dimen>
 </resources>
diff --git a/core/res/res/values-watch-v36/styles_material.xml b/core/res/res/values-watch-v36/styles_material.xml
index 00f3f09..7da7435 100644
--- a/core/res/res/values-watch-v36/styles_material.xml
+++ b/core/res/res/values-watch-v36/styles_material.xml
@@ -57,7 +57,7 @@
     </style>
 
     <!--  AlertDialog Styles  -->
-    <style name="Widget.DeviceDefault.Button.ButtonBar.AlertDialog" parent="Widget.DeviceDefault.Button">
+    <style name="Widget.DeviceDefault.Button.ButtonBar.AlertDialog.WearMaterial3" parent="Widget.DeviceDefault.Button">
         <item name="android:textSize">0sp</item>
         <item name="android:gravity">center</item>
         <item name="android:paddingStart">0dp</item>
@@ -65,14 +65,14 @@
         <item name="android:drawablePadding">0dp</item>
     </style>
 
-    <style name="Widget.DeviceDefault.Button.ButtonBar.AlertDialog.Confirm" parent="Widget.DeviceDefault.Button.ButtonBar.AlertDialog">
+    <style name="Widget.DeviceDefault.Button.ButtonBar.AlertDialog.WearMaterial3.Confirm">
         <!-- Use a ImageView as background -->
         <item name="background">@android:color/transparent</item>
         <item name="minWidth">@dimen/dialog_btn_confirm_width</item>
         <item name="minHeight">@dimen/dialog_btn_confirm_height</item>
     </style>
 
-    <style name="Widget.DeviceDefault.Button.ButtonBar.AlertDialog.Negative" parent="Widget.DeviceDefault.Button.ButtonBar.AlertDialog">
+    <style name="Widget.DeviceDefault.Button.ButtonBar.AlertDialog.WearMaterial3.Negative">
         <item name="background">@drawable/dialog_alert_button_negative</item>
         <item name="minWidth">@dimen/dialog_btn_negative_width</item>
         <item name="minHeight">@dimen/dialog_btn_negative_height</item>
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index af690f4..897fc54 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -677,8 +677,4 @@
         <permission name="android.permission.BATTERY_STATS"/>
         <permission name="android.permission.ENTER_TRADE_IN_MODE"/>
     </privapp-permissions>
-
-    <privapp-permissions package="com.android.multiuser">
-        <permission name="android.permission.MANAGE_USERS"/>
-    </privapp-permissions>
 </permissions>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index 1ba29a2..8ee087b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -910,6 +910,11 @@
                         "Unexpected bundle for " + mPipTransitionState);
                 break;
             case PipTransitionState.EXITED_PIP:
+                // Save the PiP bounds in case, we re-enter the PiP with the same component.
+                float snapFraction = mPipBoundsAlgorithm.getSnapFraction(
+                        mPipBoundsState.getBounds());
+                mPipBoundsState.saveReentryState(snapFraction);
+
                 mPipTransitionState.setPipTaskToken(null);
                 mPipTransitionState.setPinnedTaskLeash(null);
                 break;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 1913e93..f482269 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -1575,8 +1575,6 @@
 
         final DesktopModeTouchEventListener touchEventListener =
                 new DesktopModeTouchEventListener(taskInfo, taskPositioner);
-        InputMethod inputMethod = DesktopModeEventLogger.getInputMethodFromMotionEvent(
-                touchEventListener.mMotionEvent);
         windowDecoration.setOnMaximizeOrRestoreClickListener(() -> {
             onMaximizeOrRestore(taskInfo.taskId, "maximize_menu", ResizeTrigger.MAXIMIZE_MENU,
                     touchEventListener.mMotionEvent);
@@ -1587,11 +1585,15 @@
             return Unit.INSTANCE;
         });
         windowDecoration.setOnLeftSnapClickListener(() -> {
-            onSnapResize(taskInfo.taskId, /* isLeft= */ true, inputMethod);
+            onSnapResize(taskInfo.taskId, /* isLeft= */ true,
+                    DesktopModeEventLogger.getInputMethodFromMotionEvent(
+                            touchEventListener.mMotionEvent));
             return Unit.INSTANCE;
         });
         windowDecoration.setOnRightSnapClickListener(() -> {
-            onSnapResize(taskInfo.taskId, /* isLeft= */ false, inputMethod);
+            onSnapResize(taskInfo.taskId, /* isLeft= */ false,
+                    DesktopModeEventLogger.getInputMethodFromMotionEvent(
+                            touchEventListener.mMotionEvent));
             return Unit.INSTANCE;
         });
         windowDecoration.setOnToDesktopClickListener(desktopModeTransitionSource -> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
index 6f72d34..c8aff78 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
@@ -74,8 +74,7 @@
         mFineTaskCorners = new TaskCorners(mTaskSize, fineCornerSize, disabledEdge);
 
         // Save touch areas for each edge.
-        mTaskEdges = new TaskEdges(mTaskSize, mResizeHandleEdgeOutset, mResizeHandleEdgeInset,
-                mDisabledEdge);
+        mTaskEdges = new TaskEdges(mTaskSize, mResizeHandleEdgeOutset, mDisabledEdge);
     }
 
     /**
@@ -459,7 +458,7 @@
         private final @NonNull DisabledEdge mDisabledEdge;
 
         private TaskEdges(@NonNull Size taskSize, int resizeHandleThickness,
-                int resizeHandleEdgeInset, DisabledEdge disabledEdge) {
+                DisabledEdge disabledEdge) {
             // Save touch areas for each edge.
             mDisabledEdge = disabledEdge;
             // Save touch areas for each edge.
@@ -471,16 +470,16 @@
             mLeftEdgeBounds = new Rect(
                     -resizeHandleThickness,
                     0,
-                    resizeHandleEdgeInset,
+                    resizeHandleThickness,
                     taskSize.getHeight());
             mRightEdgeBounds = new Rect(
-                    taskSize.getWidth() - resizeHandleEdgeInset,
+                    taskSize.getWidth() - resizeHandleThickness,
                     0,
                     taskSize.getWidth() + resizeHandleThickness,
                     taskSize.getHeight());
             mBottomEdgeBounds = new Rect(
                     -resizeHandleThickness,
-                    taskSize.getHeight() - resizeHandleEdgeInset,
+                    taskSize.getHeight() - resizeHandleThickness,
                     taskSize.getWidth() + resizeHandleThickness,
                     taskSize.getHeight() + resizeHandleThickness);
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
index e7d328e..479f156 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
@@ -63,6 +63,7 @@
     private static final int EDGE_RESIZE_HANDLE_INSET = 4;
     private static final int FINE_CORNER_SIZE = EDGE_RESIZE_THICKNESS * 2 + 10;
     private static final int LARGE_CORNER_SIZE = FINE_CORNER_SIZE + 10;
+    private static final int SMALL_OFFSET = 10;
     private static final DragResizeWindowGeometry GEOMETRY = new DragResizeWindowGeometry(
             TASK_CORNER_RADIUS, TASK_SIZE, EDGE_RESIZE_THICKNESS, EDGE_RESIZE_HANDLE_INSET,
             FINE_CORNER_SIZE, LARGE_CORNER_SIZE, DragResizeWindowGeometry.DisabledEdge.NONE);
@@ -147,15 +148,19 @@
         assertThat(region.contains(point.x + EDGE_RESIZE_THICKNESS, point.y)).isTrue();
         assertThat(region.contains(point.x - EDGE_RESIZE_THICKNESS, point.y)).isTrue();
         // Vertically along the edge is not contained.
-        assertThat(region.contains(point.x, point.y - EDGE_RESIZE_THICKNESS)).isFalse();
-        assertThat(region.contains(point.x, point.y + EDGE_RESIZE_THICKNESS + 10)).isFalse();
+        assertThat(
+                region.contains(point.x, point.y - EDGE_RESIZE_THICKNESS - SMALL_OFFSET)).isFalse();
+        assertThat(
+                region.contains(point.x, point.y + EDGE_RESIZE_THICKNESS + SMALL_OFFSET)).isFalse();
     }
 
     private static void verifyVerticalEdge(@NonNull Region region, @NonNull Point point) {
         assertThat(region.contains(point.x, point.y)).isTrue();
         // Horizontally along the edge is not contained.
-        assertThat(region.contains(point.x + EDGE_RESIZE_THICKNESS, point.y)).isFalse();
-        assertThat(region.contains(point.x - EDGE_RESIZE_THICKNESS, point.y)).isFalse();
+        assertThat(
+                region.contains(point.x + EDGE_RESIZE_THICKNESS + SMALL_OFFSET, point.y)).isFalse();
+        assertThat(
+                region.contains(point.x - EDGE_RESIZE_THICKNESS - SMALL_OFFSET, point.y)).isFalse();
         // Vertically along the edge is contained.
         assertThat(region.contains(point.x, point.y - EDGE_RESIZE_THICKNESS)).isTrue();
         assertThat(region.contains(point.x, point.y + EDGE_RESIZE_THICKNESS)).isTrue();
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index d8a8c8b..bbe8e4e 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -141,6 +141,14 @@
 }
 
 flag {
+    name: "enable_route_visibility_control_api"
+    namespace: "media_better_together"
+    description: "API changes to allow more control over route visibility by route providers"
+    bug: "367799834"
+    is_exported: true
+}
+
+flag {
     name: "enable_screen_off_scanning"
     is_exported: true
     namespace: "media_solutions"
diff --git a/packages/StatementService/Android.bp b/packages/StatementService/Android.bp
index 90e1808..39b0302 100644
--- a/packages/StatementService/Android.bp
+++ b/packages/StatementService/Android.bp
@@ -38,8 +38,10 @@
         "StatementServiceParser",
         "androidx.appcompat_appcompat",
         "androidx.collection_collection-ktx",
+        "androidx.room_room-runtime",
         "androidx.work_work-runtime",
         "androidx.work_work-runtime-ktx",
         "kotlinx-coroutines-android",
     ],
+    plugins: ["androidx.room_room-compiler-plugin"],
 }
diff --git a/packages/StatementService/src/com/android/statementservice/database/Converters.kt b/packages/StatementService/src/com/android/statementservice/database/Converters.kt
new file mode 100644
index 0000000..21ecc8b
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/database/Converters.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.statementservice.database
+
+import android.content.UriRelativeFilter
+import android.content.UriRelativeFilterGroup
+import android.util.JsonReader
+import androidx.room.TypeConverter
+import org.json.JSONArray
+import org.json.JSONObject
+import java.io.StringReader
+import java.util.ArrayList
+
+class Converters {
+    companion object {
+        private const val ACTION_NAME = "action"
+        private const val FILTERS_NAME = "filters"
+        private const val URI_PART_NAME = "uriPart"
+        private const val PATTERN_TYPE_NAME = "patternType"
+        private const val FILTER_NAME = "filter"
+    }
+
+    @TypeConverter
+    fun groupsToJson(groups: List<UriRelativeFilterGroup>): String {
+        val json = JSONArray()
+        for (group in groups) {
+            json.put(groupToJson(group))
+        }
+        return json.toString()
+    }
+
+    @TypeConverter
+    fun stringToGroups(json: String): List<UriRelativeFilterGroup> {
+        val groups = ArrayList<UriRelativeFilterGroup>()
+        StringReader(json).use { stringReader ->
+            JsonReader(stringReader).use { reader ->
+                reader.beginArray()
+                while (reader.hasNext()) {
+                    groups.add(parseGroup(reader))
+                }
+                reader.endArray()
+            }
+        }
+        return groups
+    }
+
+    private fun groupToJson(group: UriRelativeFilterGroup): JSONObject {
+        val jsonObject = JSONObject()
+        jsonObject.put(ACTION_NAME, group.action)
+        val filters = JSONArray()
+        for (filter in group.uriRelativeFilters) {
+            filters.put(filterToJson(filter))
+        }
+        jsonObject.put(FILTERS_NAME, filters)
+        return jsonObject
+    }
+
+    private fun filterToJson(filter: UriRelativeFilter): JSONObject {
+        val jsonObject = JSONObject()
+        jsonObject.put(URI_PART_NAME, filter.uriPart)
+        jsonObject.put(PATTERN_TYPE_NAME, filter.patternType)
+        jsonObject.put(FILTER_NAME, filter.filter)
+        return jsonObject
+    }
+
+    private fun parseGroup(reader: JsonReader): UriRelativeFilterGroup {
+        val jsonObject = JSONObject()
+        reader.beginObject()
+        while (reader.hasNext()) {
+            val name = reader.nextName()
+            when (name) {
+                ACTION_NAME -> jsonObject.put(ACTION_NAME, reader.nextInt())
+                FILTERS_NAME -> jsonObject.put(FILTERS_NAME, parseFilters(reader))
+                else -> reader.skipValue()
+            }
+        }
+        reader.endObject()
+
+        val group = UriRelativeFilterGroup(jsonObject.getInt(ACTION_NAME))
+        val filters = jsonObject.getJSONArray(FILTERS_NAME)
+        for (i in 0 until filters.length()) {
+            val filter = filters.getJSONObject(i)
+            group.addUriRelativeFilter(UriRelativeFilter(
+                filter.getInt(URI_PART_NAME),
+                filter.getInt(PATTERN_TYPE_NAME),
+                filter.getString(FILTER_NAME)
+            ))
+        }
+        return group
+    }
+
+    private fun parseFilters(reader: JsonReader): JSONArray {
+        val filters = JSONArray()
+        reader.beginArray()
+        while (reader.hasNext()) {
+            filters.put(parseFilter(reader))
+        }
+        reader.endArray()
+        return filters
+    }
+
+    private fun parseFilter(reader: JsonReader): JSONObject {
+        reader.beginObject()
+        val jsonObject = JSONObject()
+        while (reader.hasNext()) {
+            val name = reader.nextName()
+            when (name) {
+                URI_PART_NAME, PATTERN_TYPE_NAME -> jsonObject.put(name, reader.nextInt())
+                FILTER_NAME -> jsonObject.put(name, reader.nextString())
+                else -> reader.skipValue()
+            }
+        }
+        reader.endObject()
+        return jsonObject
+    }
+}
\ No newline at end of file
diff --git a/packages/StatementService/src/com/android/statementservice/database/DomainGroups.kt b/packages/StatementService/src/com/android/statementservice/database/DomainGroups.kt
new file mode 100644
index 0000000..c616669
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/database/DomainGroups.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.statementservice.database
+
+import android.content.UriRelativeFilterGroup
+import androidx.room.Entity
+
+@Entity(primaryKeys = ["packageName", "domain"])
+data class DomainGroups(
+    val packageName: String,
+    val domain: String,
+    val groups: List<UriRelativeFilterGroup>
+)
\ No newline at end of file
diff --git a/packages/StatementService/src/com/android/statementservice/database/DomainGroupsDao.kt b/packages/StatementService/src/com/android/statementservice/database/DomainGroupsDao.kt
new file mode 100644
index 0000000..3b4dcea
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/database/DomainGroupsDao.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.statementservice.database
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.Query
+
+@Dao
+interface DomainGroupsDao {
+    @Query("SELECT * FROM DomainGroups WHERE packageName = :packageName")
+    fun getDomainGroups(packageName: String): List<DomainGroups>
+
+    @Insert
+    fun insertDomainGroups(vararg domainGroups: DomainGroups)
+
+    @Query("DELETE FROM DomainGroups WHERE packageName = :packageName AND domain = :domain")
+    fun clear(packageName: String, domain: String)
+
+    @Query("DELETE FROM DomainGroups WHERE packageName = :packageName")
+    fun clear(packageName: String)
+}
\ No newline at end of file
diff --git a/packages/StatementService/src/com/android/statementservice/database/DomainGroupsDatabase.kt b/packages/StatementService/src/com/android/statementservice/database/DomainGroupsDatabase.kt
new file mode 100644
index 0000000..39833f6
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/database/DomainGroupsDatabase.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.statementservice.database
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+
+@Database(entities = [DomainGroups::class], version = 1)
+@TypeConverters(Converters::class)
+abstract class DomainGroupsDatabase : RoomDatabase() {
+    companion object {
+        private const val DATABASE_NAME = "domain-groups"
+        @Volatile
+        private var instance: DomainGroupsDatabase? = null
+
+        fun getInstance(context: Context) = instance ?: synchronized(this) {
+            instance ?: Room.databaseBuilder(
+                context,
+                DomainGroupsDatabase::class.java, DATABASE_NAME
+            ).build().also { instance = it }
+        }
+    }
+    abstract fun domainGroupsDao(): DomainGroupsDao
+}
\ No newline at end of file
diff --git a/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV1.kt b/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV1.kt
index acb54f6..0d7a1fd 100644
--- a/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV1.kt
+++ b/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV1.kt
@@ -22,6 +22,7 @@
 import androidx.work.ExistingWorkPolicy
 import androidx.work.WorkManager
 import com.android.statementservice.domain.worker.CollectV1Worker
+import com.android.statementservice.domain.worker.GroupUpdateV1Worker
 import com.android.statementservice.domain.worker.SingleV1RequestWorker
 
 /**
@@ -67,7 +68,7 @@
             }
         }
 
-        //clear sp before enqueue unique work since policy is REPLACE
+        // clear sp before enqueue unique work since policy is REPLACE
         val deContext = context.createDeviceProtectedStorageContext()
         val editor = deContext?.getSharedPreferences(packageName, Context.MODE_PRIVATE)?.edit()
         editor?.clear()?.apply()
@@ -78,6 +79,7 @@
                 workRequests
             )
             .then(CollectV1Worker.buildRequest(verificationId, packageName))
+            .then(GroupUpdateV1Worker.buildRequest(packageName))
             .enqueue()
     }
 }
diff --git a/packages/StatementService/src/com/android/statementservice/domain/DomainVerifier.kt b/packages/StatementService/src/com/android/statementservice/domain/DomainVerifier.kt
index 29f844f..6914347 100644
--- a/packages/StatementService/src/com/android/statementservice/domain/DomainVerifier.kt
+++ b/packages/StatementService/src/com/android/statementservice/domain/DomainVerifier.kt
@@ -24,6 +24,7 @@
 import com.android.statementservice.network.retriever.StatementRetriever
 import com.android.statementservice.retriever.AbstractAsset
 import com.android.statementservice.retriever.AbstractAssetMatcher
+import com.android.statementservice.retriever.Statement
 import com.android.statementservice.utils.Result
 import com.android.statementservice.utils.StatementUtils
 import com.android.statementservice.utils.component1
@@ -87,10 +88,10 @@
         host: String,
         packageName: String,
         network: Network? = null
-    ): Pair<WorkResult, VerifyStatus> {
+    ): Triple<WorkResult, VerifyStatus, Statement?> {
         val assetMatcher = synchronized(targetAssetCache) { targetAssetCache[packageName] }
             .takeIf { it!!.isPresent }
-            ?: return WorkResult.failure() to VerifyStatus.FAILURE_PACKAGE_MANAGER
+            ?: return Triple(WorkResult.failure(), VerifyStatus.FAILURE_PACKAGE_MANAGER, null)
         return verifyHost(host, assetMatcher.get(), network)
     }
 
@@ -98,34 +99,34 @@
         host: String,
         assetMatcher: AbstractAssetMatcher,
         network: Network? = null
-    ): Pair<WorkResult, VerifyStatus> {
+    ): Triple<WorkResult, VerifyStatus, Statement?> {
         var exception: Exception? = null
         val resultAndStatus = try {
             val sourceAsset = StatementUtils.createWebAssetString(host)
                 .let(AbstractAsset::create)
             val result = retriever.retrieve(sourceAsset, network)
-                ?: return WorkResult.success() to VerifyStatus.FAILURE_UNKNOWN
+                ?: return Triple(WorkResult.success(), VerifyStatus.FAILURE_UNKNOWN, null)
             when (result.responseCode) {
                 HttpURLConnection.HTTP_MOVED_PERM,
                 HttpURLConnection.HTTP_MOVED_TEMP -> {
-                    WorkResult.failure() to VerifyStatus.FAILURE_REDIRECT
+                    Triple(WorkResult.failure(), VerifyStatus.FAILURE_REDIRECT, null)
                 }
                 else -> {
-                    val isVerified = result.statements.any { statement ->
+                    val statement = result.statements.firstOrNull { statement ->
                         (StatementUtils.RELATION.matches(statement.relation) &&
                                 assetMatcher.matches(statement.target))
                     }
 
-                    if (isVerified) {
-                        WorkResult.success() to VerifyStatus.SUCCESS
+                    if (statement != null) {
+                        Triple(WorkResult.success(), VerifyStatus.SUCCESS, statement)
                     } else {
-                        WorkResult.failure() to VerifyStatus.FAILURE_REJECTED_BY_SERVER
+                        Triple(WorkResult.failure(), VerifyStatus.FAILURE_REJECTED_BY_SERVER, statement)
                     }
                 }
             }
         } catch (e: Exception) {
             exception = e
-            WorkResult.retry() to VerifyStatus.FAILURE_UNKNOWN
+            Triple(WorkResult.retry(), VerifyStatus.FAILURE_UNKNOWN, null)
         }
 
         if (DEBUG) {
diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/BaseRequestWorker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/BaseRequestWorker.kt
index a17f9c9..64d2d98 100644
--- a/packages/StatementService/src/com/android/statementservice/domain/worker/BaseRequestWorker.kt
+++ b/packages/StatementService/src/com/android/statementservice/domain/worker/BaseRequestWorker.kt
@@ -17,9 +17,12 @@
 package com.android.statementservice.domain.worker
 
 import android.content.Context
+import android.content.UriRelativeFilterGroup
+import android.content.pm.verify.domain.DomainVerificationInfo
 import android.content.pm.verify.domain.DomainVerificationManager
 import androidx.work.CoroutineWorker
 import androidx.work.WorkerParameters
+import com.android.statementservice.database.DomainGroupsDatabase
 import com.android.statementservice.domain.DomainVerifier
 
 abstract class BaseRequestWorker(
@@ -27,8 +30,19 @@
     protected val params: WorkerParameters
 ) : CoroutineWorker(appContext, params) {
 
+    protected val database = DomainGroupsDatabase.getInstance(appContext).domainGroupsDao()
+
     protected val verificationManager =
         appContext.getSystemService(DomainVerificationManager::class.java)!!
 
     protected val verifier = DomainVerifier.getInstance(appContext)
+
+    protected fun updateUriRelativeFilterGroups(packageName: String, domainGroupUpdates: Map<String, List<UriRelativeFilterGroup>>) {
+        val verifiedDomains = verificationManager.getDomainVerificationInfo(packageName)?.hostToStateMap?.filterValues {
+            it == DomainVerificationInfo.STATE_SUCCESS || it == DomainVerificationInfo.STATE_MODIFIABLE_VERIFIED
+        }?.keys?.toList() ?: emptyList()
+        val domainGroups = verificationManager.getUriRelativeFilterGroups(packageName, verifiedDomains)
+        domainGroupUpdates.forEach { (domain, groups) -> domainGroups[domain] = groups }
+        verificationManager.setUriRelativeFilterGroups(packageName, domainGroups)
+    }
 }
diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/GroupUpdateV1Worker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/GroupUpdateV1Worker.kt
new file mode 100644
index 0000000..f53dfc4
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/domain/worker/GroupUpdateV1Worker.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.statementservice.domain.worker
+
+import android.content.Context
+import androidx.work.Data
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkerParameters
+import kotlinx.coroutines.coroutineScope
+
+class GroupUpdateV1Worker(appContext: Context, params: WorkerParameters) :
+    BaseRequestWorker(appContext, params) {
+
+    companion object {
+
+        private const val PACKAGE_NAME_KEY = "packageName"
+
+        fun buildRequest(packageName: String) = OneTimeWorkRequestBuilder<GroupUpdateV1Worker>()
+            .setInputData(
+                Data.Builder()
+                    .putString(PACKAGE_NAME_KEY, packageName)
+                    .build()
+            )
+            .build()
+    }
+
+    override suspend fun doWork() = coroutineScope {
+        val packageName = params.inputData.getString(PACKAGE_NAME_KEY)!!
+        updateUriRelativeFilterGroups(packageName)
+        Result.success()
+    }
+
+    private fun updateUriRelativeFilterGroups(packageName: String) {
+        val groupUpdates = database.getDomainGroups(packageName)
+        updateUriRelativeFilterGroups(
+            packageName,
+            groupUpdates.associateBy({it.domain}, {it.groups})
+        )
+        database.clear(packageName)
+    }
+}
\ No newline at end of file
diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/RetryRequestWorker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/RetryRequestWorker.kt
index 61ab2c2..f83601a 100644
--- a/packages/StatementService/src/com/android/statementservice/domain/worker/RetryRequestWorker.kt
+++ b/packages/StatementService/src/com/android/statementservice/domain/worker/RetryRequestWorker.kt
@@ -17,10 +17,13 @@
 package com.android.statementservice.domain.worker
 
 import android.content.Context
+import android.content.UriRelativeFilterGroup
+import android.content.pm.verify.domain.DomainVerificationManager
 import androidx.work.NetworkType
 import androidx.work.WorkerParameters
 import com.android.statementservice.domain.VerifyStatus
 import com.android.statementservice.utils.AndroidUtils
+import com.android.statementservice.utils.StatementUtils
 import kotlinx.coroutines.async
 import kotlinx.coroutines.awaitAll
 import kotlinx.coroutines.coroutineScope
@@ -36,7 +39,13 @@
     params: WorkerParameters
 ) : BaseRequestWorker(appContext, params) {
 
-    data class VerifyResult(val domainSetId: UUID, val host: String, val status: VerifyStatus)
+    data class VerifyResult(
+        val domainSetId: UUID,
+        val host: String,
+        val status: VerifyStatus,
+        val packageName: String,
+        val groups: List<UriRelativeFilterGroup>
+    )
 
     override suspend fun doWork() = coroutineScope {
         if (!AndroidUtils.isReceiverV2Enabled(appContext)) {
@@ -49,8 +58,11 @@
             .map { (domainSetId, packageName, host) ->
                 async {
                     if (isActive && !isStopped) {
-                        val (_, status) = verifier.verifyHost(host, packageName, params.network)
-                        VerifyResult(domainSetId, host, status)
+                        val (_, status, statement) = verifier.verifyHost(host, packageName, params.network)
+                        val groups = statement?.dynamicAppLinkComponents.orEmpty().map {
+                            StatementUtils.createUriRelativeFilterGroup(it)
+                        }
+                        VerifyResult(domainSetId, host, status, packageName, groups)
                     } else {
                         // If the job gets cancelled, stop the remaining hosts, but continue the
                         // job to commit the results for hosts that were already requested.
@@ -60,17 +72,25 @@
             }
             .awaitAll()
             .filterNotNull() // TODO(b/159952358): Fast fail packages which can't be retrieved.
-            .groupBy { it.domainSetId }
-            .forEach { (domainSetId, resultsById) ->
-                resultsById.groupBy { it.status }
-                    .mapValues { it.value.map(VerifyResult::host).toSet() }
-                    .forEach { (status, hosts) ->
-                        verificationManager.setDomainVerificationStatus(
-                            domainSetId,
-                            hosts,
-                            status.value
-                        )
+            .groupBy { it.packageName }
+            .forEach { (packageName, resultsByName) ->
+                val groupUpdates = mutableMapOf<String, List<UriRelativeFilterGroup>>()
+                resultsByName.groupBy { it.domainSetId }
+                    .forEach { (domainSetId, resultsById) ->
+                        resultsById.groupBy { it.status }
+                            .forEach { (status, verifyResults) ->
+                                val error = verificationManager.setDomainVerificationStatus(
+                                    domainSetId,
+                                    verifyResults.map(VerifyResult::host).toSet(),
+                                    status.value
+                                )
+                                if (error == DomainVerificationManager.STATUS_OK
+                                    && status == VerifyStatus.SUCCESS) {
+                                    verifyResults.forEach { groupUpdates[it.host] = it.groups }
+                                }
+                            }
                     }
+                updateUriRelativeFilterGroups(packageName, groupUpdates)
             }
 
         // Succeed regardless of results since this retry is best effort and not required
diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV1RequestWorker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV1RequestWorker.kt
index 7a198cb..253a162 100644
--- a/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV1RequestWorker.kt
+++ b/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV1RequestWorker.kt
@@ -22,7 +22,9 @@
 import androidx.work.OneTimeWorkRequest
 import androidx.work.OneTimeWorkRequestBuilder
 import androidx.work.WorkerParameters
+import com.android.statementservice.database.DomainGroups
 import com.android.statementservice.utils.AndroidUtils
+import com.android.statementservice.utils.StatementUtils
 import kotlinx.coroutines.coroutineScope
 
 class SingleV1RequestWorker(appContext: Context, params: WorkerParameters) :
@@ -60,7 +62,9 @@
         val packageName = params.inputData.getString(PACKAGE_NAME_KEY)!!
         val host = params.inputData.getString(HOST_KEY)!!
 
-        val (result, status) = verifier.verifyHost(host, packageName, params.network)
+        database.clear(packageName, host)
+
+        val (result, status, statement) = verifier.verifyHost(host, packageName, params.network)
 
         if (DEBUG) {
             Log.d(
@@ -75,6 +79,10 @@
                 val deContext = appContext.createDeviceProtectedStorageContext()
                 val sp = deContext?.getSharedPreferences(packageName, Context.MODE_PRIVATE)
                 sp?.edit()?.putInt("$HOST_SUCCESS_PREFIX$host", status.value)?.apply()
+                val groups = statement?.dynamicAppLinkComponents.orEmpty().map {
+                    StatementUtils.createUriRelativeFilterGroup(it)
+                }
+                database.insertDomainGroups(DomainGroups(packageName, host, groups))
                 Result.success()
             }
             is Result.Failure -> {
diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV2RequestWorker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV2RequestWorker.kt
index 562b132..8b1347a 100644
--- a/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV2RequestWorker.kt
+++ b/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV2RequestWorker.kt
@@ -22,6 +22,7 @@
 import androidx.work.OneTimeWorkRequestBuilder
 import androidx.work.WorkerParameters
 import com.android.statementservice.utils.AndroidUtils
+import com.android.statementservice.utils.StatementUtils
 import kotlinx.coroutines.coroutineScope
 import java.util.UUID
 
@@ -59,9 +60,13 @@
         val packageName = params.inputData.getString(PACKAGE_NAME_KEY)!!
         val host = params.inputData.getString(HOST_KEY)!!
 
-        val (result, status) = verifier.verifyHost(host, packageName, params.network)
+        val (result, status, statement) = verifier.verifyHost(host, packageName, params.network)
 
         verificationManager.setDomainVerificationStatus(domainSetId, setOf(host), status.value)
+        val groups = statement?.dynamicAppLinkComponents.orEmpty().map {
+            StatementUtils.createUriRelativeFilterGroup(it)
+        }
+        updateUriRelativeFilterGroups(packageName, mapOf(host to groups))
 
         result
     }
diff --git a/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt b/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt
index ad137400..d10cb0f 100644
--- a/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt
+++ b/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt
@@ -39,6 +39,11 @@
 
     private const val FIELD_NOT_STRING_FORMAT_STRING = "Expected %s to be string."
     private const val FIELD_NOT_ARRAY_FORMAT_STRING = "Expected %s to be array."
+    private const val COMMENTS_NAME = "comments"
+    private const val EXCLUDE_NAME = "exclude"
+    private const val FRAGMENT_NAME = "#"
+    private const val QUERY_NAME = "?"
+    private const val PATH_NAME = "/"
 
     /**
      * Parses a JSON array of statements.
@@ -99,9 +104,7 @@
                 FIELD_NOT_ARRAY_FORMAT_STRING.format(StatementUtils.ASSET_DESCRIPTOR_FIELD_RELATION)
             )
         val target = AssetFactory.create(targetObject)
-        val dynamicAppLinkComponents = parseDynamicAppLinkComponents(
-            statement.optJSONObject(StatementUtils.ASSET_DESCRIPTOR_FIELD_RELATION_EXTENSIONS)
-        )
+        val dynamicAppLinkComponents = parseDynamicAppLinkComponents(statement)
 
         val statements = (0 until relations.length())
             .map { relations.getString(it) }
@@ -129,13 +132,13 @@
     }
 
     private fun parseComponent(component: JSONObject): DynamicAppLinkComponent {
-        val query = component.optJSONObject("?")
+        val query = component.optJSONObject(QUERY_NAME)
         return DynamicAppLinkComponent.create(
-            component.optBoolean("exclude", false),
-            component.optString("#"),
-            component.optString("/"),
+            component.optBoolean(EXCLUDE_NAME, false),
+            if (component.has(FRAGMENT_NAME)) component.getString(FRAGMENT_NAME) else null,
+            if (component.has(PATH_NAME)) component.getString(PATH_NAME) else null,
             query?.keys()?.asSequence()?.associateWith { query.getString(it) },
-            component.optString("comments")
+            component.optString(COMMENTS_NAME)
         )
     }
 
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/DynamicAppLinkComponent.java b/packages/StatementService/src/com/android/statementservice/retriever/DynamicAppLinkComponent.java
index dc27e12..c32f194 100644
--- a/packages/StatementService/src/com/android/statementservice/retriever/DynamicAppLinkComponent.java
+++ b/packages/StatementService/src/com/android/statementservice/retriever/DynamicAppLinkComponent.java
@@ -130,7 +130,7 @@
     @Override
     public String toString() {
         StringBuilder statement = new StringBuilder();
-        statement.append("HandleAllUriRule: ");
+        statement.append("DynamicAppLinkComponent: ");
         statement.append(mExclude);
         statement.append(", ");
         statement.append(mFragment);
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/JsonParser.java b/packages/StatementService/src/com/android/statementservice/retriever/JsonParser.java
index 7635e82..ab1853c 100644
--- a/packages/StatementService/src/com/android/statementservice/retriever/JsonParser.java
+++ b/packages/StatementService/src/com/android/statementservice/retriever/JsonParser.java
@@ -24,8 +24,6 @@
 import org.json.JSONObject;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
 
 /**
  * A helper class that creates a {@link JSONObject} from a {@link JsonReader}.
@@ -48,7 +46,7 @@
 
             JsonToken token = reader.peek();
             if (token.equals(JsonToken.BEGIN_ARRAY)) {
-                output.put(fieldName, new JSONArray(parseArray(reader)));
+                output.put(fieldName, parseArray(reader));
             } else if (token.equals(JsonToken.STRING)) {
                 output.put(fieldName, reader.nextString());
             } else if (token.equals(JsonToken.BEGIN_OBJECT)) {
@@ -57,9 +55,11 @@
                 } catch (JSONException e) {
                     errorMsg = e.getMessage();
                 }
+            } else if (token.equals(JsonToken.BOOLEAN)) {
+                output.put(fieldName, reader.nextBoolean());
             } else {
                 reader.skipValue();
-                errorMsg = "Unsupported value type.";
+                errorMsg = "Unsupported value type: " + token;
             }
         }
         reader.endObject();
@@ -72,17 +72,36 @@
     }
 
     /**
-     * Parses one string array from the {@link JsonReader}.
+     * Parses one JSON array from the {@link JsonReader}.
      */
-    public static List<String> parseArray(JsonReader reader) throws IOException {
-        ArrayList<String> output = new ArrayList<>();
+    public static JSONArray parseArray(JsonReader reader) throws IOException, JSONException {
+        JSONArray output = new JSONArray();
+        String errorMsg = null;
 
         reader.beginArray();
         while (reader.hasNext()) {
-            output.add(reader.nextString());
+            JsonToken token = reader.peek();
+            if (token.equals(JsonToken.BEGIN_ARRAY)) {
+                output.put(parseArray(reader));
+            } else if (token.equals(JsonToken.STRING)) {
+                output.put(reader.nextString());
+            } else if (token.equals(JsonToken.BEGIN_OBJECT)) {
+                try {
+                    output.put(parse(reader));
+                } catch (JSONException e) {
+                    errorMsg = e.getMessage();
+                }
+            } else {
+                reader.skipValue();
+                errorMsg = "Unsupported value type: " + token;
+            }
         }
         reader.endArray();
 
+        if (errorMsg != null) {
+            throw new JSONException(errorMsg);
+        }
+
         return output;
     }
 }
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java
index c71ef83..129dd9b 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java
@@ -36,7 +36,6 @@
 import androidx.preference.PreferenceFragmentCompat;
 import androidx.preference.PreferenceManager;
 
-import com.android.systemui.accessibility.accessibilitymenu.Flags;
 import com.android.systemui.accessibility.accessibilitymenu.R;
 
 /**
@@ -62,10 +61,8 @@
         ((TextView) findViewById(R.id.action_bar_title)).setText(
                 getResources().getString(R.string.accessibility_menu_settings_name)
         );
-        if (Flags.actionBarWrapContent()) {
-            setHeightWrapContent(findViewById(com.android.internal.R.id.action_bar));
-            setHeightWrapContent(findViewById(com.android.internal.R.id.action_bar_container));
-        }
+        setHeightWrapContent(findViewById(com.android.internal.R.id.action_bar));
+        setHeightWrapContent(findViewById(com.android.internal.R.id.action_bar_container));
     }
 
     private void setHeightWrapContent(View view) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt
index 82bcece..55b87db 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.communal.domain.interactor.setCommunalAvailable
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.flags.EnableSceneContainer
@@ -72,8 +73,10 @@
 
     @Test
     @DisableFlags(DualShade.FLAG_NAME)
-    fun actions_singleShade() =
+    fun actions_communalNotAvailable_singleShade() =
         testScope.runTest {
+            kosmos.setCommunalAvailable(false)
+
             val actions by collectLastValue(underTest.actions)
 
             setUpState(
@@ -85,6 +88,8 @@
             assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
             assertThat(actions?.get(Swipe.Down))
                 .isEqualTo(UserActionResult(Scenes.Shade, isIrreversible = true))
+            assertThat(actions?.get(Swipe.Start)).isNull()
+            assertThat(actions?.get(Swipe.End)).isNull()
 
             setUpState(
                 isShadeTouchable = false,
@@ -102,12 +107,16 @@
             assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
             assertThat(actions?.get(Swipe.Down))
                 .isEqualTo(UserActionResult(Scenes.Shade, isIrreversible = true))
+            assertThat(actions?.get(Swipe.Start)).isNull()
+            assertThat(actions?.get(Swipe.End)).isNull()
         }
 
     @Test
     @DisableFlags(DualShade.FLAG_NAME)
-    fun actions_splitShade() =
+    fun actions_communalNotAvailable_splitShade() =
         testScope.runTest {
+            kosmos.setCommunalAvailable(false)
+
             val actions by collectLastValue(underTest.actions)
 
             setUpState(
@@ -119,6 +128,8 @@
             assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
             assertThat(actions?.get(Swipe.Down))
                 .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade, isIrreversible = true))
+            assertThat(actions?.get(Swipe.Start)).isNull()
+            assertThat(actions?.get(Swipe.End)).isNull()
 
             setUpState(
                 isShadeTouchable = false,
@@ -136,12 +147,16 @@
             assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
             assertThat(actions?.get(Swipe.Down))
                 .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade, isIrreversible = true))
+            assertThat(actions?.get(Swipe.Start)).isNull()
+            assertThat(actions?.get(Swipe.End)).isNull()
         }
 
     @Test
     @EnableFlags(DualShade.FLAG_NAME)
-    fun actions_dualShade() =
+    fun actions_communalNotAvailable_dualShade() =
         testScope.runTest {
+            kosmos.setCommunalAvailable(false)
+
             val actions by collectLastValue(underTest.actions)
 
             setUpState(
@@ -155,6 +170,8 @@
                 .isEqualTo(
                     UserActionResult.ShowOverlay(Overlays.NotificationsShade, isIrreversible = true)
                 )
+            assertThat(actions?.get(Swipe.Start)).isNull()
+            assertThat(actions?.get(Swipe.End)).isNull()
 
             setUpState(
                 isShadeTouchable = false,
@@ -170,6 +187,128 @@
                 .isEqualTo(
                     UserActionResult.ShowOverlay(Overlays.NotificationsShade, isIrreversible = true)
                 )
+            assertThat(actions?.get(Swipe.Start)).isNull()
+            assertThat(actions?.get(Swipe.End)).isNull()
+        }
+
+    @Test
+    @DisableFlags(DualShade.FLAG_NAME)
+    fun actions_communalAvailable_singleShade() =
+        testScope.runTest {
+            kosmos.setCommunalAvailable(true)
+
+            val actions by collectLastValue(underTest.actions)
+
+            setUpState(
+                isShadeTouchable = true,
+                isDeviceUnlocked = false,
+                shadeMode = ShadeMode.Single,
+            )
+            assertThat(actions).isNotEmpty()
+            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
+            assertThat(actions?.get(Swipe.Down))
+                .isEqualTo(UserActionResult(Scenes.Shade, isIrreversible = true))
+            assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
+            assertThat(actions?.get(Swipe.End)).isNull()
+
+            setUpState(
+                isShadeTouchable = false,
+                isDeviceUnlocked = false,
+                shadeMode = ShadeMode.Single,
+            )
+            assertThat(actions).isEmpty()
+
+            setUpState(
+                isShadeTouchable = true,
+                isDeviceUnlocked = true,
+                shadeMode = ShadeMode.Single,
+            )
+            assertThat(actions).isNotEmpty()
+            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
+            assertThat(actions?.get(Swipe.Down))
+                .isEqualTo(UserActionResult(Scenes.Shade, isIrreversible = true))
+            assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
+            assertThat(actions?.get(Swipe.End)).isNull()
+        }
+
+    @Test
+    @DisableFlags(DualShade.FLAG_NAME)
+    fun actions_communalAvailable_splitShade() =
+        testScope.runTest {
+            kosmos.setCommunalAvailable(true)
+
+            val actions by collectLastValue(underTest.actions)
+
+            setUpState(
+                isShadeTouchable = true,
+                isDeviceUnlocked = false,
+                shadeMode = ShadeMode.Split,
+            )
+            assertThat(actions).isNotEmpty()
+            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
+            assertThat(actions?.get(Swipe.Down))
+                .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade, isIrreversible = true))
+            assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
+            assertThat(actions?.get(Swipe.End)).isNull()
+
+            setUpState(
+                isShadeTouchable = false,
+                isDeviceUnlocked = false,
+                shadeMode = ShadeMode.Split,
+            )
+            assertThat(actions).isEmpty()
+
+            setUpState(
+                isShadeTouchable = true,
+                isDeviceUnlocked = true,
+                shadeMode = ShadeMode.Split,
+            )
+            assertThat(actions).isNotEmpty()
+            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
+            assertThat(actions?.get(Swipe.Down))
+                .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade, isIrreversible = true))
+            assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
+            assertThat(actions?.get(Swipe.End)).isNull()
+        }
+
+    @Test
+    @EnableFlags(DualShade.FLAG_NAME)
+    fun actions_communalAvailable_dualShade() =
+        testScope.runTest {
+            kosmos.setCommunalAvailable(true)
+
+            val actions by collectLastValue(underTest.actions)
+
+            setUpState(
+                isShadeTouchable = true,
+                isDeviceUnlocked = false,
+                shadeMode = ShadeMode.Dual,
+            )
+            assertThat(actions).isNotEmpty()
+            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
+            assertThat(actions?.get(Swipe.Down))
+                .isEqualTo(
+                    UserActionResult.ShowOverlay(Overlays.NotificationsShade, isIrreversible = true)
+                )
+            assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
+            assertThat(actions?.get(Swipe.End)).isNull()
+
+            setUpState(
+                isShadeTouchable = false,
+                isDeviceUnlocked = false,
+                shadeMode = ShadeMode.Dual,
+            )
+            assertThat(actions).isEmpty()
+
+            setUpState(isShadeTouchable = true, isDeviceUnlocked = true, shadeMode = ShadeMode.Dual)
+            assertThat(actions).isNotEmpty()
+            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
+            assertThat(actions?.get(Swipe.Down))
+                .isEqualTo(
+                    UserActionResult.ShowOverlay(Overlays.NotificationsShade, isIrreversible = true)
+                )
+            assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
+            assertThat(actions?.get(Swipe.End)).isNull()
         }
 
     private fun TestScope.setUpState(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt
index f045404..0d32b7f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt
@@ -18,36 +18,31 @@
 
 import android.content.Context
 import android.content.Context.INPUT_SERVICE
-import android.hardware.input.InputGestureData
-import android.hardware.input.InputGestureData.createKeyTrigger
-import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS
+import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS
 import android.hardware.input.fakeInputManager
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
-import android.view.KeyEvent.KEYCODE_A
 import android.view.KeyEvent.KEYCODE_SLASH
-import android.view.KeyEvent.META_ALT_ON
 import android.view.KeyEvent.META_CAPS_LOCK_ON
-import android.view.KeyEvent.META_CTRL_ON
-import android.view.KeyEvent.META_FUNCTION_ON
-import android.view.KeyEvent.META_META_LEFT_ON
 import android.view.KeyEvent.META_META_ON
-import android.view.KeyEvent.META_SHIFT_ON
-import android.view.KeyEvent.META_SHIFT_RIGHT_ON
-import android.view.KeyEvent.META_SYM_ON
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.hardware.input.Flags.FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES
 import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult
 import com.android.systemui.keyboard.shortcut.customShortcutCategoriesRepository
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.ALL_SUPPORTED_MODIFIERS
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allAppsInputGestureData
 import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allCustomizableInputGesturesWithSimpleShortcutCombinations
 import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.customizableInputGestureWithUnknownKeyGestureType
 import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.expectedShortcutCategoriesWithSimpleShortcutCombination
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.goHomeInputGestureData
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.standardAddCustomShortcutRequestInfo
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.standardDeleteCustomShortcutRequestInfo
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.standardKeyCombination
 import com.android.systemui.keyboard.shortcut.shared.model.KeyCombination
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
 import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
 import com.android.systemui.kosmos.testScope
@@ -74,24 +69,21 @@
             it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext })
         }
 
-    private val fakeInputManager = kosmos.fakeInputManager
+    private val inputManager = kosmos.fakeInputManager.inputManager
     private val testScope = kosmos.testScope
     private val helper = kosmos.shortcutHelperTestHelper
     private val repo = kosmos.customShortcutCategoriesRepository
 
     @Before
     fun setup() {
-        whenever(mockUserContext.getSystemService(INPUT_SERVICE))
-            .thenReturn(fakeInputManager.inputManager)
+        whenever(mockUserContext.getSystemService(INPUT_SERVICE)).thenReturn(inputManager)
     }
 
     @Test
     @EnableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     fun categories_emitsCorrectlyConvertedShortcutCategories() {
         testScope.runTest {
-            whenever(
-                    fakeInputManager.inputManager.getCustomInputGestures(/* filter= */ anyOrNull())
-                )
+            whenever(inputManager.getCustomInputGestures(/* filter= */ anyOrNull()))
                 .thenReturn(allCustomizableInputGesturesWithSimpleShortcutCombinations)
 
             helper.toggle(deviceId = 123)
@@ -106,9 +98,7 @@
     @DisableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     fun categories_emitsEmptyListWhenFlagIsDisabled() {
         testScope.runTest {
-            whenever(
-                    fakeInputManager.inputManager.getCustomInputGestures(/* filter= */ anyOrNull())
-                )
+            whenever(inputManager.getCustomInputGestures(/* filter= */ anyOrNull()))
                 .thenReturn(allCustomizableInputGesturesWithSimpleShortcutCombinations)
 
             helper.toggle(deviceId = 123)
@@ -122,9 +112,7 @@
     @EnableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     fun categories_ignoresUnknownKeyGestureTypes() {
         testScope.runTest {
-            whenever(
-                    fakeInputManager.inputManager.getCustomInputGestures(/* filter= */ anyOrNull())
-                )
+            whenever(inputManager.getCustomInputGestures(/* filter= */ anyOrNull()))
                 .thenReturn(customizableInputGestureWithUnknownKeyGestureType)
 
             helper.toggle(deviceId = 123)
@@ -151,7 +139,7 @@
             helper.toggle(deviceId = 123)
             val pressedKeys by collectLastValue(repo.pressedKeys)
             repo.updateUserKeyCombination(
-                KeyCombination(modifiers = allSupportedModifiers, keyCode = null)
+                KeyCombination(modifiers = ALL_SUPPORTED_MODIFIERS, keyCode = null)
             )
 
             assertThat(pressedKeys)
@@ -199,11 +187,11 @@
     @Test
     fun shortcutBeingCustomized_updatedOnCustomizationRequested() {
         testScope.runTest {
-            repo.onCustomizationRequested(standardCustomizationRequestInfo)
+            repo.onCustomizationRequested(standardAddCustomShortcutRequestInfo)
 
             val shortcutBeingCustomized = repo.getShortcutBeingCustomized()
 
-            assertThat(shortcutBeingCustomized).isEqualTo(standardCustomizationRequestInfo)
+            assertThat(shortcutBeingCustomized).isEqualTo(standardAddCustomShortcutRequestInfo)
         }
     }
 
@@ -223,7 +211,7 @@
     fun buildInputGestureDataForShortcutBeingCustomized_noKeyCombinationSelected_returnsNull() {
         testScope.runTest {
             helper.toggle(deviceId = 123)
-            repo.onCustomizationRequested(standardCustomizationRequestInfo)
+            repo.onCustomizationRequested(standardAddCustomShortcutRequestInfo)
 
             val inputGestureData = repo.buildInputGestureDataForShortcutBeingCustomized()
 
@@ -235,46 +223,32 @@
     fun buildInputGestureDataForShortcutBeingCustomized_successfullyBuildInputGestureData() {
         testScope.runTest {
             helper.toggle(deviceId = 123)
-            repo.onCustomizationRequested(standardCustomizationRequestInfo)
+            repo.onCustomizationRequested(standardAddCustomShortcutRequestInfo)
             repo.updateUserKeyCombination(standardKeyCombination)
             val inputGestureData = repo.buildInputGestureDataForShortcutBeingCustomized()
 
             // using toString as we're testing for only structural equality not referential.
             // inputGestureData is a java class and isEqual Tests for referential equality
             // as well which would cause this assert to fail
-            assertThat(inputGestureData.toString()).isEqualTo(standardInputGestureData.toString())
+            assertThat(inputGestureData.toString()).isEqualTo(allAppsInputGestureData.toString())
         }
     }
 
-    private val standardCustomizationRequestInfo =
-        ShortcutCustomizationRequestInfo.Add(
-            label = "Open apps list",
-            categoryType = ShortcutCategoryType.System,
-            subCategoryLabel = "System controls",
-        )
+    @Test
+    @EnableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
+    fun deleteShortcut_successfullyRetrievesGestureDataAndDeletesShortcut() {
+        testScope.runTest {
+            whenever(inputManager.getCustomInputGestures(anyOrNull()))
+                .thenReturn(listOf(allAppsInputGestureData, goHomeInputGestureData))
+            whenever(inputManager.removeCustomInputGesture(allAppsInputGestureData))
+                .thenReturn(CUSTOM_INPUT_GESTURE_RESULT_SUCCESS)
 
-    private val standardKeyCombination =
-        KeyCombination(
-            modifiers = META_META_ON or META_SHIFT_ON or META_META_LEFT_ON or META_SHIFT_RIGHT_ON,
-            keyCode = KEYCODE_A,
-        )
+            helper.toggle(deviceId = 123)
+            repo.onCustomizationRequested(standardDeleteCustomShortcutRequestInfo)
 
-    private val allSupportedModifiers =
-        META_META_ON or
-            META_CTRL_ON or
-            META_FUNCTION_ON or
-            META_SHIFT_ON or
-            META_ALT_ON or
-            META_SYM_ON
+            val result = repo.deleteShortcutCurrentlyBeingCustomized()
 
-    private val standardInputGestureData =
-        InputGestureData.Builder()
-            .setKeyGestureType(KEY_GESTURE_TYPE_ALL_APPS)
-            .setTrigger(
-                createKeyTrigger(
-                    /* keycode = */ standardKeyCombination.keyCode!!,
-                    /* modifierState = */ standardKeyCombination.modifiers and allSupportedModifiers,
-                )
-            )
-            .build()
+            assertThat(result).isEqualTo(ShortcutCustomizationRequestResult.SUCCESS)
+        }
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
index a1e7ef4..8466eab 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
@@ -19,9 +19,23 @@
 import android.hardware.input.InputGestureData
 import android.hardware.input.InputGestureData.createKeyTrigger
 import android.hardware.input.KeyGestureEvent
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_HOME
+import android.os.SystemClock
 import android.view.KeyEvent
+import android.view.KeyEvent.ACTION_DOWN
+import android.view.KeyEvent.KEYCODE_A
+import android.view.KeyEvent.META_ALT_ON
+import android.view.KeyEvent.META_CTRL_ON
+import android.view.KeyEvent.META_FUNCTION_ON
+import android.view.KeyEvent.META_META_LEFT_ON
+import android.view.KeyEvent.META_META_ON
+import android.view.KeyEvent.META_SHIFT_ON
+import android.view.KeyEvent.META_SHIFT_RIGHT_ON
+import android.view.KeyEvent.META_SYM_ON
 import android.view.KeyboardShortcutGroup
 import android.view.KeyboardShortcutInfo
+import com.android.systemui.keyboard.shortcut.shared.model.KeyCombination
 import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
@@ -29,9 +43,11 @@
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
 import com.android.systemui.keyboard.shortcut.shared.model.shortcut
+import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState
 import com.android.systemui.res.R
 
 object TestShortcuts {
@@ -525,4 +541,110 @@
                 keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER
             ),
         )
+
+    val standardAddCustomShortcutRequestInfo =
+        ShortcutCustomizationRequestInfo.Add(
+            label = "Open apps list",
+            categoryType = System,
+            subCategoryLabel = "System controls",
+        )
+
+    val standardDeleteCustomShortcutRequestInfo =
+        ShortcutCustomizationRequestInfo.Delete(
+            label = "Open apps list",
+            categoryType = System,
+            subCategoryLabel = "System controls",
+        )
+
+    val standardKeyCombination =
+        KeyCombination(
+            modifiers = META_META_ON or META_SHIFT_ON or META_META_LEFT_ON or META_SHIFT_RIGHT_ON,
+            keyCode = KEYCODE_A,
+        )
+
+    const val ALL_SUPPORTED_MODIFIERS =
+        META_META_ON or
+            META_CTRL_ON or
+            META_FUNCTION_ON or
+            META_SHIFT_ON or
+            META_ALT_ON or
+            META_SYM_ON
+
+    val allAppsInputGestureData: InputGestureData =
+        InputGestureData.Builder()
+            .setKeyGestureType(KEY_GESTURE_TYPE_ALL_APPS)
+            .setTrigger(
+                createKeyTrigger(
+                    /* keycode = */ standardKeyCombination.keyCode!!,
+                    /* modifierState = */ standardKeyCombination.modifiers and
+                        ALL_SUPPORTED_MODIFIERS,
+                )
+            )
+            .build()
+
+    val goHomeInputGestureData: InputGestureData =
+        InputGestureData.Builder()
+            .setKeyGestureType(KEY_GESTURE_TYPE_HOME)
+            .setTrigger(
+                createKeyTrigger(
+                    /* keycode = */ standardKeyCombination.keyCode!!,
+                    /* modifierState = */ standardKeyCombination.modifiers and
+                        ALL_SUPPORTED_MODIFIERS,
+                )
+            )
+            .build()
+
+    val expectedStandardDeleteShortcutUiState =
+        ShortcutCustomizationUiState.DeleteShortcutDialog(isDialogShowing = false)
+
+    val keyDownEventWithoutActionKeyPressed =
+        androidx.compose.ui.input.key.KeyEvent(
+            android.view.KeyEvent(
+                /* downTime = */ SystemClock.uptimeMillis(),
+                /* eventTime = */ SystemClock.uptimeMillis(),
+                /* action = */ ACTION_DOWN,
+                /* code = */ KEYCODE_A,
+                /* repeat = */ 0,
+                /* metaState = */ META_CTRL_ON,
+            )
+        )
+
+    val keyDownEventWithActionKeyPressed =
+        androidx.compose.ui.input.key.KeyEvent(
+            android.view.KeyEvent(
+                /* downTime = */ SystemClock.uptimeMillis(),
+                /* eventTime = */ SystemClock.uptimeMillis(),
+                /* action = */ ACTION_DOWN,
+                /* code = */ KEYCODE_A,
+                /* repeat = */ 0,
+                /* metaState = */ META_CTRL_ON or META_META_ON,
+            )
+        )
+
+    val keyUpEventWithActionKeyPressed =
+        androidx.compose.ui.input.key.KeyEvent(
+            android.view.KeyEvent(
+                /* downTime = */ SystemClock.uptimeMillis(),
+                /* eventTime = */ SystemClock.uptimeMillis(),
+                /* action = */ ACTION_DOWN,
+                /* code = */ KEYCODE_A,
+                /* repeat = */ 0,
+                /* metaState = */ 0,
+            )
+        )
+
+    val standardAddShortcutRequest =
+        ShortcutCustomizationRequestInfo.Add(
+            label = "Standard shortcut",
+            categoryType = ShortcutCategoryType.System,
+            subCategoryLabel = "Standard subcategory",
+        )
+
+    val expectedStandardAddShortcutUiState =
+        ShortcutCustomizationUiState.AddShortcutDialog(
+            shortcutLabel = "Standard shortcut",
+            defaultCustomShortcutModifierKey =
+                ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta),
+            isDialogShowing = false,
+        )
 }
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockAnimations.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockAnimations.kt
new file mode 100644
index 0000000..2df14a8
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockAnimations.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.plugins.clocks
+
+import android.view.View
+import com.android.systemui.plugins.annotations.ProtectedInterface
+
+/** Methods which trigger various clock animations */
+@ProtectedInterface
+interface ClockAnimations {
+    /** Runs an enter animation (if any) */
+    fun enter()
+
+    /** Sets how far into AOD the device currently is. */
+    fun doze(fraction: Float)
+
+    /** Sets how far into the folding animation the device is. */
+    fun fold(fraction: Float)
+
+    /** Runs the battery animation (if any). */
+    fun charge()
+
+    /**
+     * Runs when the clock's position changed during the move animation.
+     *
+     * @param fromLeft the [View.getLeft] position of the clock, before it started moving.
+     * @param direction the direction in which it is moving. A positive number means right, and
+     *   negative means left.
+     * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means
+     *   it finished moving.
+     * @deprecated use {@link #onPositionUpdated(float, float)} instead.
+     */
+    fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float)
+
+    /**
+     * Runs when the clock's position changed during the move animation.
+     *
+     * @param distance is the total distance in pixels to offset the glyphs when animation
+     *   completes. Negative distance means we are animating the position towards the center.
+     * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means
+     *   it finished moving.
+     */
+    fun onPositionUpdated(distance: Float, fraction: Float)
+
+    /**
+     * Runs when swiping clock picker, swipingFraction: 1.0 -> clock is scaled up in the preview,
+     * 0.0 -> clock is scaled down in the shade; previewRatio is previewSize / screenSize
+     */
+    fun onPickerCarouselSwiping(swipingFraction: Float)
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt
new file mode 100644
index 0000000..d84d890
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.plugins.clocks
+
+/**
+ * Exposes the rendering capabilities of this clock to SystemUI so that it can be hosted and render
+ * correctly in SystemUI's process. Ideally all clocks could be rendered identically, but in
+ * practice we different clocks require different behavior from SystemUI.
+ */
+data class ClockConfig(
+    val id: ClockId,
+
+    /** Localized name of the clock */
+    val name: String,
+
+    /** Localized accessibility description for the clock */
+    val description: String,
+
+    /** Transition to AOD should move smartspace like large clock instead of small clock */
+    val useAlternateSmartspaceAODTransition: Boolean = false,
+
+    /** Deprecated version of isReactiveToTone; moved to ClockPickerConfig */
+    @Deprecated("TODO(b/352049256): Remove in favor of ClockPickerConfig.isReactiveToTone")
+    val isReactiveToTone: Boolean = true,
+
+    /** True if the clock is large frame clock, which will use weather in compose. */
+    val useCustomClockScene: Boolean = false,
+)
+
+/** Render configuration options for a specific clock face. */
+data class ClockFaceConfig(
+    /** Expected interval between calls to onTimeTick. Can always reduce to PER_MINUTE in AOD. */
+    val tickRate: ClockTickRate = ClockTickRate.PER_MINUTE,
+
+    /** Call to check whether the clock consumes weather data */
+    val hasCustomWeatherDataDisplay: Boolean = false,
+
+    /**
+     * Whether this clock has a custom position update animation. If true, the keyguard will call
+     * `onPositionUpdated` to notify the clock of a position update animation. If false, a default
+     * animation will be used (e.g. a simple translation).
+     */
+    val hasCustomPositionUpdatedAnimation: Boolean = false,
+
+    /** True if the clock is large frame clock, which will use weatherBlueprint in compose. */
+    val useCustomClockScene: Boolean = false,
+)
+
+/** Tick rates for clocks */
+enum class ClockTickRate(val value: Int) {
+    PER_MINUTE(2), // Update the clock once per minute.
+    PER_SECOND(1), // Update the clock once per second.
+    PER_FRAME(0), // Update the clock every second.
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt
new file mode 100644
index 0000000..32fec32
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.plugins.clocks
+
+import com.android.systemui.plugins.annotations.ProtectedInterface
+import com.android.systemui.plugins.annotations.SimpleProperty
+import java.io.PrintWriter
+
+/** Interface for controlling an active clock */
+@ProtectedInterface
+interface ClockController {
+    @get:SimpleProperty
+    /** A small version of the clock, appropriate for smaller viewports */
+    val smallClock: ClockFaceController
+
+    @get:SimpleProperty
+    /** A large version of the clock, appropriate when a bigger viewport is available */
+    val largeClock: ClockFaceController
+
+    @get:SimpleProperty
+    /** Determines the way the hosting app should behave when rendering either clock face */
+    val config: ClockConfig
+
+    @get:SimpleProperty
+    /** Events that clocks may need to respond to */
+    val events: ClockEvents
+
+    /** Initializes various rendering parameters. If never called, provides reasonable defaults. */
+    fun initialize(isDarkTheme: Boolean, dozeFraction: Float, foldFraction: Float)
+
+    /** Optional method for dumping debug information */
+    fun dump(pw: PrintWriter)
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockEvents.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockEvents.kt
new file mode 100644
index 0000000..235475f
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockEvents.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.plugins.clocks
+
+import com.android.systemui.plugins.annotations.ProtectedInterface
+import com.android.systemui.plugins.annotations.ProtectedReturn
+import java.util.Locale
+import java.util.TimeZone
+
+/** Events that should call when various rendering parameters change */
+@ProtectedInterface
+interface ClockEvents {
+    @get:ProtectedReturn("return false;")
+    /** Set to enable or disable swipe interaction */
+    var isReactiveTouchInteractionEnabled: Boolean // TODO(b/364664388): Remove/Rename
+
+    /** Call whenever timezone changes */
+    fun onTimeZoneChanged(timeZone: TimeZone)
+
+    /** Call whenever the text time format changes (12hr vs 24hr) */
+    fun onTimeFormatChanged(is24Hr: Boolean)
+
+    /** Call whenever the locale changes */
+    fun onLocaleChanged(locale: Locale)
+
+    /** Call whenever the weather data should update */
+    fun onWeatherDataChanged(data: WeatherData)
+
+    /** Call with alarm information */
+    fun onAlarmDataChanged(data: AlarmData)
+
+    /** Call with zen/dnd information */
+    fun onZenDataChanged(data: ZenData)
+
+    /** Update reactive axes for this clock */
+    fun onFontAxesChanged(axes: List<ClockFontAxisSetting>)
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceController.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceController.kt
new file mode 100644
index 0000000..8a023f1
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceController.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.plugins.clocks
+
+import android.view.View
+import com.android.systemui.plugins.annotations.ProtectedInterface
+import com.android.systemui.plugins.annotations.SimpleProperty
+
+/** Interface for a specific clock face version rendered by the clock */
+@ProtectedInterface
+interface ClockFaceController {
+    @get:SimpleProperty
+    @Deprecated("Prefer use of layout")
+    /** View that renders the clock face */
+    val view: View
+
+    @get:SimpleProperty
+    /** Layout specification for this clock */
+    val layout: ClockFaceLayout
+
+    @get:SimpleProperty
+    /** Determines the way the hosting app should behave when rendering this clock face */
+    val config: ClockFaceConfig
+
+    @get:SimpleProperty
+    /** Current theme information the clock is using */
+    val theme: ThemeConfig
+
+    @get:SimpleProperty
+    /** Events specific to this clock face */
+    val events: ClockFaceEvents
+
+    @get:SimpleProperty
+    /** Triggers for various animations */
+    val animations: ClockAnimations
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt
new file mode 100644
index 0000000..029e546
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.plugins.clocks
+
+import android.graphics.Rect
+import com.android.systemui.plugins.annotations.ProtectedInterface
+
+/** Events that have specific data about the related face */
+@ProtectedInterface
+interface ClockFaceEvents {
+    /** Call every tick to update the rendered time */
+    fun onTimeTick()
+
+    /**
+     * Call whenever the theme or seedColor is updated
+     *
+     * Theme can be specific to the clock face.
+     * - isDarkTheme -> clock should be light
+     * - !isDarkTheme -> clock should be dark
+     */
+    fun onThemeChanged(theme: ThemeConfig)
+
+    /**
+     * Call whenever font settings change. Pass in a target font size in pixels. The specific clock
+     * design is allowed to ignore this target size on a case-by-case basis.
+     */
+    fun onFontSettingChanged(fontSizePx: Float)
+
+    /**
+     * Target region information for the clock face. For small clock, this will match the bounds of
+     * the parent view mostly, but have a target height based on the height of the default clock.
+     * For large clocks, the parent view is the entire device size, but most clocks will want to
+     * render within the centered targetRect to avoid obstructing other elements. The specified
+     * targetRegion is relative to the parent view.
+     */
+    fun onTargetRegionChanged(targetRegion: Rect?)
+
+    /** Called to notify the clock about its display. */
+    fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean)
+}
+
+/** Contains Theming information for the clock face */
+data class ThemeConfig(
+    /** True if the clock should use dark theme (light text on dark background) */
+    val isDarkTheme: Boolean,
+
+    /**
+     * A clock specific seed color to use when theming, if any was specified by the user. A null
+     * value denotes that we should use the seed color for the current system theme.
+     */
+    val seedColor: Int?,
+)
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceLayout.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceLayout.kt
new file mode 100644
index 0000000..fb5ef02
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceLayout.kt
@@ -0,0 +1,174 @@
+/*
+ * 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.plugins.clocks
+
+import android.content.Context
+import android.util.DisplayMetrics
+import android.view.View
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
+import androidx.constraintlayout.widget.ConstraintSet.END
+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 com.android.internal.policy.SystemBarUtils
+import com.android.systemui.plugins.annotations.GeneratedImport
+import com.android.systemui.plugins.annotations.ProtectedInterface
+import com.android.systemui.plugins.annotations.ProtectedReturn
+
+/** Specifies layout information for the clock face */
+@ProtectedInterface
+@GeneratedImport("java.util.ArrayList")
+@GeneratedImport("android.view.View")
+interface ClockFaceLayout {
+    @get:ProtectedReturn("return new ArrayList<View>();")
+    /** All clock views to add to the root constraint layout before applying constraints. */
+    val views: List<View>
+
+    @ProtectedReturn("return constraints;")
+    /** Custom constraints to apply to Lockscreen ConstraintLayout. */
+    fun applyConstraints(constraints: ConstraintSet): ConstraintSet
+
+    @ProtectedReturn("return constraints;")
+    /** Custom constraints to apply to preview ConstraintLayout. */
+    fun applyPreviewConstraints(
+        clockPreviewConfig: ClockPreviewConfig,
+        constraints: ConstraintSet,
+    ): ConstraintSet
+
+    /** Apply specified AOD BurnIn parameters to this layout */
+    fun applyAodBurnIn(aodBurnInModel: AodClockBurnInModel)
+}
+
+/** Data class to contain AOD BurnIn information for correct aod rendering */
+data class AodClockBurnInModel(
+    /** Scale that the clock should render at to mitigate burnin */
+    val scale: Float,
+
+    /** X-Translation for the clock to mitigate burnin */
+    val translationX: Float,
+
+    /** Y-Translation for the clock to mitigate burnin */
+    val translationY: Float,
+)
+
+/** A ClockFaceLayout that applies the default lockscreen layout to a single view */
+class DefaultClockFaceLayout(val view: View) : ClockFaceLayout {
+    override val views = listOf(view)
+
+    override fun applyConstraints(constraints: ConstraintSet): ConstraintSet {
+        if (views.size != 1) {
+            throw IllegalArgumentException(
+                "Should have only one container view when using DefaultClockFaceLayout"
+            )
+        }
+        return constraints
+    }
+
+    override fun applyPreviewConstraints(
+        clockPreviewConfig: ClockPreviewConfig,
+        constraints: ConstraintSet,
+    ): ConstraintSet {
+        return applyDefaultPreviewConstraints(clockPreviewConfig, constraints)
+    }
+
+    override fun applyAodBurnIn(aodBurnInModel: AodClockBurnInModel) {
+        // Default clock doesn't need detailed control of view
+    }
+
+    companion object {
+        fun applyDefaultPreviewConstraints(
+            clockPreviewConfig: ClockPreviewConfig,
+            constraints: ConstraintSet,
+        ): ConstraintSet {
+            constraints.apply {
+                val context = clockPreviewConfig.previewContext
+                val lockscreenClockViewLargeId = getId(context, "lockscreen_clock_view_large")
+                constrainWidth(lockscreenClockViewLargeId, WRAP_CONTENT)
+                constrainHeight(lockscreenClockViewLargeId, WRAP_CONTENT)
+                constrainMaxHeight(lockscreenClockViewLargeId, 0)
+
+                val largeClockTopMargin =
+                    SystemBarUtils.getStatusBarHeight(context) +
+                        getDimen(context, "small_clock_padding_top") +
+                        getDimen(context, "keyguard_smartspace_top_offset") +
+                        getDimen(context, "date_weather_view_height") +
+                        getDimen(context, "enhanced_smartspace_height")
+                connect(lockscreenClockViewLargeId, TOP, PARENT_ID, TOP, largeClockTopMargin)
+                connect(lockscreenClockViewLargeId, START, PARENT_ID, START)
+                connect(lockscreenClockViewLargeId, END, PARENT_ID, END)
+
+                // In preview, we'll show UDFPS icon for UDFPS devices
+                // and nothing for non-UDFPS devices,
+                // and we're not planning to add this vide in clockHostView
+                // so we only need position of device entry icon to constrain clock
+                // Copied calculation codes from applyConstraints in DefaultDeviceEntrySection
+                val bottomPaddingPx = getDimen(context, "lock_icon_margin_bottom")
+                val defaultDensity =
+                    DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() /
+                        DisplayMetrics.DENSITY_DEFAULT.toFloat()
+                val lockIconRadiusPx = (defaultDensity * 36).toInt()
+                val clockBottomMargin = bottomPaddingPx + 2 * lockIconRadiusPx
+
+                connect(lockscreenClockViewLargeId, BOTTOM, PARENT_ID, BOTTOM, clockBottomMargin)
+                val smallClockViewId = getId(context, "lockscreen_clock_view")
+                constrainWidth(smallClockViewId, WRAP_CONTENT)
+                constrainHeight(smallClockViewId, getDimen(context, "small_clock_height"))
+                connect(
+                    smallClockViewId,
+                    START,
+                    PARENT_ID,
+                    START,
+                    getDimen(context, "clock_padding_start") +
+                        getDimen(context, "status_view_margin_horizontal"),
+                )
+                val smallClockTopMargin =
+                    getSmallClockTopPadding(
+                        clockPreviewConfig = clockPreviewConfig,
+                        SystemBarUtils.getStatusBarHeight(context),
+                    )
+                connect(smallClockViewId, TOP, PARENT_ID, TOP, smallClockTopMargin)
+            }
+            return constraints
+        }
+
+        fun getId(context: Context, name: String): Int {
+            val packageName = context.packageName
+            val res = context.packageManager.getResourcesForApplication(packageName)
+            val id = res.getIdentifier(name, "id", packageName)
+            return id
+        }
+
+        fun getDimen(context: Context, name: String): Int {
+            val packageName = context.packageName
+            val res = context.resources
+            val id = res.getIdentifier(name, "dimen", packageName)
+            return if (id == 0) 0 else res.getDimensionPixelSize(id)
+        }
+
+        fun getSmallClockTopPadding(
+            clockPreviewConfig: ClockPreviewConfig,
+            statusBarHeight: Int,
+        ): Int {
+            return if (clockPreviewConfig.isShadeLayoutWide) {
+                getDimen(clockPreviewConfig.previewContext, "keyguard_split_shade_top_margin") -
+                    if (clockPreviewConfig.isSceneContainerFlagEnabled) statusBarHeight else 0
+            } else {
+                getDimen(clockPreviewConfig.previewContext, "keyguard_clock_top_margin") +
+                    if (!clockPreviewConfig.isSceneContainerFlagEnabled) statusBarHeight else 0
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockMessageBuffers.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockMessageBuffers.kt
new file mode 100644
index 0000000..bec589a
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockMessageBuffers.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.plugins.clocks
+
+import com.android.systemui.log.core.MessageBuffer
+
+/** MessageBuffers for clocks that want to log information to SystemUI dumps */
+data class ClockMessageBuffers(
+    /** Message buffer for general infrastructure */
+    val infraMessageBuffer: MessageBuffer,
+
+    /** Message buffer for small clock rendering */
+    val smallClockMessageBuffer: MessageBuffer,
+
+    /** Message buffer for large clock rendering */
+    val largeClockMessageBuffer: MessageBuffer,
+) {
+    constructor(buffer: MessageBuffer) : this(buffer, buffer, buffer) {}
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt
new file mode 100644
index 0000000..1bc9367
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.plugins.clocks
+
+import android.graphics.drawable.Drawable
+
+data class ClockPickerConfig
+@JvmOverloads
+constructor(
+    val id: String,
+
+    /** Localized name of the clock */
+    val name: String,
+
+    /** Localized accessibility description for the clock */
+    val description: String,
+
+    /* Static & lightweight thumbnail version of the clock */
+    val thumbnail: Drawable,
+
+    /** True if the clock will react to tone changes in the seed color */
+    val isReactiveToTone: Boolean = true,
+
+    /** Font axes that can be modified on this clock */
+    val axes: List<ClockFontAxis> = listOf(),
+)
+
+/** Represents an Axis that can be modified */
+data class ClockFontAxis(
+    /** Axis key, not user renderable */
+    val key: String,
+
+    /** Intended mode of user interaction */
+    val type: AxisType,
+
+    /** Maximum value the axis supports */
+    val maxValue: Float,
+
+    /** Minimum value the axis supports */
+    val minValue: Float,
+
+    /** Current value the axis is set to */
+    val currentValue: Float,
+
+    /** User-renderable name of the axis */
+    val name: String,
+
+    /** Description of the axis */
+    val description: String,
+) {
+    fun toSetting() = ClockFontAxisSetting(key, currentValue)
+
+    companion object {
+        fun merge(
+            fontAxes: List<ClockFontAxis>,
+            axisSettings: List<ClockFontAxisSetting>,
+        ): List<ClockFontAxis> {
+            val result = mutableListOf<ClockFontAxis>()
+            for (axis in fontAxes) {
+                val setting = axisSettings.firstOrNull { axis.key == it.key }
+                val output = setting?.let { axis.copy(currentValue = it.value) } ?: axis
+                result.add(output)
+            }
+            return result
+        }
+    }
+}
+
+/** Axis user interaction modes */
+enum class AxisType {
+    /** Continuous range between minValue & maxValue. */
+    Float,
+
+    /** Only minValue & maxValue are valid. No intermediate values between them are allowed. */
+    Boolean,
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
index 8ea5725..7426f06 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
@@ -13,35 +13,11 @@
  */
 package com.android.systemui.plugins.clocks
 
-import android.content.Context
-import android.graphics.Rect
-import android.graphics.drawable.Drawable
-import android.util.DisplayMetrics
-import android.view.View
-import androidx.constraintlayout.widget.ConstraintSet
-import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
-import androidx.constraintlayout.widget.ConstraintSet.END
-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 com.android.internal.annotations.Keep
-import com.android.internal.policy.SystemBarUtils
-import com.android.systemui.log.core.MessageBuffer
 import com.android.systemui.plugins.Plugin
 import com.android.systemui.plugins.annotations.GeneratedImport
 import com.android.systemui.plugins.annotations.ProtectedInterface
 import com.android.systemui.plugins.annotations.ProtectedReturn
 import com.android.systemui.plugins.annotations.ProvidesInterface
-import com.android.systemui.plugins.annotations.SimpleProperty
-import java.io.PrintWriter
-import java.util.Locale
-import java.util.TimeZone
-import org.json.JSONArray
-import org.json.JSONObject
-
-/** Identifies a clock design */
-typealias ClockId = String
 
 /** A Plugin which exposes the ClockProvider interface */
 @ProtectedInterface
@@ -74,528 +50,8 @@
     fun getClockPickerConfig(settings: ClockSettings): ClockPickerConfig
 }
 
-/** Interface for controlling an active clock */
-@ProtectedInterface
-interface ClockController {
-    @get:SimpleProperty
-    /** A small version of the clock, appropriate for smaller viewports */
-    val smallClock: ClockFaceController
-
-    @get:SimpleProperty
-    /** A large version of the clock, appropriate when a bigger viewport is available */
-    val largeClock: ClockFaceController
-
-    @get:SimpleProperty
-    /** Determines the way the hosting app should behave when rendering either clock face */
-    val config: ClockConfig
-
-    @get:SimpleProperty
-    /** Events that clocks may need to respond to */
-    val events: ClockEvents
-
-    /** Initializes various rendering parameters. If never called, provides reasonable defaults. */
-    fun initialize(isDarkTheme: Boolean, dozeFraction: Float, foldFraction: Float)
-
-    /** Optional method for dumping debug information */
-    fun dump(pw: PrintWriter)
-}
-
-/** Interface for a specific clock face version rendered by the clock */
-@ProtectedInterface
-interface ClockFaceController {
-    @get:SimpleProperty
-    @Deprecated("Prefer use of layout")
-    /** View that renders the clock face */
-    val view: View
-
-    @get:SimpleProperty
-    /** Layout specification for this clock */
-    val layout: ClockFaceLayout
-
-    @get:SimpleProperty
-    /** Determines the way the hosting app should behave when rendering this clock face */
-    val config: ClockFaceConfig
-
-    @get:SimpleProperty
-    /** Current theme information the clock is using */
-    val theme: ThemeConfig
-
-    @get:SimpleProperty
-    /** Events specific to this clock face */
-    val events: ClockFaceEvents
-
-    @get:SimpleProperty
-    /** Triggers for various animations */
-    val animations: ClockAnimations
-}
-
-/** For clocks that want to report debug information */
-data class ClockMessageBuffers(
-    /** Message buffer for general infra */
-    val infraMessageBuffer: MessageBuffer,
-
-    /** Message buffer for small clock renering */
-    val smallClockMessageBuffer: MessageBuffer,
-
-    /** Message buffer for large clock rendering */
-    val largeClockMessageBuffer: MessageBuffer,
-) {
-    constructor(buffer: MessageBuffer) : this(buffer, buffer, buffer) {}
-}
-
-data class AodClockBurnInModel(val scale: Float, val translationX: Float, val translationY: Float)
-
-/** Specifies layout information for the clock face */
-@ProtectedInterface
-@GeneratedImport("java.util.ArrayList")
-@GeneratedImport("android.view.View")
-interface ClockFaceLayout {
-    @get:ProtectedReturn("return new ArrayList<View>();")
-    /** All clock views to add to the root constraint layout before applying constraints. */
-    val views: List<View>
-
-    @ProtectedReturn("return constraints;")
-    /** Custom constraints to apply to Lockscreen ConstraintLayout. */
-    fun applyConstraints(constraints: ConstraintSet): ConstraintSet
-
-    @ProtectedReturn("return constraints;")
-    /** Custom constraints to apply to preview ConstraintLayout. */
-    fun applyPreviewConstraints(
-        clockPreviewConfig: ClockPreviewConfig,
-        constraints: ConstraintSet,
-    ): ConstraintSet
-
-    fun applyAodBurnIn(aodBurnInModel: AodClockBurnInModel)
-}
-
-/** A ClockFaceLayout that applies the default lockscreen layout to a single view */
-class DefaultClockFaceLayout(val view: View) : ClockFaceLayout {
-    // both small and large clock should have a container (RelativeLayout in
-    // SimpleClockFaceController)
-    override val views = listOf(view)
-
-    override fun applyConstraints(constraints: ConstraintSet): ConstraintSet {
-        if (views.size != 1) {
-            throw IllegalArgumentException(
-                "Should have only one container view when using DefaultClockFaceLayout"
-            )
-        }
-        return constraints
-    }
-
-    override fun applyPreviewConstraints(
-        clockPreviewConfig: ClockPreviewConfig,
-        constraints: ConstraintSet,
-    ): ConstraintSet {
-        return applyDefaultPreviewConstraints(clockPreviewConfig, constraints)
-    }
-
-    override fun applyAodBurnIn(aodBurnInModel: AodClockBurnInModel) {
-        // Default clock doesn't need detailed control of view
-    }
-
-    companion object {
-        fun applyDefaultPreviewConstraints(
-            clockPreviewConfig: ClockPreviewConfig,
-            constraints: ConstraintSet,
-        ): ConstraintSet {
-            constraints.apply {
-                val context = clockPreviewConfig.previewContext
-                val lockscreenClockViewLargeId = getId(context, "lockscreen_clock_view_large")
-                constrainWidth(lockscreenClockViewLargeId, WRAP_CONTENT)
-                constrainHeight(lockscreenClockViewLargeId, WRAP_CONTENT)
-                constrainMaxHeight(lockscreenClockViewLargeId, 0)
-
-                val largeClockTopMargin =
-                    SystemBarUtils.getStatusBarHeight(context) +
-                        getDimen(context, "small_clock_padding_top") +
-                        getDimen(context, "keyguard_smartspace_top_offset") +
-                        getDimen(context, "date_weather_view_height") +
-                        getDimen(context, "enhanced_smartspace_height")
-                connect(lockscreenClockViewLargeId, TOP, PARENT_ID, TOP, largeClockTopMargin)
-                connect(lockscreenClockViewLargeId, START, PARENT_ID, START)
-                connect(lockscreenClockViewLargeId, END, PARENT_ID, END)
-
-                // In preview, we'll show UDFPS icon for UDFPS devices
-                // and nothing for non-UDFPS devices,
-                // and we're not planning to add this vide in clockHostView
-                // so we only need position of device entry icon to constrain clock
-                // Copied calculation codes from applyConstraints in DefaultDeviceEntrySection
-                val bottomPaddingPx = getDimen(context, "lock_icon_margin_bottom")
-                val defaultDensity =
-                    DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() /
-                        DisplayMetrics.DENSITY_DEFAULT.toFloat()
-                val lockIconRadiusPx = (defaultDensity * 36).toInt()
-                val clockBottomMargin = bottomPaddingPx + 2 * lockIconRadiusPx
-
-                connect(lockscreenClockViewLargeId, BOTTOM, PARENT_ID, BOTTOM, clockBottomMargin)
-                val smallClockViewId = getId(context, "lockscreen_clock_view")
-                constrainWidth(smallClockViewId, WRAP_CONTENT)
-                constrainHeight(smallClockViewId, getDimen(context, "small_clock_height"))
-                connect(
-                    smallClockViewId,
-                    START,
-                    PARENT_ID,
-                    START,
-                    getDimen(context, "clock_padding_start") +
-                        getDimen(context, "status_view_margin_horizontal"),
-                )
-                val smallClockTopMargin =
-                    getSmallClockTopPadding(
-                        clockPreviewConfig = clockPreviewConfig,
-                        SystemBarUtils.getStatusBarHeight(context),
-                    )
-                connect(smallClockViewId, TOP, PARENT_ID, TOP, smallClockTopMargin)
-            }
-            return constraints
-        }
-
-        fun getId(context: Context, name: String): Int {
-            val packageName = context.packageName
-            val res = context.packageManager.getResourcesForApplication(packageName)
-            val id = res.getIdentifier(name, "id", packageName)
-            return id
-        }
-
-        fun getDimen(context: Context, name: String): Int {
-            val packageName = context.packageName
-            val res = context.resources
-            val id = res.getIdentifier(name, "dimen", packageName)
-            return if (id == 0) 0 else res.getDimensionPixelSize(id)
-        }
-
-        fun getSmallClockTopPadding(
-            clockPreviewConfig: ClockPreviewConfig,
-            statusBarHeight: Int,
-        ): Int {
-            return if (clockPreviewConfig.isShadeLayoutWide) {
-                getDimen(clockPreviewConfig.previewContext, "keyguard_split_shade_top_margin") -
-                    if (clockPreviewConfig.isSceneContainerFlagEnabled) statusBarHeight else 0
-            } else {
-                getDimen(clockPreviewConfig.previewContext, "keyguard_clock_top_margin") +
-                    if (!clockPreviewConfig.isSceneContainerFlagEnabled) statusBarHeight else 0
-            }
-        }
-    }
-}
-
-/** Events that should call when various rendering parameters change */
-@ProtectedInterface
-interface ClockEvents {
-    @get:ProtectedReturn("return false;")
-    /** Set to enable or disable swipe interaction */
-    var isReactiveTouchInteractionEnabled: Boolean // TODO(b/364664388): Remove/Rename
-
-    /** Call whenever timezone changes */
-    fun onTimeZoneChanged(timeZone: TimeZone)
-
-    /** Call whenever the text time format changes (12hr vs 24hr) */
-    fun onTimeFormatChanged(is24Hr: Boolean)
-
-    /** Call whenever the locale changes */
-    fun onLocaleChanged(locale: Locale)
-
-    /** Call whenever the weather data should update */
-    fun onWeatherDataChanged(data: WeatherData)
-
-    /** Call with alarm information */
-    fun onAlarmDataChanged(data: AlarmData)
-
-    /** Call with zen/dnd information */
-    fun onZenDataChanged(data: ZenData)
-
-    /** Update reactive axes for this clock */
-    fun onFontAxesChanged(axes: List<ClockFontAxisSetting>)
-}
-
-/** Axis setting value for a clock */
-data class ClockFontAxisSetting(
-    /** Axis key; matches ClockFontAxis.key */
-    val key: String,
-
-    /** Value to set this axis to */
-    val value: Float,
-) {
-    companion object {
-        private val KEY_AXIS_KEY = "key"
-        private val KEY_AXIS_VALUE = "value"
-
-        fun toJson(setting: ClockFontAxisSetting): JSONObject {
-            return JSONObject().apply {
-                put(KEY_AXIS_KEY, setting.key)
-                put(KEY_AXIS_VALUE, setting.value)
-            }
-        }
-
-        fun toJson(settings: List<ClockFontAxisSetting>): JSONArray {
-            return JSONArray().apply {
-                for (axis in settings) {
-                    put(toJson(axis))
-                }
-            }
-        }
-
-        fun fromJson(jsonObj: JSONObject): ClockFontAxisSetting {
-            return ClockFontAxisSetting(
-                key = jsonObj.getString(KEY_AXIS_KEY),
-                value = jsonObj.getDouble(KEY_AXIS_VALUE).toFloat(),
-            )
-        }
-
-        fun fromJson(jsonArray: JSONArray): List<ClockFontAxisSetting> {
-            val result = mutableListOf<ClockFontAxisSetting>()
-            for (i in 0..jsonArray.length() - 1) {
-                val obj = jsonArray.getJSONObject(i)
-                if (obj == null) continue
-                result.add(fromJson(obj))
-            }
-            return result
-        }
-
-        fun toFVar(settings: List<ClockFontAxisSetting>): String {
-            val sb = StringBuilder()
-            for (axis in settings) {
-                if (sb.length > 0) sb.append(", ")
-                sb.append("'${axis.key}' ${axis.value.toInt()}")
-            }
-            return sb.toString()
-        }
-    }
-}
-
-/** Methods which trigger various clock animations */
-@ProtectedInterface
-interface ClockAnimations {
-    /** Runs an enter animation (if any) */
-    fun enter()
-
-    /** Sets how far into AOD the device currently is. */
-    fun doze(fraction: Float)
-
-    /** Sets how far into the folding animation the device is. */
-    fun fold(fraction: Float)
-
-    /** Runs the battery animation (if any). */
-    fun charge()
-
-    /**
-     * Runs when the clock's position changed during the move animation.
-     *
-     * @param fromLeft the [View.getLeft] position of the clock, before it started moving.
-     * @param direction the direction in which it is moving. A positive number means right, and
-     *   negative means left.
-     * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means
-     *   it finished moving.
-     * @deprecated use {@link #onPositionUpdated(float, float)} instead.
-     */
-    fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float)
-
-    /**
-     * Runs when the clock's position changed during the move animation.
-     *
-     * @param distance is the total distance in pixels to offset the glyphs when animation
-     *   completes. Negative distance means we are animating the position towards the center.
-     * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means
-     *   it finished moving.
-     */
-    fun onPositionUpdated(distance: Float, fraction: Float)
-
-    /**
-     * Runs when swiping clock picker, swipingFraction: 1.0 -> clock is scaled up in the preview,
-     * 0.0 -> clock is scaled down in the shade; previewRatio is previewSize / screenSize
-     */
-    fun onPickerCarouselSwiping(swipingFraction: Float)
-}
-
-/** Events that have specific data about the related face */
-@ProtectedInterface
-interface ClockFaceEvents {
-    /** Call every time tick */
-    fun onTimeTick()
-
-    /**
-     * Call whenever the theme or seedColor is updated
-     *
-     * Theme can be specific to the clock face.
-     * - isDarkTheme -> clock should be light
-     * - !isDarkTheme -> clock should be dark
-     */
-    fun onThemeChanged(theme: ThemeConfig)
-
-    /**
-     * Call whenever font settings change. Pass in a target font size in pixels. The specific clock
-     * design is allowed to ignore this target size on a case-by-case basis.
-     */
-    fun onFontSettingChanged(fontSizePx: Float)
-
-    /**
-     * Target region information for the clock face. For small clock, this will match the bounds of
-     * the parent view mostly, but have a target height based on the height of the default clock.
-     * For large clocks, the parent view is the entire device size, but most clocks will want to
-     * render within the centered targetRect to avoid obstructing other elements. The specified
-     * targetRegion is relative to the parent view.
-     */
-    fun onTargetRegionChanged(targetRegion: Rect?)
-
-    /** Called to notify the clock about its display. */
-    fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean)
-}
-
-data class ThemeConfig(val isDarkTheme: Boolean, val seedColor: Int?)
-
-/** Tick rates for clocks */
-enum class ClockTickRate(val value: Int) {
-    PER_MINUTE(2), // Update the clock once per minute.
-    PER_SECOND(1), // Update the clock once per second.
-    PER_FRAME(0), // Update the clock every second.
-}
+/** Identifies a clock design */
+typealias ClockId = String
 
 /** Some data about a clock design */
 data class ClockMetadata(val clockId: ClockId)
-
-data class ClockPickerConfig
-@JvmOverloads
-constructor(
-    val id: String,
-
-    /** Localized name of the clock */
-    val name: String,
-
-    /** Localized accessibility description for the clock */
-    val description: String,
-
-    /* Static & lightweight thumbnail version of the clock */
-    val thumbnail: Drawable,
-
-    /** True if the clock will react to tone changes in the seed color */
-    val isReactiveToTone: Boolean = true,
-
-    /** Font axes that can be modified on this clock */
-    val axes: List<ClockFontAxis> = listOf(),
-)
-
-/** Represents an Axis that can be modified */
-data class ClockFontAxis(
-    /** Axis key, not user renderable */
-    val key: String,
-
-    /** Intended mode of user interaction */
-    val type: AxisType,
-
-    /** Maximum value the axis supports */
-    val maxValue: Float,
-
-    /** Minimum value the axis supports */
-    val minValue: Float,
-
-    /** Current value the axis is set to */
-    val currentValue: Float,
-
-    /** User-renderable name of the axis */
-    val name: String,
-
-    /** Description of the axis */
-    val description: String,
-) {
-    fun toSetting() = ClockFontAxisSetting(key, currentValue)
-
-    companion object {
-        fun merge(
-            fontAxes: List<ClockFontAxis>,
-            axisSettings: List<ClockFontAxisSetting>,
-        ): List<ClockFontAxis> {
-            val result = mutableListOf<ClockFontAxis>()
-            for (axis in fontAxes) {
-                val setting = axisSettings.firstOrNull { axis.key == it.key }
-                val output = setting?.let { axis.copy(currentValue = it.value) } ?: axis
-                result.add(output)
-            }
-            return result
-        }
-    }
-}
-
-/** Axis user interaction modes */
-enum class AxisType {
-    /** Continuous range between minValue & maxValue. */
-    Float,
-
-    /** Only minValue & maxValue are valid. No intermediate values between them are allowed. */
-    Boolean,
-}
-
-/** Render configuration for the full clock. Modifies the way systemUI behaves with this clock. */
-data class ClockConfig(
-    val id: String,
-
-    /** Localized name of the clock */
-    val name: String,
-
-    /** Localized accessibility description for the clock */
-    val description: String,
-
-    /** Transition to AOD should move smartspace like large clock instead of small clock */
-    val useAlternateSmartspaceAODTransition: Boolean = false,
-
-    /** Deprecated version of isReactiveToTone; moved to ClockPickerConfig */
-    @Deprecated("TODO(b/352049256): Remove in favor of ClockPickerConfig.isReactiveToTone")
-    val isReactiveToTone: Boolean = true,
-
-    /** True if the clock is large frame clock, which will use weather in compose. */
-    val useCustomClockScene: Boolean = false,
-)
-
-/** Render configuration options for a clock face. Modifies the way SystemUI behaves. */
-data class ClockFaceConfig(
-    /** Expected interval between calls to onTimeTick. Can always reduce to PER_MINUTE in AOD. */
-    val tickRate: ClockTickRate = ClockTickRate.PER_MINUTE,
-
-    /** Call to check whether the clock consumes weather data */
-    val hasCustomWeatherDataDisplay: Boolean = false,
-
-    /**
-     * Whether this clock has a custom position update animation. If true, the keyguard will call
-     * `onPositionUpdated` to notify the clock of a position update animation. If false, a default
-     * animation will be used (e.g. a simple translation).
-     */
-    val hasCustomPositionUpdatedAnimation: Boolean = false,
-
-    /** True if the clock is large frame clock, which will use weatherBlueprint in compose. */
-    val useCustomClockScene: Boolean = false,
-)
-
-/** Structure for keeping clock-specific settings */
-@Keep
-data class ClockSettings(
-    val clockId: ClockId? = null,
-    val seedColor: Int? = null,
-    val axes: List<ClockFontAxisSetting> = listOf(),
-) {
-    // Exclude metadata from equality checks
-    var metadata: JSONObject = JSONObject()
-
-    companion object {
-        private val KEY_CLOCK_ID = "clockId"
-        private val KEY_SEED_COLOR = "seedColor"
-        private val KEY_METADATA = "metadata"
-        private val KEY_AXIS_LIST = "axes"
-
-        fun toJson(setting: ClockSettings): JSONObject {
-            return JSONObject().apply {
-                put(KEY_CLOCK_ID, setting.clockId)
-                put(KEY_SEED_COLOR, setting.seedColor)
-                put(KEY_METADATA, setting.metadata)
-                put(KEY_AXIS_LIST, ClockFontAxisSetting.toJson(setting.axes))
-            }
-        }
-
-        fun fromJson(json: JSONObject): ClockSettings {
-            val clockId = if (!json.isNull(KEY_CLOCK_ID)) json.getString(KEY_CLOCK_ID) else null
-            val seedColor = if (!json.isNull(KEY_SEED_COLOR)) json.getInt(KEY_SEED_COLOR) else null
-            val axisList = json.optJSONArray(KEY_AXIS_LIST)?.let(ClockFontAxisSetting::fromJson)
-            return ClockSettings(clockId, seedColor, axisList ?: listOf()).apply {
-                metadata = json.optJSONObject(KEY_METADATA) ?: JSONObject()
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockSettings.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockSettings.kt
new file mode 100644
index 0000000..6128c00
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockSettings.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.plugins.clocks
+
+import com.android.internal.annotations.Keep
+import org.json.JSONArray
+import org.json.JSONObject
+
+@Keep
+/** Structure for keeping clock-specific settings */
+data class ClockSettings(
+    val clockId: ClockId? = null,
+    val seedColor: Int? = null,
+    val axes: List<ClockFontAxisSetting> = listOf(),
+) {
+    // Exclude metadata from equality checks
+    var metadata: JSONObject = JSONObject()
+
+    companion object {
+        private val KEY_CLOCK_ID = "clockId"
+        private val KEY_SEED_COLOR = "seedColor"
+        private val KEY_METADATA = "metadata"
+        private val KEY_AXIS_LIST = "axes"
+
+        fun toJson(setting: ClockSettings): JSONObject {
+            return JSONObject().apply {
+                put(KEY_CLOCK_ID, setting.clockId)
+                put(KEY_SEED_COLOR, setting.seedColor)
+                put(KEY_METADATA, setting.metadata)
+                put(KEY_AXIS_LIST, ClockFontAxisSetting.toJson(setting.axes))
+            }
+        }
+
+        fun fromJson(json: JSONObject): ClockSettings {
+            val clockId = if (!json.isNull(KEY_CLOCK_ID)) json.getString(KEY_CLOCK_ID) else null
+            val seedColor = if (!json.isNull(KEY_SEED_COLOR)) json.getInt(KEY_SEED_COLOR) else null
+            val axisList = json.optJSONArray(KEY_AXIS_LIST)?.let(ClockFontAxisSetting::fromJson)
+            return ClockSettings(clockId, seedColor, axisList ?: listOf()).apply {
+                metadata = json.optJSONObject(KEY_METADATA) ?: JSONObject()
+            }
+        }
+    }
+}
+
+@Keep
+/** Axis setting value for a clock */
+data class ClockFontAxisSetting(
+    /** Axis key; matches ClockFontAxis.key */
+    val key: String,
+
+    /** Value to set this axis to */
+    val value: Float,
+) {
+    companion object {
+        private val KEY_AXIS_KEY = "key"
+        private val KEY_AXIS_VALUE = "value"
+
+        fun toJson(setting: ClockFontAxisSetting): JSONObject {
+            return JSONObject().apply {
+                put(KEY_AXIS_KEY, setting.key)
+                put(KEY_AXIS_VALUE, setting.value)
+            }
+        }
+
+        fun toJson(settings: List<ClockFontAxisSetting>): JSONArray {
+            return JSONArray().apply {
+                for (axis in settings) {
+                    put(toJson(axis))
+                }
+            }
+        }
+
+        fun fromJson(jsonObj: JSONObject): ClockFontAxisSetting {
+            return ClockFontAxisSetting(
+                key = jsonObj.getString(KEY_AXIS_KEY),
+                value = jsonObj.getDouble(KEY_AXIS_VALUE).toFloat(),
+            )
+        }
+
+        fun fromJson(jsonArray: JSONArray): List<ClockFontAxisSetting> {
+            val result = mutableListOf<ClockFontAxisSetting>()
+            for (i in 0..jsonArray.length() - 1) {
+                val obj = jsonArray.getJSONObject(i)
+                if (obj == null) continue
+                result.add(fromJson(obj))
+            }
+            return result
+        }
+
+        fun toFVar(settings: List<ClockFontAxisSetting>): String {
+            val sb = StringBuilder()
+            for (axis in settings) {
+                if (sb.length > 0) sb.append(", ")
+                sb.append("'${axis.key}' ${axis.value.toInt()}")
+            }
+            return sb.toString()
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt
index b37206a..160574fa 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt
@@ -19,6 +19,7 @@
 import com.android.compose.animation.scene.Swipe
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
@@ -38,6 +39,7 @@
 class DreamUserActionsViewModel
 @AssistedInject
 constructor(
+    private val communalInteractor: CommunalInteractor,
     private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
     private val shadeInteractor: ShadeInteractor,
 ) : UserActionsViewModel() {
@@ -50,10 +52,13 @@
                 } else {
                     combine(
                         deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked },
+                        communalInteractor.isCommunalAvailable,
                         shadeInteractor.shadeMode,
-                    ) { isDeviceUnlocked, shadeMode ->
+                    ) { isDeviceUnlocked, isCommunalAvailable, shadeMode ->
                         buildList {
-                                add(Swipe.Start to Scenes.Communal)
+                                if (isCommunalAvailable) {
+                                    add(Swipe.Start to Scenes.Communal)
+                                }
 
                                 val bouncerOrGone =
                                     if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt
index 99cafd3..321fd57 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt
@@ -28,6 +28,7 @@
 import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS
 import android.hardware.input.InputSettings
 import android.hardware.input.KeyGestureEvent
+import android.hardware.input.KeyGestureEvent.KeyGestureType
 import android.util.Log
 import androidx.annotation.VisibleForTesting
 import androidx.compose.runtime.mutableStateOf
@@ -44,6 +45,8 @@
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
 import com.android.systemui.settings.UserTracker
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -52,8 +55,6 @@
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.withContext
-import javax.inject.Inject
-import kotlin.coroutines.CoroutineContext
 
 @SysUISingleton
 class CustomShortcutCategoriesRepository
@@ -65,7 +66,7 @@
     @Background private val bgCoroutineContext: CoroutineContext,
     private val shortcutCategoriesUtils: ShortcutCategoriesUtils,
     private val context: Context,
-    private val inputGestureMaps: InputGestureMaps
+    private val inputGestureMaps: InputGestureMaps,
 ) : ShortcutCategoriesRepository {
 
     private val userContext: Context
@@ -130,9 +131,7 @@
                     emptyList()
                 } else {
                     val customInputGesturesForUser: List<InputGestureData> =
-                        if (InputSettings.isCustomizableInputGesturesFeatureFlagEnabled()) {
-                            inputManager.getCustomInputGestures(/* filter= */ null)
-                        } else emptyList()
+                        getCustomInputGestures()
                     val sources = toInternalGroupSources(customInputGesturesForUser)
                     val supportedKeyCodes =
                         shortcutCategoriesUtils.fetchSupportedKeyCodes(
@@ -173,16 +172,20 @@
                 .addKeyGestureTypeFromShortcutLabel()
                 .addTriggerFromSelectedKeyCombination()
                 .build()
-            // TODO(b/379648200) add app launch data for application categories shortcut after
-            // dynamic
-            // label/icon mapping implementation
+            // TODO(b/379648200) add app launch data after dynamic label/icon mapping implementation
         } catch (e: IllegalArgumentException) {
             Log.w(TAG, "could not add custom shortcut: $e")
             return null
         }
     }
 
-    suspend fun confirmAndSetShortcutCurrentlyBeingCustomized(): ShortcutCustomizationRequestResult {
+    private fun retrieveInputGestureDataForShortcutBeingDeleted(): InputGestureData? {
+        val keyGestureType = getKeyGestureTypeFromShortcutBeingDeletedLabel()
+        return getCustomInputGestures().firstOrNull { it.action.keyGestureType() == keyGestureType }
+    }
+
+    suspend fun confirmAndSetShortcutCurrentlyBeingCustomized():
+        ShortcutCustomizationRequestResult {
         return withContext(bgCoroutineContext) {
             val inputGestureData =
                 buildInputGestureDataForShortcutBeingCustomized()
@@ -201,33 +204,88 @@
         }
     }
 
-    private fun Builder.addKeyGestureTypeFromShortcutLabel(): Builder {
-        val shortcutBeingCustomized =
-            getShortcutBeingCustomized() as? ShortcutCustomizationRequestInfo.Add
-
-        if (shortcutBeingCustomized == null) {
-            Log.w(TAG, "User requested to set shortcut but shortcut being customized is null")
-            return this
+    suspend fun deleteShortcutCurrentlyBeingCustomized():
+        ShortcutCustomizationRequestResult {
+        return withContext(bgCoroutineContext) {
+            val inputGestureData =
+                retrieveInputGestureDataForShortcutBeingDeleted()
+                    ?: return@withContext ShortcutCustomizationRequestResult.ERROR_OTHER
+            return@withContext when (
+                val result = inputManager.removeCustomInputGesture(inputGestureData)
+            ) {
+                CUSTOM_INPUT_GESTURE_RESULT_SUCCESS -> ShortcutCustomizationRequestResult.SUCCESS
+                else -> {
+                    Log.w(
+                        TAG,
+                        "Attempted to delete shortcut being customized " +
+                            "${_shortcutBeingCustomized.value} but ran into an error. InputGestureData" +
+                            " = $inputGestureData, error code: $result",
+                    )
+                    ShortcutCustomizationRequestResult.ERROR_OTHER
+                }
+            }
         }
+    }
 
-        val keyGestureType =
-            inputGestureMaps.shortcutLabelToKeyGestureTypeMap[shortcutBeingCustomized.label]
+    private fun getCustomInputGestures(): List<InputGestureData> {
+        return if (InputSettings.isCustomizableInputGesturesFeatureFlagEnabled()) {
+            inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY)
+        } else emptyList()
+    }
+
+    private fun Builder.addKeyGestureTypeFromShortcutLabel(): Builder {
+        val keyGestureType = getKeyGestureTypeFromShortcutBeingCustomizedLabel()
 
         if (keyGestureType == null) {
-            Log.w(TAG, "Could not find KeyGestureType for shortcut $shortcutBeingCustomized")
+            Log.w(
+                TAG,
+                "Could not find KeyGestureType for shortcut ${_shortcutBeingCustomized.value}",
+            )
             return this
         }
 
         return setKeyGestureType(keyGestureType)
     }
 
+    @KeyGestureType
+    private fun getKeyGestureTypeFromShortcutBeingCustomizedLabel(): Int? {
+        val shortcutBeingCustomized =
+            getShortcutBeingCustomized() as? ShortcutCustomizationRequestInfo.Add
+
+        if (shortcutBeingCustomized == null) {
+            Log.w(
+                TAG,
+                "Requested key gesture type from label but shortcut being customized is null",
+            )
+            return null
+        }
+
+        return inputGestureMaps.shortcutLabelToKeyGestureTypeMap[shortcutBeingCustomized.label]
+    }
+
+    @KeyGestureType
+    private fun getKeyGestureTypeFromShortcutBeingDeletedLabel(): Int? {
+        val shortcutBeingCustomized =
+            getShortcutBeingCustomized() as? ShortcutCustomizationRequestInfo.Delete
+
+        if (shortcutBeingCustomized == null) {
+            Log.w(
+                TAG,
+                "Requested key gesture type from label but shortcut being customized is null",
+            )
+            return null
+        }
+
+        return inputGestureMaps.shortcutLabelToKeyGestureTypeMap[shortcutBeingCustomized.label]
+    }
+
     private fun Builder.addTriggerFromSelectedKeyCombination(): Builder {
         val selectedKeyCombination = _selectedKeyCombination.value
         if (selectedKeyCombination?.keyCode == null) {
             Log.w(
                 TAG,
                 "User requested to set shortcut but selected key combination is " +
-                        "$selectedKeyCombination",
+                    "$selectedKeyCombination",
             )
             return this
         }
@@ -235,8 +293,7 @@
         return setTrigger(
             createKeyTrigger(
                 /* keycode = */ selectedKeyCombination.keyCode,
-                /* modifierState = */
-                shortcutCategoriesUtils.removeUnsupportedModifiers(
+                /* modifierState = */ shortcutCategoriesUtils.removeUnsupportedModifiers(
                     selectedKeyCombination.modifiers
                 ),
             )
@@ -256,10 +313,8 @@
                 val keyTrigger = gestureData.trigger as KeyTrigger
                 val keyGestureType = gestureData.action.keyGestureType()
                 fetchGroupLabelByGestureType(keyGestureType)?.let { groupLabel ->
-                    toInternalKeyboardShortcutInfo(
-                        keyGestureType,
-                        keyTrigger
-                    )?.let { internalKeyboardShortcutInfo ->
+                    toInternalKeyboardShortcutInfo(keyGestureType, keyTrigger)?.let {
+                        internalKeyboardShortcutInfo ->
                         val group =
                             InternalKeyboardShortcutGroup(
                                 label = groupLabel,
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt
index d228a15..ecc0761 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt
@@ -48,9 +48,7 @@
 import com.android.systemui.res.R
 import javax.inject.Inject
 
-class InputGestureMaps
-@Inject
-constructor(private val context: Context) {
+class InputGestureMaps @Inject constructor(private val context: Context) {
     val gestureToShortcutCategoryTypeMap =
         mapOf(
             // System Category
@@ -180,9 +178,10 @@
         )
 
     val shortcutLabelToKeyGestureTypeMap: Map<String, Int>
-        get() = gestureToInternalKeyboardShortcutInfoLabelResIdMap.entries.associateBy({
-            context.getString(it.value)
-        }) {
-            it.key
-        }
+        get() =
+            gestureToInternalKeyboardShortcutInfoLabelResIdMap.entries.associateBy({
+                context.getString(it.value)
+            }) {
+                it.key
+            }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt
index 95bc9f6..a0897f2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt
@@ -36,9 +36,9 @@
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutIcon
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
-import kotlinx.coroutines.withContext
 import javax.inject.Inject
 import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.withContext
 
 class ShortcutCategoriesUtils
 @Inject
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt
index f4e2f05..7743c53 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt
@@ -41,7 +41,13 @@
         customShortcutRepository.onCustomizationRequested(requestInfo)
     }
 
-    suspend fun confirmAndSetShortcutCurrentlyBeingCustomized(): ShortcutCustomizationRequestResult {
+    suspend fun confirmAndSetShortcutCurrentlyBeingCustomized():
+        ShortcutCustomizationRequestResult {
         return customShortcutRepository.confirmAndSetShortcutCurrentlyBeingCustomized()
     }
+
+    suspend fun deleteShortcutCurrentlyBeingCustomized():
+        ShortcutCustomizationRequestResult {
+        return customShortcutRepository.deleteShortcutCurrentlyBeingCustomized()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt
index 813a1fca..4648053 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt
@@ -18,25 +18,31 @@
 
 sealed interface ShortcutCategoryType {
     val isTrusted: Boolean
+    val includeInCustomization: Boolean
 
     data object System : ShortcutCategoryType {
         override val isTrusted: Boolean = true
+        override val includeInCustomization: Boolean = true
     }
 
     data object MultiTasking : ShortcutCategoryType {
         override val isTrusted: Boolean = true
+        override val includeInCustomization: Boolean = true
     }
 
     data object InputMethodEditor : ShortcutCategoryType {
         override val isTrusted: Boolean = false
+        override val includeInCustomization: Boolean = false
     }
 
     data object AppCategories : ShortcutCategoryType {
         override val isTrusted: Boolean = true
+        override val includeInCustomization: Boolean = true
     }
 
     data class CurrentApp(val packageName: String) : ShortcutCategoryType {
         override val isTrusted: Boolean = false
+        override val includeInCustomization: Boolean = false
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
index 67caadb..f28618b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
@@ -73,22 +73,17 @@
         return dialogFactory.create(dialogDelegate = ShortcutCustomizationDialogDelegate()) { dialog
             ->
             val uiState by
-            viewModel.shortcutCustomizationUiState.collectAsStateWithLifecycle(
-                initialValue = ShortcutCustomizationUiState.Inactive
-            )
+                viewModel.shortcutCustomizationUiState.collectAsStateWithLifecycle(
+                    initialValue = ShortcutCustomizationUiState.Inactive
+                )
             val coroutineScope = rememberCoroutineScope()
             ShortcutCustomizationDialog(
                 uiState = uiState,
-                modifier = Modifier
-                    .width(364.dp)
-                    .wrapContentHeight()
-                    .padding(vertical = 24.dp),
+                modifier = Modifier.width(364.dp).wrapContentHeight().padding(vertical = 24.dp),
                 onKeyPress = { viewModel.onKeyPressed(it) },
                 onCancel = { dialog.dismiss() },
-                onConfirmSetShortcut = {
-                    coroutineScope.launch { viewModel.onSetShortcut() }
-                },
-                onConfirmDeleteShortcut = { viewModel.onDeleteShortcut() },
+                onConfirmSetShortcut = { coroutineScope.launch { viewModel.onSetShortcut() } },
+                onConfirmDeleteShortcut = { coroutineScope.launch { viewModel.deleteShortcutCurrentlyBeingCustomized() } },
             )
             dialog.setOnDismissListener { viewModel.onDialogDismissed() }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
index f41ff7c..e3675de 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
@@ -461,7 +461,7 @@
             SubCategoryContainerDualPane(
                 searchQuery = searchQuery,
                 subCategory = subcategory,
-                isCustomizing = isCustomizing,
+                isCustomizing = isCustomizing and category.type.includeInCustomization,
                 onCustomizationRequested = { requestInfo ->
                     when (requestInfo) {
                         is ShortcutCustomizationRequestInfo.Add ->
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
index 8696827..b467bb4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
@@ -79,8 +79,7 @@
             }
 
             is ShortcutCustomizationRequestInfo.Delete -> {
-                _shortcutCustomizationUiState.value =
-                    DeleteShortcutDialog(isDialogShowing = false)
+                _shortcutCustomizationUiState.value = DeleteShortcutDialog(isDialogShowing = false)
                 shortcutCustomizationInteractor.onCustomizationRequested(requestInfo)
             }
         }
@@ -134,17 +133,23 @@
         }
     }
 
-    fun onDeleteShortcut() {
-        // TODO(b/373631984) not yet implemented
+    suspend fun deleteShortcutCurrentlyBeingCustomized() {
+        val result =
+            shortcutCustomizationInteractor.deleteShortcutCurrentlyBeingCustomized()
+
+        _shortcutCustomizationUiState.update { uiState ->
+            when (result) {
+                ShortcutCustomizationRequestResult.SUCCESS -> ShortcutCustomizationUiState.Inactive
+                else -> uiState
+            }
+        }
     }
 
     private fun getUiStateWithErrorMessage(
         uiState: ShortcutCustomizationUiState,
         errorMessage: String,
     ): ShortcutCustomizationUiState {
-        return (uiState as? AddShortcutDialog)?.copy(
-            errorMessage = errorMessage
-        ) ?: uiState
+        return (uiState as? AddShortcutDialog)?.copy(errorMessage = errorMessage) ?: uiState
     }
 
     private fun updatePressedKeys(keyEvent: KeyEvent) {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 80ac2fc..0a4e8c6 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -18,6 +18,7 @@
 import static android.content.pm.ActivityInfo.CONFIG_FONT_SCALE;
 import static android.view.InputDevice.SOURCE_MOUSE;
 import static android.view.InputDevice.SOURCE_TOUCHPAD;
+import static android.view.MotionEvent.TOOL_TYPE_FINGER;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
 
 import static com.android.systemui.Flags.edgebackGestureHandlerGetRunningTasksBackground;
@@ -216,6 +217,7 @@
     private final int mDisplayId;
 
     private final UiThreadContext mUiThreadContext;
+    private final Handler mBgHandler;
     private final Executor mBackgroundExecutor;
 
     private final Rect mPipExcludedBounds = new Rect();
@@ -378,11 +380,14 @@
         @Override
         public void onInputDeviceAdded(int deviceId) {
             if (isTrackpadDevice(deviceId)) {
-                boolean wasEmpty = mTrackpadsConnected.isEmpty();
-                mTrackpadsConnected.add(deviceId);
-                if (wasEmpty) {
-                    update();
-                }
+                // This updates the gesture handler state and should be running on the main thread.
+                mUiThreadContext.getHandler().post(() -> {
+                    boolean wasEmpty = mTrackpadsConnected.isEmpty();
+                    mTrackpadsConnected.add(deviceId);
+                    if (wasEmpty) {
+                        update();
+                    }
+                });
             }
         }
 
@@ -391,10 +396,13 @@
 
         @Override
         public void onInputDeviceRemoved(int deviceId) {
-            mTrackpadsConnected.remove(deviceId);
-            if (mTrackpadsConnected.isEmpty()) {
-                update();
-            }
+            // This updates the gesture handler state and should be running on the main thread.
+            mUiThreadContext.getHandler().post(() -> {
+                mTrackpadsConnected.remove(deviceId);
+                if (mTrackpadsConnected.isEmpty()) {
+                    update();
+                }
+            });
         }
 
         private void update() {
@@ -408,12 +416,12 @@
         }
 
         private boolean isTrackpadDevice(int deviceId) {
+            // This is a blocking binder call that should run on a bg thread.
             InputDevice inputDevice = mInputManager.getInputDevice(deviceId);
             if (inputDevice == null) {
                 return false;
             }
-            return inputDevice.getSources() == (InputDevice.SOURCE_MOUSE
-                    | InputDevice.SOURCE_TOUCHPAD);
+            return inputDevice.getSources() == (SOURCE_MOUSE | SOURCE_TOUCHPAD);
         }
     };
 
@@ -457,6 +465,7 @@
         mDisplayId = context.getDisplayId();
         mUiThreadContext = uiThreadContext;
         mBackgroundExecutor = backgroundExecutor;
+        mBgHandler = bgHandler;
         mUserTracker = userTracker;
         mOverviewProxyService = overviewProxyService;
         mSysUiState = sysUiState;
@@ -611,9 +620,7 @@
         mIsAttached = true;
         mOverviewProxyService.addCallback(mQuickSwitchListener);
         mSysUiState.addCallback(mSysUiStateCallback);
-        mInputManager.registerInputDeviceListener(
-                mInputDeviceListener,
-                mUiThreadContext.getHandler());
+        mInputManager.registerInputDeviceListener(mInputDeviceListener, mBgHandler);
         int[] inputDevices = mInputManager.getInputDeviceIds();
         for (int inputDeviceId : inputDevices) {
             mInputDeviceListener.onInputDeviceAdded(inputDeviceId);
@@ -1089,8 +1096,8 @@
                         && isValidTrackpadBackGesture(true /* isTrackpadEvent */);
             } else {
                 mAllowGesture = isBackAllowedCommon && !mUsingThreeButtonNav && isWithinInsets
-                    && isWithinTouchRegion((int) ev.getX(), (int) ev.getY())
-                    && !isButtonPressFromTrackpad(ev);
+                        && isWithinTouchRegion((int) ev.getX(), (int) ev.getY())
+                        && !isButtonPressFromTrackpad(ev);
             }
             if (mAllowGesture) {
                 mEdgeBackPlugin.setIsLeftPanel(mIsOnLeftEdge);
@@ -1202,10 +1209,8 @@
     }
 
     private boolean isButtonPressFromTrackpad(MotionEvent ev) {
-        // We don't allow back for button press from the trackpad, and yet we do with a mouse.
-        int sources = InputManager.getInstance().getInputDevice(ev.getDeviceId()).getSources();
-        int sourceTrackpad = (SOURCE_MOUSE | SOURCE_TOUCHPAD);
-        return (sources & sourceTrackpad) == sourceTrackpad && ev.getButtonState() != 0;
+        return ev.getSource() == (SOURCE_MOUSE | SOURCE_TOUCHPAD)
+                && ev.getToolType(ev.getActionIndex()) == TOOL_TYPE_FINGER;
     }
 
     private void dispatchToBackAnimation(MotionEvent event) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt
index f706cf6..d0ce34c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt
@@ -23,18 +23,20 @@
 import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_RESERVED_GESTURE
 import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS
 import android.hardware.input.fakeInputManager
-import android.os.SystemClock
-import android.view.KeyEvent.ACTION_DOWN
-import android.view.KeyEvent.KEYCODE_A
-import android.view.KeyEvent.META_CTRL_ON
-import android.view.KeyEvent.META_META_ON
-import androidx.compose.ui.input.key.KeyEvent
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allAppsInputGestureData
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.expectedStandardAddShortcutUiState
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.expectedStandardDeleteShortcutUiState
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.goHomeInputGestureData
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.keyDownEventWithActionKeyPressed
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.keyDownEventWithoutActionKeyPressed
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.keyUpEventWithActionKeyPressed
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.standardAddCustomShortcutRequestInfo
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.standardAddShortcutRequest
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.standardDeleteCustomShortcutRequestInfo
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
 import com.android.systemui.keyboard.shortcut.shortcutCustomizationViewModelFactory
 import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
@@ -97,7 +99,7 @@
     @Test
     fun uiState_correctlyUpdatedWhenDeleteShortcutCustomizationIsRequested() {
         testScope.runTest {
-            viewModel.onShortcutCustomizationRequested(standardDeleteShortcutRequest)
+            viewModel.onShortcutCustomizationRequested(standardDeleteCustomShortcutRequestInfo)
             val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
 
             assertThat(uiState).isEqualTo(expectedStandardDeleteShortcutUiState)
@@ -120,7 +122,7 @@
     fun uiState_consumedOnDeleteDialogShown() {
         testScope.runTest {
             val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
-            viewModel.onShortcutCustomizationRequested(standardDeleteShortcutRequest)
+            viewModel.onShortcutCustomizationRequested(standardDeleteCustomShortcutRequestInfo)
             viewModel.onDialogShown()
 
             assertThat(
@@ -168,7 +170,7 @@
     fun uiState_errorMessage_isEmptyByDefault() {
         testScope.runTest {
             val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
-            viewModel.onShortcutCustomizationRequested(allAppsShortcutCustomizationRequest)
+            viewModel.onShortcutCustomizationRequested(standardAddCustomShortcutRequestInfo)
             viewModel.onDialogShown()
 
             assertThat((uiState as ShortcutCustomizationUiState.AddShortcutDialog).errorMessage)
@@ -227,6 +229,21 @@
     }
 
     @Test
+    fun uiState_becomesInactiveAfterSuccessfullyDeletingShortcut() {
+        testScope.runTest {
+            val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
+            whenever(inputManager.getCustomInputGestures(any()))
+                .thenReturn(listOf(goHomeInputGestureData, allAppsInputGestureData))
+            whenever(inputManager.removeCustomInputGesture(any()))
+                .thenReturn(CUSTOM_INPUT_GESTURE_RESULT_SUCCESS)
+
+            openDeleteShortcutDialogAndDeleteShortcut()
+
+            assertThat(uiState).isEqualTo(ShortcutCustomizationUiState.Inactive)
+        }
+    }
+
+    @Test
     fun onKeyPressed_handlesKeyEvents_whereActionKeyIsAlsoPressed() {
         testScope.runTest {
             viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
@@ -281,7 +298,7 @@
     }
 
     private suspend fun openAddShortcutDialogAndSetShortcut() {
-        viewModel.onShortcutCustomizationRequested(allAppsShortcutCustomizationRequest)
+        viewModel.onShortcutCustomizationRequested(standardAddCustomShortcutRequestInfo)
         viewModel.onDialogShown()
 
         viewModel.onKeyPressed(keyDownEventWithActionKeyPressed)
@@ -290,71 +307,10 @@
         viewModel.onSetShortcut()
     }
 
-    private val keyDownEventWithoutActionKeyPressed =
-        KeyEvent(
-            android.view.KeyEvent(
-                /* downTime = */ SystemClock.uptimeMillis(),
-                /* eventTime = */ SystemClock.uptimeMillis(),
-                /* action = */ ACTION_DOWN,
-                /* code = */ KEYCODE_A,
-                /* repeat = */ 0,
-                /* metaState = */ META_CTRL_ON,
-            )
-        )
+    private suspend fun openDeleteShortcutDialogAndDeleteShortcut() {
+        viewModel.onShortcutCustomizationRequested(standardDeleteCustomShortcutRequestInfo)
+        viewModel.onDialogShown()
 
-    private val keyDownEventWithActionKeyPressed =
-        KeyEvent(
-            android.view.KeyEvent(
-                /* downTime = */ SystemClock.uptimeMillis(),
-                /* eventTime = */ SystemClock.uptimeMillis(),
-                /* action = */ ACTION_DOWN,
-                /* code = */ KEYCODE_A,
-                /* repeat = */ 0,
-                /* metaState = */ META_CTRL_ON or META_META_ON,
-            )
-        )
-
-    private val keyUpEventWithActionKeyPressed =
-        KeyEvent(
-            android.view.KeyEvent(
-                /* downTime = */ SystemClock.uptimeMillis(),
-                /* eventTime = */ SystemClock.uptimeMillis(),
-                /* action = */ ACTION_DOWN,
-                /* code = */ KEYCODE_A,
-                /* repeat = */ 0,
-                /* metaState = */ 0,
-            )
-        )
-
-    private val standardAddShortcutRequest =
-        ShortcutCustomizationRequestInfo.Add(
-            label = "Standard shortcut",
-            categoryType = ShortcutCategoryType.System,
-            subCategoryLabel = "Standard subcategory",
-        )
-
-    private val standardDeleteShortcutRequest =
-        ShortcutCustomizationRequestInfo.Delete(
-            label = "Standard shortcut",
-            categoryType = ShortcutCategoryType.System,
-            subCategoryLabel = "Standard subcategory",
-        )
-
-    private val allAppsShortcutCustomizationRequest =
-        ShortcutCustomizationRequestInfo.Add(
-            label = "Open apps list",
-            categoryType = ShortcutCategoryType.System,
-            subCategoryLabel = "System controls",
-        )
-
-    private val expectedStandardAddShortcutUiState =
-        ShortcutCustomizationUiState.AddShortcutDialog(
-            shortcutLabel = "Standard shortcut",
-            defaultCustomShortcutModifierKey =
-                ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta),
-            isDialogShowing = false,
-        )
-
-    private val expectedStandardDeleteShortcutUiState =
-        ShortcutCustomizationUiState.DeleteShortcutDialog(isDialogShowing = false)
+        viewModel.deleteShortcutCurrentlyBeingCustomized()
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt
index b24b3ad..71746b5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.dreams.ui.viewmodel
 
+import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.shade.domain.interactor.shadeInteractor
@@ -23,6 +24,7 @@
 val Kosmos.dreamUserActionsViewModel by
     Kosmos.Fixture {
         DreamUserActionsViewModel(
+            communalInteractor = communalInteractor,
             deviceUnlockedInteractor = deviceUnlockedInteractor,
             shadeInteractor = shadeInteractor,
         )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
index 721c0b8..2c85816 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
@@ -102,10 +102,7 @@
         )
     }
 
-val Kosmos.inputGestureMaps by
-        Kosmos.Fixture {
-            InputGestureMaps(applicationContext)
-        }
+val Kosmos.inputGestureMaps by Kosmos.Fixture { InputGestureMaps(applicationContext) }
 
 val Kosmos.customShortcutCategoriesRepository by
     Kosmos.Fixture {
@@ -116,7 +113,7 @@
             testDispatcher,
             shortcutCategoriesUtils,
             applicationContext,
-            inputGestureMaps
+            inputGestureMaps,
         )
     }
 
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 053ec82..d79e66d 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -131,9 +131,6 @@
 import static android.provider.Settings.Global.DEBUG_APP;
 import static android.provider.Settings.Global.WAIT_FOR_DEBUGGER;
 import static android.security.Flags.preventIntentRedirect;
-import static android.security.Flags.preventIntentRedirectCollectNestedKeysOnServerIfNotCollected;
-import static android.security.Flags.preventIntentRedirectShowToast;
-import static android.security.Flags.preventIntentRedirectThrowExceptionIfNestedKeysNotCollected;
 import static android.util.FeatureFlagUtils.SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS;
 import static android.view.Display.INVALID_DISPLAY;
 
@@ -390,7 +387,6 @@
 import android.view.View;
 import android.view.WindowManager;
 import android.view.autofill.AutofillManagerInternal;
-import android.widget.Toast;
 
 import com.android.internal.annotations.CompositeRWLock;
 import com.android.internal.annotations.GuardedBy;
@@ -441,7 +437,6 @@
 import com.android.server.SystemService;
 import com.android.server.SystemServiceManager;
 import com.android.server.ThreadPriorityBooster;
-import com.android.server.UiThread;
 import com.android.server.Watchdog;
 import com.android.server.am.LowMemDetector.MemFactor;
 import com.android.server.appop.AppOpsService;
@@ -483,7 +478,6 @@
 
 import dalvik.annotation.optimization.NeverCompile;
 import dalvik.system.VMRuntime;
-
 import libcore.util.EmptyArray;
 
 import java.io.File;
@@ -19319,31 +19313,8 @@
      */
     public void addCreatorToken(@Nullable Intent intent, String creatorPackage) {
         if (!preventIntentRedirect()) return;
-        if (intent == null) return;
 
-        if ((intent.getExtendedFlags() & Intent.EXTENDED_FLAG_NESTED_INTENT_KEYS_COLLECTED) == 0) {
-            Slog.wtf(TAG,
-                    "[IntentRedirect] The intent does not have its nested keys collected as a "
-                            + "preparation for creating intent creator tokens. Intent: "
-                            + intent + "; creatorPackage: " + creatorPackage);
-            if (preventIntentRedirectShowToast()) {
-                UiThread.getHandler().post(
-                        () -> Toast.makeText(mContext,
-                                "Nested keys not collected. go/report-bug-intentRedir to report a"
-                                        + " bug", Toast.LENGTH_LONG).show());
-            }
-            if (preventIntentRedirectThrowExceptionIfNestedKeysNotCollected()) {
-                // this flag will be internal only, not ramped to public.
-                throw new SecurityException(
-                        "The intent does not have its nested keys collected as a preparation for "
-                                + "creating intent creator tokens. Intent: "
-                                + intent + "; creatorPackage: " + creatorPackage);
-            }
-            if (preventIntentRedirectCollectNestedKeysOnServerIfNotCollected()) {
-                // this flag will be ramped to public.
-                intent.collectExtraIntentKeys();
-            }
-        }
+        if (intent == null) return;
 
         String targetPackage = intent.getComponent() != null
                 ? intent.getComponent().getPackageName()
diff --git a/services/core/java/com/android/server/am/OWNERS b/services/core/java/com/android/server/am/OWNERS
index ab7cd5f..1a6051b 100644
--- a/services/core/java/com/android/server/am/OWNERS
+++ b/services/core/java/com/android/server/am/OWNERS
@@ -66,6 +66,9 @@
 # Activity Security
 per-file ActivityManager* = file:/ACTIVITY_SECURITY_OWNERS
 
+# Aconfig Flags
+per-file flags.aconfig = yamasani@google.com, bills@google.com, nalini@google.com
+
 # Londoners
 michaelwr@google.com #{LAST_RESORT_SUGGESTION}
 narayan@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 78c4f74..f28f3e1 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -140,6 +140,7 @@
     // The list is sorted.
     @VisibleForTesting
     static final String[] sDeviceConfigAconfigScopes = new String[] {
+        "aaos_power_triage",
         "aaos_sdv",
         "accessibility",
         "android_core_networking",
diff --git a/services/core/java/com/android/server/hdmi/Constants.java b/services/core/java/com/android/server/hdmi/Constants.java
index 0e7d2b6..a06f9ef 100644
--- a/services/core/java/com/android/server/hdmi/Constants.java
+++ b/services/core/java/com/android/server/hdmi/Constants.java
@@ -524,6 +524,13 @@
     static final String PROPERTY_DISABLE_CEC_ON_STANDBY_IN_LOW_ENERGY_MODE =
             "persist.sys.hdmi.property_disable_cec_on_standby_in_low_energy_mode";
 
+    /**
+     * Property that checks if CEC was manually enabled by the user in offline mode. With the help
+     * of this property we avoid turning off CEC when the device goes to sleep and if the device
+     * is in low energy mode.
+     */
+    static final String PROPERTY_USER_ACTION_KEEP_CEC_ENABLED_IN_OFFLINE_MODE =
+            "persist.sys.hdmi.property_user_action_keep_cec_enabled_in_offline_mode";
     static final int RECORDING_TYPE_DIGITAL_RF = 1;
     static final int RECORDING_TYPE_ANALOGUE_RF = 2;
     static final int RECORDING_TYPE_EXTERNAL_PHYSICAL_ADDRESS = 3;
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 0c5069f..6e98bff 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -4027,7 +4027,8 @@
                     return;
                 }
                 if (isTvDevice() && getDisableCecOnStandbyByLowEnergyMode()
-                        && mPowerManager.isLowPowerStandbyEnabled()) {
+                        && mPowerManager.isLowPowerStandbyEnabled()
+                        && !userEnabledCecInOfflineMode()) {
                     Slog.w(TAG, "Disable CEC on standby due to low power energy mode.");
                     setWasCecDisabledOnStandbyByLowEnergyMode(true);
                     getHdmiCecConfig().setIntValue(
@@ -5225,4 +5226,14 @@
                 Constants.PROPERTY_WAS_CEC_DISABLED_ON_STANDBY_BY_LOW_ENERGY_MODE,
                 String.valueOf(value));
     }
+
+    /**
+     * Reads the property that checks if CEC was enabled by the user while in offline mode such that
+     * it won't be disabled when going to sleep by low energy mode.
+     */
+    @VisibleForTesting
+    protected boolean userEnabledCecInOfflineMode() {
+        return SystemProperties.getBoolean(
+                Constants.PROPERTY_USER_ACTION_KEEP_CEC_ENABLED_IN_OFFLINE_MODE, false);
+    }
 }
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index 76049ca..d177d0e 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -34,6 +34,7 @@
 import android.hardware.contexthub.HubEndpointInfo;
 import android.hardware.contexthub.IContextHubEndpoint;
 import android.hardware.contexthub.IContextHubEndpointCallback;
+import android.hardware.contexthub.IContextHubEndpointDiscoveryCallback;
 import android.hardware.contexthub.MessageDeliveryStatus;
 import android.hardware.location.ContextHubInfo;
 import android.hardware.location.ContextHubMessage;
@@ -793,6 +794,31 @@
         return null;
     }
 
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+    @Override
+    public void registerEndpointDiscoveryCallbackId(
+            long endpointId, IContextHubEndpointDiscoveryCallback callback) throws RemoteException {
+        super.registerEndpointDiscoveryCallbackId_enforcePermission();
+        // TODO(b/375487784): Implement this
+    }
+
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+    @Override
+    public void registerEndpointDiscoveryCallbackDescriptor(
+            String serviceDescriptor, IContextHubEndpointDiscoveryCallback callback)
+            throws RemoteException {
+        super.registerEndpointDiscoveryCallbackDescriptor_enforcePermission();
+        // TODO(b/375487784): Implement this
+    }
+
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+    @Override
+    public void unregisterEndpointDiscoveryCallback(IContextHubEndpointDiscoveryCallback callback)
+            throws RemoteException {
+        super.unregisterEndpointDiscoveryCallback_enforcePermission();
+        // TODO(b/375487784): Implement this
+    }
+
     /**
      * Creates an internal load transaction callback to be used for old API clients
      *
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index 887e186..708bca7 100644
--- a/services/core/java/com/android/server/trust/TrustManagerService.java
+++ b/services/core/java/com/android/server/trust/TrustManagerService.java
@@ -2528,22 +2528,24 @@
     }
 
     private void notifyDeviceLockedListenersForUser(int userId, boolean locked) {
-        int numListeners = mDeviceLockedStateListeners.beginBroadcast();
-        try {
-            IntStream.range(0, numListeners).forEach(i -> {
-                try {
-                    Integer uid = (Integer) mDeviceLockedStateListeners.getBroadcastCookie(i);
-                    if (userId == uid.intValue()) {
-                        mDeviceLockedStateListeners.getBroadcastItem(i)
-                                .onDeviceLockedStateChanged(locked);
+        synchronized (mDeviceLockedStateListeners) {
+            int numListeners = mDeviceLockedStateListeners.beginBroadcast();
+            try {
+                IntStream.range(0, numListeners).forEach(i -> {
+                    try {
+                        Integer uid = (Integer) mDeviceLockedStateListeners.getBroadcastCookie(i);
+                        if (userId == uid.intValue()) {
+                            mDeviceLockedStateListeners.getBroadcastItem(i)
+                                    .onDeviceLockedStateChanged(locked);
+                        }
+                    } catch (RemoteException re) {
+                        Log.i(TAG, "Service died", re);
                     }
-                } catch (RemoteException re) {
-                    Log.i(TAG, "Service died", re);
-                }
-            });
+                });
 
-        } finally {
-            mDeviceLockedStateListeners.finishBroadcast();
+            } finally {
+                mDeviceLockedStateListeners.finishBroadcast();
+            }
         }
     }
 }
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 0337f5f..bbef578 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -3181,7 +3181,7 @@
                     throw new IllegalArgumentException("Invalid crop rect supplied: " + crop);
                 }
                 int orientation = screenOrientations[i];
-                if (orientation == ORIENTATION_UNKNOWN && cropMap.size() > 1) {
+                if (orientation == ORIENTATION_UNKNOWN && crops.size() > 1) {
                     throw new IllegalArgumentException("Invalid crops supplied: the UNKNOWN"
                             + "screen orientation should only be used in a singleton map");
                 }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index 0816e7b..5be4490 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -118,6 +118,7 @@
     private boolean mDisableCecOnStandbyByLowEnergyMode;
     private boolean mWasCecDisabledOnStandbyByLowEnergyMode;
     private boolean mUseHdmiCecPowerStatusController;
+    private boolean mUserEnabledCecInOfflineMode;
 
     private class DeviceEventListener {
         private HdmiDeviceInfo mDevice;
@@ -250,6 +251,11 @@
                     protected void setWasCecDisabledOnStandbyByLowEnergyMode(boolean value) {
                         mWasCecDisabledOnStandbyByLowEnergyMode = value;
                     }
+
+                    @Override
+                    protected boolean userEnabledCecInOfflineMode() {
+                        return mUserEnabledCecInOfflineMode;
+                    }
                 };
 
         mHdmiControlService.setIoLooper(mMyLooper);
@@ -298,6 +304,7 @@
         mWasCecDisabledOnStandbyByLowEnergyMode = false;
         mDisableCecOnStandbyByLowEnergyMode = false;
         mUseHdmiCecPowerStatusController = false;
+        mUserEnabledCecInOfflineMode = false;
         mNativeWrapper.clearResultMessages();
     }
 
@@ -2400,6 +2407,32 @@
         assertTrue(mVendorCommandListeners.contains(vendorCommandListenerInvocationSettingChange));
     }
 
+    @Test
+    public void lowEnergyMode_userEnabledCecInOfflineMode_onStandby_cecStaysEnabled() {
+        mDisableCecOnStandbyByLowEnergyMode = true;
+        mUseHdmiCecPowerStatusController = true;
+        mUserEnabledCecInOfflineMode = true;
+        mPowerManager.setIsLowPowerStandbyEnabled(true);
+
+        assertEquals(mHdmiCecLocalDeviceTv.mService.getHdmiCecConfig().getIntValue(
+                        HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED),
+                HdmiControlManager.HDMI_CEC_CONTROL_ENABLED);
+        mHdmiControlService.onStandby(STANDBY_SCREEN_OFF);
+        mTestLooper.dispatchAll();
+
+        assertEquals(mHdmiCecLocalDeviceTv.mService.getHdmiCecConfig().getIntValue(
+                        HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED),
+                HdmiControlManager.HDMI_CEC_CONTROL_ENABLED);
+        assertFalse(mWasCecDisabledOnStandbyByLowEnergyMode);
+        mHdmiControlService.onWakeUp(WAKE_UP_SCREEN_ON);
+        mTestLooper.dispatchAll();
+
+        assertEquals(mHdmiCecLocalDeviceTv.mService.getHdmiCecConfig().getIntValue(
+                        HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED),
+                HdmiControlManager.HDMI_CEC_CONTROL_ENABLED);
+        assertFalse(mWasCecDisabledOnStandbyByLowEnergyMode);
+    }
+
     protected static class MockTvDevice extends HdmiCecLocalDeviceTv {
         MockTvDevice(HdmiControlService service) {
             super(service);
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 23203ed..bf103d5 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -746,7 +746,7 @@
      * @hide
      */
     public static final String ACTION_SATELLITE_SUBSCRIBER_ID_LIST_CHANGED =
-            "android.telephony.action.ACTION_SATELLITE_SUBSCRIBER_ID_LIST_CHANGED";
+            "android.telephony.satellite.action.SATELLITE_SUBSCRIBER_ID_LIST_CHANGED";
 
 
     /**
@@ -757,7 +757,7 @@
      * @hide
      */
     public static final String ACTION_SATELLITE_START_NON_EMERGENCY_SESSION =
-            "android.telephony.action.ACTION_SATELLITE_START_NON_EMERGENCY_SESSION";
+            "android.telephony.satellite.action.SATELLITE_START_NON_EMERGENCY_SESSION";
     /**
      * Meta-data represents whether the application supports P2P SMS over carrier roaming satellite
      * which needs manual trigger to connect to satellite. The messaging applications that supports
diff --git a/tools/systemfeatures/Android.bp b/tools/systemfeatures/Android.bp
index e6d0a3d..2ebede3 100644
--- a/tools/systemfeatures/Android.bp
+++ b/tools/systemfeatures/Android.bp
@@ -13,6 +13,7 @@
     srcs: [
         "src/**/*.java",
         "src/**/*.kt",
+        ":framework-metalava-annotations",
     ],
     static_libs: [
         "guava",
@@ -26,6 +27,12 @@
     static_libs: ["systemfeatures-gen-lib"],
 }
 
+java_plugin {
+    name: "systemfeatures-metadata-processor",
+    processor_class: "com.android.systemfeatures.SystemFeaturesMetadataProcessor",
+    static_libs: ["systemfeatures-gen-lib"],
+}
+
 genrule {
     name: "systemfeatures-gen-tests-srcs",
     cmd: "$(location systemfeatures-gen-tool) com.android.systemfeatures.RwNoFeatures --readonly=false > $(location RwNoFeatures.java) && " +
@@ -61,6 +68,7 @@
         "systemfeatures-gen-lib",
         "truth",
     ],
+    plugins: ["systemfeatures-metadata-processor"],
 }
 
 // Rename the goldens as they may be copied into the source tree, and we don't
diff --git a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesMetadataProcessor.kt b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesMetadataProcessor.kt
new file mode 100644
index 0000000..100d869
--- /dev/null
+++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesMetadataProcessor.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemfeatures
+
+import android.annotation.SdkConstant
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.JavaFile
+import com.squareup.javapoet.TypeSpec
+import java.io.IOException
+import javax.annotation.processing.AbstractProcessor
+import javax.annotation.processing.ProcessingEnvironment
+import javax.annotation.processing.RoundEnvironment
+import javax.lang.model.SourceVersion
+import javax.lang.model.element.Modifier
+import javax.lang.model.element.TypeElement
+import javax.tools.Diagnostic
+
+/*
+ * Simple Java code generator for computing metadata for system features.
+ *
+ * <p>The output is a single class file, `com.android.internal.pm.SystemFeaturesMetadata`, with
+ * properties computed from feature constant definitions in the PackageManager class. This
+ * class is only produced if the processed environment includes PackageManager; all other
+ * invocations are ignored.
+ */
+class SystemFeaturesMetadataProcessor : AbstractProcessor() {
+
+    private lateinit var packageManagerType: TypeElement
+
+    override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported()
+
+    override fun getSupportedAnnotationTypes() = setOf(SDK_CONSTANT_ANNOTATION_NAME)
+
+    override fun init(processingEnv: ProcessingEnvironment) {
+        super.init(processingEnv)
+        packageManagerType =
+            processingEnv.elementUtils.getTypeElement("android.content.pm.PackageManager")!!
+    }
+
+    override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
+        if (roundEnv.processingOver()) {
+            return false
+        }
+
+        // We're only interested in feature constants defined in PackageManager.
+        var featureCount = 0
+        roundEnv.getElementsAnnotatedWith(SdkConstant::class.java).forEach {
+            if (
+                it.enclosingElement == packageManagerType &&
+                    it.getAnnotation(SdkConstant::class.java).value ==
+                        SdkConstant.SdkConstantType.FEATURE
+            ) {
+                featureCount++
+            }
+        }
+
+        if (featureCount == 0) {
+            // This is fine, and happens for any environment that doesn't include PackageManager.
+            return false
+        }
+
+        val systemFeatureMetadata =
+            TypeSpec.classBuilder("SystemFeaturesMetadata")
+                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+                .addJavadoc("@hide")
+                .addField(
+                    FieldSpec.builder(Int::class.java, "SDK_FEATURE_COUNT")
+                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
+                        .addJavadoc(
+                            "The number of `@SdkConstant` features defined in PackageManager."
+                        )
+                        .addJavadoc("@hide")
+                        .initializer("\$L", featureCount)
+                        .build()
+                )
+                .build()
+
+        try {
+            JavaFile.builder("com.android.internal.pm", systemFeatureMetadata)
+                .skipJavaLangImports(true)
+                .build()
+                .writeTo(processingEnv.filer)
+        } catch (e: IOException) {
+            processingEnv.messager.printMessage(
+                Diagnostic.Kind.ERROR,
+                "Failed to write file: ${e.message}",
+            )
+        }
+
+        return true
+    }
+
+    companion object {
+        private val SDK_CONSTANT_ANNOTATION_NAME = SdkConstant::class.qualifiedName
+    }
+}
diff --git a/tools/systemfeatures/tests/src/PackageManager.java b/tools/systemfeatures/tests/src/PackageManager.java
index db67048..839a937 100644
--- a/tools/systemfeatures/tests/src/PackageManager.java
+++ b/tools/systemfeatures/tests/src/PackageManager.java
@@ -16,14 +16,33 @@
 
 package android.content.pm;
 
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+
 /** Stub for testing */
 public class PackageManager {
+    @SdkConstant(SdkConstantType.FEATURE)
     public static final String FEATURE_AUTO = "automotive";
+
+    @SdkConstant(SdkConstantType.FEATURE)
     public static final String FEATURE_PC = "pc";
+
+    @SdkConstant(SdkConstantType.FEATURE)
     public static final String FEATURE_VULKAN = "vulkan";
+
+    @SdkConstant(SdkConstantType.FEATURE)
     public static final String FEATURE_WATCH = "watch";
+
+    @SdkConstant(SdkConstantType.FEATURE)
     public static final String FEATURE_WIFI = "wifi";
 
+    @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+    public static final String FEATURE_INTENT_CATEGORY = "intent_category_with_feature_name_prefix";
+
+    public static final String FEATURE_NOT_ANNOTATED = "not_annotated";
+
+    public static final String NOT_FEATURE = "not_feature";
+
     /** @hide */
     public boolean hasSystemFeature(String featureName, int version) {
         return false;
diff --git a/tools/systemfeatures/tests/src/SystemFeaturesMetadataProcessorTest.java b/tools/systemfeatures/tests/src/SystemFeaturesMetadataProcessorTest.java
new file mode 100644
index 0000000..4ffb5b9
--- /dev/null
+++ b/tools/systemfeatures/tests/src/SystemFeaturesMetadataProcessorTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemfeatures;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.internal.pm.SystemFeaturesMetadata;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class SystemFeaturesMetadataProcessorTest {
+
+    @Test
+    public void testSdkFeatureCount() {
+        // See the fake PackageManager definition in this directory.
+        // It defines 5 annotated features, and any/all other constants should be ignored.
+        assertThat(SystemFeaturesMetadata.SDK_FEATURE_COUNT).isEqualTo(5);
+    }
+}