Merge "JavaAdapter: add combineFlows with six inputs" into main
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 1a1f354..4b6c62e 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -91,6 +91,7 @@
     field public static final String BIND_TRANSLATION_SERVICE = "android.permission.BIND_TRANSLATION_SERVICE";
     field public static final String BIND_TRUST_AGENT = "android.permission.BIND_TRUST_AGENT";
     field public static final String BIND_TV_REMOTE_SERVICE = "android.permission.BIND_TV_REMOTE_SERVICE";
+    field @FlaggedApi("android.content.pm.verification_service") public static final String BIND_VERIFICATION_AGENT = "android.permission.BIND_VERIFICATION_AGENT";
     field public static final String BIND_VISUAL_QUERY_DETECTION_SERVICE = "android.permission.BIND_VISUAL_QUERY_DETECTION_SERVICE";
     field public static final String BIND_WALLPAPER_EFFECTS_GENERATION_SERVICE = "android.permission.BIND_WALLPAPER_EFFECTS_GENERATION_SERVICE";
     field public static final String BIND_WEARABLE_SENSING_SERVICE = "android.permission.BIND_WEARABLE_SENSING_SERVICE";
@@ -412,6 +413,7 @@
     field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String USE_ON_DEVICE_INTELLIGENCE = "android.permission.USE_ON_DEVICE_INTELLIGENCE";
     field public static final String USE_RESERVED_DISK = "android.permission.USE_RESERVED_DISK";
     field public static final String UWB_PRIVILEGED = "android.permission.UWB_PRIVILEGED";
+    field @FlaggedApi("android.content.pm.verification_service") public static final String VERIFICATION_AGENT = "android.permission.VERIFICATION_AGENT";
     field @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public static final String VIBRATE_VENDOR_EFFECTS = "android.permission.VIBRATE_VENDOR_EFFECTS";
     field public static final String WHITELIST_AUTO_REVOKE_PERMISSIONS = "android.permission.WHITELIST_AUTO_REVOKE_PERMISSIONS";
     field public static final String WHITELIST_RESTRICTED_PERMISSIONS = "android.permission.WHITELIST_RESTRICTED_PERMISSIONS";
@@ -4303,6 +4305,7 @@
     method @Deprecated @RequiresPermission(android.Manifest.permission.INTENT_FILTER_VERIFICATION_AGENT) public abstract void verifyIntentFilter(int, int, @NonNull java.util.List<java.lang.String>);
     field public static final String ACTION_REQUEST_PERMISSIONS = "android.content.pm.action.REQUEST_PERMISSIONS";
     field public static final String ACTION_REQUEST_PERMISSIONS_FOR_OTHER = "android.content.pm.action.REQUEST_PERMISSIONS_FOR_OTHER";
+    field @FlaggedApi("android.content.pm.verification_service") public static final String ACTION_VERIFY_PACKAGE = "android.content.pm.action.VERIFY_PACKAGE";
     field @FlaggedApi("android.content.pm.asl_in_apk_app_metadata_source") public static final int APP_METADATA_SOURCE_APK = 1; // 0x1
     field @FlaggedApi("android.content.pm.asl_in_apk_app_metadata_source") public static final int APP_METADATA_SOURCE_INSTALLER = 2; // 0x2
     field @FlaggedApi("android.content.pm.asl_in_apk_app_metadata_source") public static final int APP_METADATA_SOURCE_SYSTEM_IMAGE = 3; // 0x3
@@ -4616,6 +4619,61 @@
 
 }
 
+package android.content.pm.verify.pkg {
+
+  @FlaggedApi("android.content.pm.verification_service") public final class VerificationSession implements android.os.Parcelable {
+    method public int describeContents();
+    method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public long extendTimeRemaining(long);
+    method @NonNull public java.util.List<android.content.pm.SharedLibraryInfo> getDeclaredLibraries();
+    method @NonNull public android.os.PersistableBundle getExtensionParams();
+    method public int getId();
+    method public int getInstallSessionId();
+    method @NonNull public String getPackageName();
+    method @NonNull public android.content.pm.SigningInfo getSigningInfo();
+    method @NonNull public android.net.Uri getStagedPackageUri();
+    method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public long getTimeoutTime();
+    method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationComplete(@NonNull android.content.pm.verify.pkg.VerificationStatus);
+    method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationComplete(@NonNull android.content.pm.verify.pkg.VerificationStatus, @NonNull android.os.PersistableBundle);
+    method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationIncomplete(int);
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.verify.pkg.VerificationSession> CREATOR;
+    field public static final int VERIFICATION_INCOMPLETE_NETWORK_LIMITED = 2; // 0x2
+    field public static final int VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE = 1; // 0x1
+    field public static final int VERIFICATION_INCOMPLETE_UNKNOWN = 0; // 0x0
+  }
+
+  @FlaggedApi("android.content.pm.verification_service") public final class VerificationStatus implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getAslStatus();
+    method @NonNull public String getFailureMessage();
+    method public boolean isVerified();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.verify.pkg.VerificationStatus> CREATOR;
+    field public static final int VERIFIER_STATUS_ASL_BAD = 2; // 0x2
+    field public static final int VERIFIER_STATUS_ASL_GOOD = 1; // 0x1
+    field public static final int VERIFIER_STATUS_ASL_UNDEFINED = 0; // 0x0
+  }
+
+  public static final class VerificationStatus.Builder {
+    ctor public VerificationStatus.Builder();
+    method @NonNull public android.content.pm.verify.pkg.VerificationStatus build();
+    method @NonNull public android.content.pm.verify.pkg.VerificationStatus.Builder setAslStatus(int);
+    method @NonNull public android.content.pm.verify.pkg.VerificationStatus.Builder setFailureMessage(@NonNull String);
+    method @NonNull public android.content.pm.verify.pkg.VerificationStatus.Builder setVerified(boolean);
+  }
+
+  @FlaggedApi("android.content.pm.verification_service") public abstract class VerifierService extends android.app.Service {
+    ctor public VerifierService();
+    method @Nullable public android.os.IBinder onBind(@Nullable android.content.Intent);
+    method public abstract void onPackageNameAvailable(@NonNull String);
+    method public abstract void onVerificationCancelled(@NonNull String);
+    method public abstract void onVerificationRequired(@NonNull android.content.pm.verify.pkg.VerificationSession);
+    method public abstract void onVerificationRetry(@NonNull android.content.pm.verify.pkg.VerificationSession);
+    method public abstract void onVerificationTimeout(int);
+  }
+
+}
+
 package android.content.rollback {
 
   public final class PackageRollbackInfo implements android.os.Parcelable {
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index fb2655c..e985f88 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -5039,6 +5039,25 @@
             "android.content.pm.action.REQUEST_PERMISSIONS_FOR_OTHER";
 
     /**
+     * Used by the system to query a {@link android.content.pm.verify.pkg.VerifierService} provider,
+     * which registers itself via an intent-filter handling this action.
+     *
+     * <p class="note">Only the system can bind to such a verifier service. This is protected by the
+     * {@link android.Manifest.permission#BIND_VERIFICATION_AGENT} permission. The verifier service
+     * app should protect the service by adding this permission in the service declaration in its
+     * manifest.
+     * <p>
+     * A verifier service must be a privileged app and hold the
+     * {@link android.Manifest.permission#VERIFICATION_AGENT} permission.
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(android.content.pm.Flags.FLAG_VERIFICATION_SERVICE)
+    @SdkConstant(SdkConstantType.SERVICE_ACTION)
+    public static final String ACTION_VERIFY_PACKAGE = "android.content.pm.action.VERIFY_PACKAGE";
+
+    /**
      * The names of the requested permissions.
      * <p>
      * <strong>Type:</strong> String[]
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 160cbdf..300740e 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -309,3 +309,11 @@
     description: "Feature flag to enable the holder of SYSTEM_APP_PROTECTION_SERVICE role to silently delete packages. To be deprecated by delete_packages_silently."
     bug: "361776825"
 }
+
+flag {
+    name: "verification_service"
+    namespace: "package_manager_service"
+    description: "Feature flag to enable the new verification service."
+    bug: "360129103"
+    is_fixed_read_only: true
+}
diff --git a/core/java/android/content/pm/verify/pkg/IVerificationSessionCallback.aidl b/core/java/android/content/pm/verify/pkg/IVerificationSessionCallback.aidl
new file mode 100644
index 0000000..38a7956
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/IVerificationSessionCallback.aidl
@@ -0,0 +1,34 @@
+/*
+ * 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.content.pm.verify.pkg;
+
+import android.content.pm.verify.pkg.VerificationStatus;
+import android.os.PersistableBundle;
+
+/**
+ * Oneway interface that allows the verifier to send response or verification results back to
+ * the system.
+ * @hide
+ */
+oneway interface IVerificationSessionCallback {
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)")
+    void reportVerificationIncomplete(int verificationId, int reason);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)")
+    void reportVerificationComplete(int verificationId, in VerificationStatus status);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)")
+    void reportVerificationCompleteWithExtensionResponse(int verificationId, in VerificationStatus status, in PersistableBundle response);
+}
diff --git a/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl b/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl
new file mode 100644
index 0000000..7a9484a
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl
@@ -0,0 +1,28 @@
+/*
+ * 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.content.pm.verify.pkg;
+
+/**
+ * Non-oneway interface that allows the verifier to retrieve information from the system.
+ * @hide
+ */
+interface IVerificationSessionInterface {
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)")
+    long getTimeoutTime(int verificationId);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)")
+    long extendTimeRemaining(int verificationId, long additionalMs);
+}
\ No newline at end of file
diff --git a/core/java/android/content/pm/verify/pkg/IVerifierService.aidl b/core/java/android/content/pm/verify/pkg/IVerifierService.aidl
new file mode 100644
index 0000000..d3071fd
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/IVerifierService.aidl
@@ -0,0 +1,31 @@
+/*
+ * 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.content.pm.verify.pkg;
+
+import android.content.pm.verify.pkg.VerificationSession;
+
+/**
+ * Oneway interface that allows the system to communicate to the verifier service agent.
+ * @hide
+ */
+oneway interface IVerifierService {
+    void onPackageNameAvailable(in String packageName);
+    void onVerificationCancelled(in String packageName);
+    void onVerificationRequired(in VerificationSession session);
+    void onVerificationRetry(in VerificationSession session);
+    void onVerificationTimeout(int verificationId);
+}
\ No newline at end of file
diff --git a/core/java/android/content/pm/verify/pkg/VerificationSession.aidl b/core/java/android/content/pm/verify/pkg/VerificationSession.aidl
new file mode 100644
index 0000000..ac85585
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/VerificationSession.aidl
@@ -0,0 +1,20 @@
+/*
+ * 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.content.pm.verify.pkg;
+
+/** @hide */
+parcelable VerificationSession;
diff --git a/core/java/android/content/pm/verify/pkg/VerificationSession.java b/core/java/android/content/pm/verify/pkg/VerificationSession.java
new file mode 100644
index 0000000..70b4a02
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/VerificationSession.java
@@ -0,0 +1,276 @@
+/*
+ * 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.content.pm.verify.pkg;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.content.pm.Flags;
+import android.content.pm.SharedLibraryInfo;
+import android.content.pm.SigningInfo;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * This class is used by the system to describe the details about a verification request sent to the
+ * verification agent, aka the verifier. It includes the interfaces for the verifier to communicate
+ * back to the system.
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE)
+@SystemApi
+public final class VerificationSession implements Parcelable {
+    /**
+     * The verification cannot be completed because of unknown reasons.
+     */
+    public static final int VERIFICATION_INCOMPLETE_UNKNOWN = 0;
+    /**
+     * The verification cannot be completed because the network is unavailable.
+     */
+    public static final int VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE = 1;
+    /**
+     * The verification cannot be completed because the network is limited.
+     */
+    public static final int VERIFICATION_INCOMPLETE_NETWORK_LIMITED = 2;
+
+    /**
+     * @hide
+     */
+    @IntDef(prefix = {"VERIFICATION_INCOMPLETE_"}, value = {
+            VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE,
+            VERIFICATION_INCOMPLETE_NETWORK_LIMITED,
+            VERIFICATION_INCOMPLETE_UNKNOWN,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface VerificationIncompleteReason {
+    }
+
+    private final int mId;
+    private final int mInstallSessionId;
+    @NonNull
+    private final String mPackageName;
+    @NonNull
+    private final Uri mStagedPackageUri;
+    @NonNull
+    private final SigningInfo mSigningInfo;
+    @NonNull
+    private final List<SharedLibraryInfo> mDeclaredLibraries;
+    @NonNull
+    private final PersistableBundle mExtensionParams;
+    @NonNull
+    private final IVerificationSessionInterface mSession;
+    @NonNull
+    private final IVerificationSessionCallback mCallback;
+
+    /**
+     * Constructor used by the system to describe the details of a verification session.
+     * @hide
+     */
+    public VerificationSession(int id, int installSessionId, @NonNull String packageName,
+            @NonNull Uri stagedPackageUri, @NonNull SigningInfo signingInfo,
+            @NonNull List<SharedLibraryInfo> declaredLibraries,
+            @NonNull PersistableBundle extensionParams,
+            @NonNull IVerificationSessionInterface session,
+            @NonNull IVerificationSessionCallback callback) {
+        mId = id;
+        mInstallSessionId = installSessionId;
+        mPackageName = packageName;
+        mStagedPackageUri = stagedPackageUri;
+        mSigningInfo = signingInfo;
+        mDeclaredLibraries = declaredLibraries;
+        mExtensionParams = extensionParams;
+        mSession = session;
+        mCallback = callback;
+    }
+
+    /**
+     * A unique identifier tied to this specific verification session.
+     */
+    public int getId() {
+        return mId;
+    }
+
+    /**
+     * The package name of the app that is to be verified.
+     */
+    public @NonNull String getPackageName() {
+        return mPackageName;
+    }
+
+    /**
+     * The id of the installation session associated with the verification.
+     */
+    public int getInstallSessionId() {
+        return mInstallSessionId;
+    }
+
+    /**
+     * The Uri of the path where the package's code files are located.
+     */
+    public @NonNull Uri getStagedPackageUri() {
+        return mStagedPackageUri;
+    }
+
+    /**
+     * Signing info of the package to be verified.
+     */
+    public @NonNull SigningInfo getSigningInfo() {
+        return mSigningInfo;
+    }
+
+    /**
+     * Returns a mapping of any shared libraries declared in the manifest
+     * to the {@link SharedLibraryInfo#Type} that is declared. This will be an empty
+     * map if no shared libraries are declared by the package.
+     */
+    @NonNull
+    public List<SharedLibraryInfo> getDeclaredLibraries() {
+        return Collections.unmodifiableList(mDeclaredLibraries);
+    }
+
+    /**
+     * Returns any extension params associated with the verification request.
+     */
+    @NonNull
+    public PersistableBundle getExtensionParams() {
+        return mExtensionParams;
+    }
+
+    /**
+     * Get the value of Clock.elapsedRealtime() at which time this verification
+     * will timeout as incomplete if no other verification response is provided.
+     */
+    @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
+    public long getTimeoutTime() {
+        try {
+            return mSession.getTimeoutTime(mId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Extend the timeout for this session by the provided additionalMs to
+     * fetch relevant information over the network or wait for the network.
+     * This may be called multiple times. If the request would bypass any max
+     * duration by the system, the method will return a lower value than the
+     * requested amount that indicates how much the time was extended.
+     */
+    @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
+    public long extendTimeRemaining(long additionalMs) {
+        try {
+            return mSession.extendTimeRemaining(mId, additionalMs);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Report to the system that verification could not be completed along
+     * with an approximate reason to pass on to the installer.
+     */
+    @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
+    public void reportVerificationIncomplete(@VerificationIncompleteReason int reason) {
+        try {
+            mCallback.reportVerificationIncomplete(mId, reason);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Report to the system that the verification has completed and the
+     * install process may act on that status to either block in the case
+     * of failure or continue to process the install in the case of success.
+     */
+    @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
+    public void reportVerificationComplete(@NonNull VerificationStatus status) {
+        try {
+            mCallback.reportVerificationComplete(mId, status);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Same as {@link #reportVerificationComplete(VerificationStatus)}, but also provide
+     * a result to the extension params provided in the request, which will be passed to the
+     * installer in the installation result.
+     */
+    @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
+    public void reportVerificationComplete(@NonNull VerificationStatus status,
+            @NonNull PersistableBundle response) {
+        try {
+            mCallback.reportVerificationCompleteWithExtensionResponse(mId, status, response);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private VerificationSession(@NonNull Parcel in) {
+        mId = in.readInt();
+        mInstallSessionId = in.readInt();
+        mPackageName = in.readString8();
+        mStagedPackageUri = Uri.CREATOR.createFromParcel(in);
+        mSigningInfo = SigningInfo.CREATOR.createFromParcel(in);
+        mDeclaredLibraries = in.createTypedArrayList(SharedLibraryInfo.CREATOR);
+        mExtensionParams = in.readPersistableBundle(getClass().getClassLoader());
+        mSession = IVerificationSessionInterface.Stub.asInterface(in.readStrongBinder());
+        mCallback = IVerificationSessionCallback.Stub.asInterface(in.readStrongBinder());
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mId);
+        dest.writeInt(mInstallSessionId);
+        dest.writeString8(mPackageName);
+        Uri.writeToParcel(dest, mStagedPackageUri);
+        mSigningInfo.writeToParcel(dest, flags);
+        dest.writeTypedList(mDeclaredLibraries);
+        dest.writePersistableBundle(mExtensionParams);
+        dest.writeStrongBinder(mSession.asBinder());
+        dest.writeStrongBinder(mCallback.asBinder());
+    }
+
+    @NonNull
+    public static final Creator<VerificationSession> CREATOR = new Creator<>() {
+        @Override
+        public VerificationSession createFromParcel(@NonNull Parcel in) {
+            return new VerificationSession(in);
+        }
+
+        @Override
+        public VerificationSession[] newArray(int size) {
+            return new VerificationSession[size];
+        }
+    };
+}
diff --git a/core/java/android/content/pm/verify/pkg/VerificationStatus.aidl b/core/java/android/content/pm/verify/pkg/VerificationStatus.aidl
new file mode 100644
index 0000000..6a1cb4f
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/VerificationStatus.aidl
@@ -0,0 +1,20 @@
+/*
+ * 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.content.pm.verify.pkg;
+
+/** @hide */
+parcelable VerificationStatus;
diff --git a/core/java/android/content/pm/verify/pkg/VerificationStatus.java b/core/java/android/content/pm/verify/pkg/VerificationStatus.java
new file mode 100644
index 0000000..4d0379d7
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/VerificationStatus.java
@@ -0,0 +1,166 @@
+/*
+ * 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.content.pm.verify.pkg;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.content.pm.Flags;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class is used by the verifier to describe the status of the verification request, whether
+ * it's successful or it has failed along with any relevant details.
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE)
+public final class  VerificationStatus implements Parcelable {
+    /**
+     * The ASL status has not been determined.  This happens in situations where the verification
+     * service is not monitoring ASLs, and means the ASL data in the app is not necessarily bad but
+     * can't be trusted.
+     */
+    public static final int VERIFIER_STATUS_ASL_UNDEFINED = 0;
+
+    /**
+     * The app's ASL data is considered to be in a good state.
+     */
+    public static final int VERIFIER_STATUS_ASL_GOOD = 1;
+
+    /**
+     * There is something bad in the app's ASL data; the user should be warned about this when shown
+     * the ASL data and/or appropriate decisions made about the use of this data by the platform.
+     */
+    public static final int VERIFIER_STATUS_ASL_BAD = 2;
+
+    /** @hide */
+    @IntDef(prefix = {"VERIFIER_STATUS_ASL_"}, value = {
+            VERIFIER_STATUS_ASL_UNDEFINED,
+            VERIFIER_STATUS_ASL_GOOD,
+            VERIFIER_STATUS_ASL_BAD,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface VerifierStatusAsl {}
+
+    private boolean mIsVerified;
+    private @VerifierStatusAsl int mAslStatus;
+    @NonNull
+    private String mFailuresMessage = "";
+
+    private VerificationStatus() {}
+
+    /**
+     * @return whether the status is set to verified or not.
+     */
+    public boolean isVerified() {
+        return mIsVerified;
+    }
+
+    /**
+     * @return the failure message associated with the failure status.
+     */
+    @NonNull
+    public String getFailureMessage() {
+        return mFailuresMessage;
+    }
+
+    /**
+     * @return the asl status.
+     */
+    public @VerifierStatusAsl int getAslStatus() {
+        return mAslStatus;
+    }
+
+    /**
+     * Builder to construct a {@link VerificationStatus} object.
+     */
+    public static final class Builder {
+        final VerificationStatus mStatus = new VerificationStatus();
+
+        /**
+         * Set in the status whether the verification has succeeded or failed.
+         */
+        @NonNull
+        public Builder setVerified(boolean verified) {
+            mStatus.mIsVerified = verified;
+            return this;
+        }
+
+        /**
+         * Set a developer-facing failure message to include in the verification failure status.
+         */
+        @NonNull
+        public Builder setFailureMessage(@NonNull String failureMessage) {
+            mStatus.mFailuresMessage = failureMessage;
+            return this;
+        }
+
+        /**
+         * Set the ASL status, as defined in {@link VerifierStatusAsl}.
+         */
+        @NonNull
+        public Builder setAslStatus(@VerifierStatusAsl int aslStatus) {
+            mStatus.mAslStatus = aslStatus;
+            return this;
+        }
+
+        /**
+         * Build the status object.
+         */
+        @NonNull
+        public VerificationStatus build() {
+            return mStatus;
+        }
+    }
+
+    private VerificationStatus(Parcel in) {
+        mIsVerified = in.readBoolean();
+        mAslStatus = in.readInt();
+        mFailuresMessage = in.readString8();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeBoolean(mIsVerified);
+        dest.writeInt(mAslStatus);
+        dest.writeString8(mFailuresMessage);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @NonNull
+    public static final Creator<VerificationStatus> CREATOR = new Creator<>() {
+        @Override
+        public VerificationStatus createFromParcel(@NonNull Parcel in) {
+            return new VerificationStatus(in);
+        }
+
+        @Override
+        public VerificationStatus[] newArray(int size) {
+            return new VerificationStatus[size];
+        }
+    };
+}
diff --git a/core/java/android/content/pm/verify/pkg/VerifierService.java b/core/java/android/content/pm/verify/pkg/VerifierService.java
new file mode 100644
index 0000000..ccf2119
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/VerifierService.java
@@ -0,0 +1,113 @@
+/*
+ * 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.content.pm.verify.pkg;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.Flags;
+import android.content.pm.PackageManager;
+import android.os.IBinder;
+
+/**
+ * A base service implementation for the verifier agent to implement.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE)
+public abstract class VerifierService extends Service {
+    /**
+     * Called when a package name is available for a pending verification,
+     * giving the verifier opportunity to pre-fetch any relevant information
+     * that may be needed should a verification for the package be required.
+     */
+    public abstract void onPackageNameAvailable(@NonNull String packageName);
+
+    /**
+     * Called when a package recently provided via {@link #onPackageNameAvailable}
+     * is no longer expected to be installed. This is a hint that any pre-fetch or
+     * cache created as a result of the previous call may be be cleared.
+     * <p>This method will never be called after {@link #onVerificationRequired} is called for the
+     * same package. Once a verification is officially requested by
+     * {@link #onVerificationRequired}, it cannot be cancelled.
+     * </p>
+     */
+    public abstract void onVerificationCancelled(@NonNull String packageName);
+
+    /**
+     * Called when an application needs to be verified. Details about the
+     * verification and actions that can be taken on it will be encapsulated in
+     * the provided {@link VerificationSession} parameter.
+     */
+    public abstract void onVerificationRequired(@NonNull VerificationSession session);
+
+    /**
+     * Called when a verification needs to be retried. This can be encountered
+     * when a prior verification was marked incomplete and the user has indicated
+     * that they've resolved the issue, or when a timeout is reached, but the
+     * the system is attempting to retry. Details about the
+     * verification and actions that can be taken on it will be encapsulated in
+     * the provided {@link VerificationSession} parameter.
+     */
+    public abstract void onVerificationRetry(@NonNull VerificationSession session);
+
+    /**
+     * Called in the case that an active verification has failed. Any APIs called
+     * on the {@link VerificationSession} instance associated with this {@code verificationId} will
+     * throw an {@link IllegalStateException}.
+     */
+    public abstract void onVerificationTimeout(int verificationId);
+
+    /**
+     * Called when the verifier service is bound to the system.
+     */
+    public @Nullable IBinder onBind(@Nullable Intent intent) {
+        if (intent == null || !PackageManager.ACTION_VERIFY_PACKAGE.equals(intent.getAction())) {
+            return null;
+        }
+        return new IVerifierService.Stub() {
+            @Override
+            public void onPackageNameAvailable(@NonNull String packageName) {
+                VerifierService.this.onPackageNameAvailable(packageName);
+            }
+
+            @Override
+            public void onVerificationCancelled(@NonNull String packageName) {
+                VerifierService.this.onVerificationCancelled(packageName);
+            }
+
+            @Override
+            public void onVerificationRequired(@NonNull VerificationSession session) {
+                VerifierService.this.onVerificationRequired(session);
+            }
+
+            @Override
+            public void onVerificationRetry(@NonNull VerificationSession session) {
+                VerifierService.this.onVerificationRetry(session);
+            }
+
+            @Override
+            public void onVerificationTimeout(int verificationId) {
+                VerifierService.this.onVerificationTimeout(verificationId);
+            }
+        };
+    }
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index d35c66e..ed33ede 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -8449,6 +8449,29 @@
     <permission android:name="android.permission.RESERVED_FOR_TESTING_SIGNATURE"
                 android:protectionLevel="signature"/>
 
+    <!-- @SystemApi
+        @FlaggedApi("android.content.pm.verification_service")
+        Allows app to be the verification agent to verify packages.
+        <p>Protection level: signature|privileged
+        @hide
+    -->
+    <permission android:name="android.permission.VERIFICATION_AGENT"
+        android:protectionLevel="signature|privileged"
+        android:featureFlag="android.content.pm.verification_service" />
+
+    <!-- @SystemApi
+        @FlaggedApi("android.content.pm.verification_service")
+        Must be required by a privileged {@link android.content.pm.verify.pkg.VerifierService}
+        to ensure that only the system can bind to it.
+        This permission should not be held by anything other than the system.
+        <p>Not for use by third-party applications. </p>
+        <p>Protection level: signature
+        @hide
+    -->
+    <permission android:name="android.permission.BIND_VERIFICATION_AGENT"
+        android:protectionLevel="internal"
+        android:featureFlag="android.content.pm.verification_service" />
+
     <!-- Attribution for Geofencing service. -->
     <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
     <!-- Attribution for Country Detector. -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 5c0dca2..71ae22f 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2309,10 +2309,6 @@
          spatial audio is enabled for a newly connected audio device -->
     <bool name="config_spatial_audio_head_tracking_enabled_default">false</bool>
 
-    <!-- Flag indicating whether platform level volume adjustments are enabled for remote sessions
-         on grouped devices. -->
-    <bool name="config_volumeAdjustmentForRemoteGroupSessions">true</bool>
-
     <!-- Flag indicating current media Output Switcher version. -->
     <integer name="config_mediaOutputSwitchDialogVersion">1</integer>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 807df1b..0ccef91 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5102,8 +5102,6 @@
 
   <java-symbol type="dimen" name="config_wallpaperDimAmount" />
 
-  <java-symbol type="bool" name="config_volumeAdjustmentForRemoteGroupSessions" />
-
   <java-symbol type="integer" name="config_mediaOutputSwitchDialogVersion" />
 
   <!-- List of shared library packages that should be loaded by the classloader after the
diff --git a/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java b/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java
new file mode 100644
index 0000000..987f68d
--- /dev/null
+++ b/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java
@@ -0,0 +1,155 @@
+/*
+ * 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.content.pm.verify;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.pm.SharedLibraryInfo;
+import android.content.pm.SigningInfo;
+import android.content.pm.VersionedPackage;
+import android.content.pm.verify.pkg.IVerificationSessionCallback;
+import android.content.pm.verify.pkg.IVerificationSessionInterface;
+import android.content.pm.verify.pkg.VerificationSession;
+import android.content.pm.verify.pkg.VerificationStatus;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.PersistableBundle;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VerificationSessionTest {
+    private static final int TEST_ID = 100;
+    private static final int TEST_INSTALL_SESSION_ID = 33;
+    private static final String TEST_PACKAGE_NAME = "com.foo";
+    private static final Uri TEST_PACKAGE_URI = Uri.parse("test://test");
+    private static final SigningInfo TEST_SIGNING_INFO = new SigningInfo();
+    private static final SharedLibraryInfo TEST_SHARED_LIBRARY_INFO1 =
+            new SharedLibraryInfo("sharedLibPath1", TEST_PACKAGE_NAME,
+                    Collections.singletonList("path1"), "sharedLib1", 101,
+                    SharedLibraryInfo.TYPE_DYNAMIC, new VersionedPackage(TEST_PACKAGE_NAME, 1),
+                    null, null, false);
+    private static final SharedLibraryInfo TEST_SHARED_LIBRARY_INFO2 =
+            new SharedLibraryInfo("sharedLibPath2", TEST_PACKAGE_NAME,
+                    Collections.singletonList("path2"), "sharedLib2", 102,
+                    SharedLibraryInfo.TYPE_DYNAMIC,
+                    new VersionedPackage(TEST_PACKAGE_NAME, 2), null, null, false);
+    private static final long TEST_TIMEOUT_TIME = System.currentTimeMillis();
+    private static final long TEST_EXTEND_TIME = 2000L;
+    private static final String TEST_KEY = "test key";
+    private static final String TEST_VALUE = "test value";
+
+    private final ArrayList<SharedLibraryInfo> mTestDeclaredLibraries = new ArrayList<>();
+    private final PersistableBundle mTestExtensionParams = new PersistableBundle();
+    @Mock
+    private IVerificationSessionInterface mTestSessionInterface;
+    @Mock
+    private IVerificationSessionCallback mTestCallback;
+    private VerificationSession mTestSession;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mTestDeclaredLibraries.add(TEST_SHARED_LIBRARY_INFO1);
+        mTestDeclaredLibraries.add(TEST_SHARED_LIBRARY_INFO2);
+        mTestExtensionParams.putString(TEST_KEY, TEST_VALUE);
+        mTestSession = new VerificationSession(TEST_ID, TEST_INSTALL_SESSION_ID,
+                TEST_PACKAGE_NAME, TEST_PACKAGE_URI, TEST_SIGNING_INFO, mTestDeclaredLibraries,
+                mTestExtensionParams, mTestSessionInterface, mTestCallback);
+    }
+
+    @Test
+    public void testParcel() {
+        Parcel parcel = Parcel.obtain();
+        mTestSession.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        VerificationSession sessionFromParcel =
+                VerificationSession.CREATOR.createFromParcel(parcel);
+        assertThat(sessionFromParcel.getId()).isEqualTo(TEST_ID);
+        assertThat(sessionFromParcel.getInstallSessionId()).isEqualTo(TEST_INSTALL_SESSION_ID);
+        assertThat(sessionFromParcel.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(sessionFromParcel.getStagedPackageUri()).isEqualTo(TEST_PACKAGE_URI);
+        assertThat(sessionFromParcel.getSigningInfo().getSigningDetails())
+                .isEqualTo(TEST_SIGNING_INFO.getSigningDetails());
+        List<SharedLibraryInfo> declaredLibrariesFromParcel =
+                sessionFromParcel.getDeclaredLibraries();
+        assertThat(declaredLibrariesFromParcel).hasSize(2);
+        // SharedLibraryInfo doesn't have a "equals" method, so we have to check it indirectly
+        assertThat(declaredLibrariesFromParcel.getFirst().toString())
+                .isEqualTo(TEST_SHARED_LIBRARY_INFO1.toString());
+        assertThat(declaredLibrariesFromParcel.get(1).toString())
+                .isEqualTo(TEST_SHARED_LIBRARY_INFO2.toString());
+        // We can't directly test with PersistableBundle.equals() because the parceled bundle's
+        // structure is different, but all the key/value pairs should be preserved as before.
+        assertThat(sessionFromParcel.getExtensionParams().getString(TEST_KEY))
+                .isEqualTo(mTestExtensionParams.getString(TEST_KEY));
+    }
+
+    @Test
+    public void testInterface() throws Exception {
+        when(mTestSessionInterface.getTimeoutTime(anyInt())).thenAnswer(i -> TEST_TIMEOUT_TIME);
+        when(mTestSessionInterface.extendTimeRemaining(anyInt(), anyLong())).thenAnswer(
+                i -> i.getArguments()[1]);
+
+        assertThat(mTestSession.getTimeoutTime()).isEqualTo(TEST_TIMEOUT_TIME);
+        verify(mTestSessionInterface, times(1)).getTimeoutTime(eq(TEST_ID));
+        assertThat(mTestSession.extendTimeRemaining(TEST_EXTEND_TIME)).isEqualTo(TEST_EXTEND_TIME);
+        verify(mTestSessionInterface, times(1)).extendTimeRemaining(
+                eq(TEST_ID), eq(TEST_EXTEND_TIME));
+    }
+
+    @Test
+    public void testCallback() throws Exception {
+        PersistableBundle response = new PersistableBundle();
+        response.putString("test key", "test value");
+        final VerificationStatus status =
+                new VerificationStatus.Builder().setVerified(true).build();
+        mTestSession.reportVerificationComplete(status);
+        verify(mTestCallback, times(1)).reportVerificationComplete(
+                eq(TEST_ID), eq(status));
+        mTestSession.reportVerificationComplete(status, response);
+        verify(mTestCallback, times(1))
+                .reportVerificationCompleteWithExtensionResponse(
+                        eq(TEST_ID), eq(status), eq(response));
+
+        final int reason = VerificationSession.VERIFICATION_INCOMPLETE_UNKNOWN;
+        mTestSession.reportVerificationIncomplete(reason);
+        verify(mTestCallback, times(1)).reportVerificationIncomplete(
+                eq(TEST_ID), eq(reason));
+    }
+}
diff --git a/core/tests/coretests/src/android/content/pm/verify/VerificationStatusTest.java b/core/tests/coretests/src/android/content/pm/verify/VerificationStatusTest.java
new file mode 100644
index 0000000..67d407a
--- /dev/null
+++ b/core/tests/coretests/src/android/content/pm/verify/VerificationStatusTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.content.pm.verify;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.pm.verify.pkg.VerificationStatus;
+import android.os.Parcel;
+import android.os.PersistableBundle;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VerificationStatusTest {
+    private static final boolean TEST_VERIFIED = true;
+    private static final int TEST_ASL_STATUS = VerificationStatus.VERIFIER_STATUS_ASL_GOOD;
+    private static final String TEST_FAILURE_MESSAGE = "test test";
+    private static final String TEST_KEY = "test key";
+    private static final String TEST_VALUE = "test value";
+    private final PersistableBundle mTestExtras = new PersistableBundle();
+    private VerificationStatus mStatus;
+
+    @Before
+    public void setUpWithBuilder() {
+        mTestExtras.putString(TEST_KEY, TEST_VALUE);
+        mStatus = new VerificationStatus.Builder()
+                .setAslStatus(TEST_ASL_STATUS)
+                .setFailureMessage(TEST_FAILURE_MESSAGE)
+                .setVerified(TEST_VERIFIED)
+                .build();
+    }
+
+    @Test
+    public void testGetters() {
+        assertThat(mStatus.isVerified()).isEqualTo(TEST_VERIFIED);
+        assertThat(mStatus.getAslStatus()).isEqualTo(TEST_ASL_STATUS);
+        assertThat(mStatus.getFailureMessage()).isEqualTo(TEST_FAILURE_MESSAGE);
+    }
+
+    @Test
+    public void testParcel() {
+        Parcel parcel = Parcel.obtain();
+        mStatus.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        VerificationStatus statusFromParcel = VerificationStatus.CREATOR.createFromParcel(parcel);
+        assertThat(statusFromParcel.isVerified()).isEqualTo(TEST_VERIFIED);
+        assertThat(statusFromParcel.getAslStatus()).isEqualTo(TEST_ASL_STATUS);
+        assertThat(statusFromParcel.getFailureMessage()).isEqualTo(TEST_FAILURE_MESSAGE);
+    }
+}
diff --git a/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java b/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java
new file mode 100644
index 0000000..7f73a1e
--- /dev/null
+++ b/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.content.pm.verify;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.SigningInfo;
+import android.content.pm.verify.pkg.IVerifierService;
+import android.content.pm.verify.pkg.VerificationSession;
+import android.content.pm.verify.pkg.VerifierService;
+import android.net.Uri;
+import android.os.PersistableBundle;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VerifierServiceTest {
+    private static final int TEST_ID = 100;
+    private static final int TEST_INSTALL_SESSION_ID = 33;
+    private static final String TEST_PACKAGE_NAME = "com.foo";
+    private static final Uri TEST_PACKAGE_URI = Uri.parse("test://test");
+    private static final SigningInfo TEST_SIGNING_INFO = new SigningInfo();
+    private VerifierService mService;
+    private VerificationSession mSession;
+
+    @Before
+    public void setUp() {
+        mService = Mockito.mock(VerifierService.class, Answers.CALLS_REAL_METHODS);
+        mSession = new VerificationSession(TEST_ID, TEST_INSTALL_SESSION_ID,
+                TEST_PACKAGE_NAME, TEST_PACKAGE_URI, TEST_SIGNING_INFO,
+                new ArrayList<>(),
+                new PersistableBundle(), null, null);
+    }
+
+    @Test
+    public void testBind() throws Exception {
+        Intent intent = Mockito.mock(Intent.class);
+        when(intent.getAction()).thenReturn(PackageManager.ACTION_VERIFY_PACKAGE);
+        IVerifierService binder =
+                (IVerifierService) mService.onBind(intent);
+        assertThat(binder).isNotNull();
+        binder.onPackageNameAvailable(TEST_PACKAGE_NAME);
+        verify(mService).onPackageNameAvailable(eq(TEST_PACKAGE_NAME));
+        binder.onVerificationCancelled(TEST_PACKAGE_NAME);
+        verify(mService).onVerificationCancelled(eq(TEST_PACKAGE_NAME));
+        binder.onVerificationRequired(mSession);
+        verify(mService).onVerificationRequired(eq(mSession));
+        binder.onVerificationRetry(mSession);
+        verify(mService).onVerificationRetry(eq(mSession));
+        binder.onVerificationTimeout(TEST_ID);
+        verify(mService).onVerificationTimeout(eq(TEST_ID));
+    }
+
+    @Test
+    public void testBindFailsWithWrongIntent() {
+        Intent intent = Mockito.mock(Intent.class);
+        when(intent.getAction()).thenReturn(Intent.ACTION_SEND);
+        assertThat(mService.onBind(intent)).isNull();
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 955fe83..985224e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -350,8 +350,17 @@
 
     /** Minimizes the task for [taskId] and [displayId] */
     fun minimizeTask(displayId: Int, taskId: Int) {
-        logD("Minimize Task: display=%d, task=%d", displayId, taskId)
-        desktopTaskDataByDisplayId.getOrCreate(displayId).minimizedTasks.add(taskId)
+        if (displayId == INVALID_DISPLAY) {
+            // When a task vanishes it doesn't have a displayId. Find the display of the task and
+            // mark it as minimized.
+            getDisplayIdForTask(taskId)?.let {
+                minimizeTask(it, taskId)
+            } ?: logW("Minimize task: No display id found for task: taskId=%d", taskId)
+        } else {
+            logD("Minimize Task: display=%d, task=%d", displayId, taskId)
+            desktopTaskDataByDisplayId.getOrCreate(displayId).minimizedTasks.add(taskId)
+        }
+
         if (Flags.enableDesktopWindowingPersistence()) {
             updatePersistentRepository(displayId)
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 125805c..b8bb73b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -44,7 +44,6 @@
 import android.view.DragEvent
 import android.view.SurfaceControl
 import android.view.WindowManager.TRANSIT_CHANGE
-import android.view.WindowManager.TRANSIT_CLOSE
 import android.view.WindowManager.TRANSIT_NONE
 import android.view.WindowManager.TRANSIT_OPEN
 import android.view.WindowManager.TRANSIT_TO_FRONT
@@ -550,7 +549,29 @@
 
     /** Move a task to the front */
     fun moveTaskToFront(taskId: Int) {
-        shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveTaskToFront(task) }
+        val task = shellTaskOrganizer.getRunningTaskInfo(taskId)
+        if (task == null) moveBackgroundTaskToFront(taskId) else moveTaskToFront(task)
+    }
+
+    /**
+     * Launch a background task in desktop. Note that this should be used when we are already in
+     * desktop. If outside of desktop and want to launch a background task in desktop, use
+     * [moveBackgroundTaskToDesktop] instead.
+     */
+    private fun moveBackgroundTaskToFront(taskId: Int) {
+        logV("moveBackgroundTaskToFront taskId=%s", taskId)
+        val wct = WindowContainerTransaction()
+        // TODO: b/342378842 - Instead of using default display, support multiple displays
+        val taskToMinimize: RunningTaskInfo? =
+            addAndGetMinimizeChangesIfNeeded(DEFAULT_DISPLAY, wct, taskId)
+        wct.startTask(
+            taskId,
+            ActivityOptions.makeBasic().apply {
+                launchWindowingMode = WINDOWING_MODE_FREEFORM
+            }.toBundle(),
+        )
+        val transition = transitions.startTransition(TRANSIT_OPEN, wct, null /* handler */)
+        addPendingMinimizeTransition(transition, taskToMinimize)
     }
 
     /** Move a task to the front */
@@ -558,7 +579,8 @@
         logV("moveTaskToFront taskId=%s", taskInfo.taskId)
         val wct = WindowContainerTransaction()
         wct.reorder(taskInfo.token, true)
-        val taskToMinimize = addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo)
+        val taskToMinimize =
+            addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo.taskId)
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
             val transition = transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */)
             addPendingMinimizeTransition(transition, taskToMinimize)
@@ -1254,7 +1276,7 @@
         }
         // Desktop Mode is showing and we're launching a new Task - we might need to minimize
         // a Task.
-        val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task)
+        val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId)
         if (taskToMinimize != null) {
             addPendingMinimizeTransition(transition, taskToMinimize)
             return wct
@@ -1280,7 +1302,8 @@
 
                 // Desktop Mode is already showing and we're launching a new Task - we might need to
                 // minimize another Task.
-                val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task)
+                val taskToMinimize =
+                    addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId)
                 addPendingMinimizeTransition(transition, taskToMinimize)
             }
         }
@@ -1313,14 +1336,11 @@
             // Remove wallpaper activity when the last active task is removed
             removeWallpaperActivity(wct)
         }
-        taskRepository.addClosingTask(task.displayId, task.taskId)
-        // If a CLOSE is triggered on a desktop task, remove the task.
-        if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue() &&
-            taskRepository.isVisibleTask(task.taskId) &&
-            transitionType == TRANSIT_CLOSE
-        ) {
-            wct.removeTask(task.token)
+
+        if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
+            taskRepository.addClosingTask(task.displayId, task.taskId)
         }
+
         taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
             doesAnyTaskRequireTaskbarRounding(
                 task.displayId,
@@ -1425,12 +1445,12 @@
     private fun addAndGetMinimizeChangesIfNeeded(
         displayId: Int,
         wct: WindowContainerTransaction,
-        newTaskInfo: RunningTaskInfo
+        newTaskId: Int
     ): RunningTaskInfo? {
         if (!desktopTasksLimiter.isPresent) return null
         return desktopTasksLimiter
             .get()
-            .addAndGetMinimizeTaskChangesIfNeeded(displayId, wct, newTaskInfo)
+            .addAndGetMinimizeTaskChangesIfNeeded(displayId, wct, newTaskId)
     }
 
     private fun addPendingMinimizeTransition(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
index d84349b..7e0741f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
@@ -208,15 +208,15 @@
     fun addAndGetMinimizeTaskChangesIfNeeded(
             displayId: Int,
             wct: WindowContainerTransaction,
-            newFrontTaskInfo: RunningTaskInfo,
+            newFrontTaskId: Int,
     ): RunningTaskInfo? {
         ProtoLog.v(
                 ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
                 "DesktopTasksLimiter: addMinimizeBackTaskChangesIfNeeded, newFrontTask=%d",
-                newFrontTaskInfo.taskId)
+            newFrontTaskId)
         val newTaskListOrderedFrontToBack = createOrderedTaskListWithGivenTaskInFront(
                 taskRepository.getActiveNonMinimizedOrderedTasks(displayId),
-                newFrontTaskInfo.taskId)
+            newFrontTaskId)
         val taskToMinimize = getTaskToMinimizeIfNeeded(newTaskListOrderedFrontToBack)
         if (taskToMinimize != null) {
             wct.reorder(taskToMinimize.token, false /* onTop */)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index 83cc18b..7f7f105 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -24,6 +24,7 @@
 import android.content.Context;
 import android.util.SparseArray;
 import android.view.SurfaceControl;
+import android.window.flags.DesktopModeFlags;
 
 import com.android.internal.protolog.ProtoLog;
 import com.android.wm.shell.ShellTaskOrganizer;
@@ -121,7 +122,16 @@
 
         if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
             mDesktopModeTaskRepository.ifPresent(repository -> {
-                repository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId);
+                // TODO: b/370038902 - Handle Activity#finishAndRemoveTask.
+                if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()
+                        || repository.isClosingTask(taskInfo.taskId)) {
+                    // A task that's vanishing should be removed:
+                    // - If it's closed by the X button which means it's marked as a closing task.
+                    repository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId);
+                } else {
+                    repository.updateTaskVisibility(taskInfo.displayId, taskInfo.taskId, false);
+                    repository.minimizeTask(taskInfo.displayId, taskInfo.taskId);
+                }
             });
         }
         mWindowDecorationViewModel.onTaskVanished(taskInfo);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index 8077aee..f7ed1dd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -52,7 +52,6 @@
 import android.util.IntArray;
 import android.util.Pair;
 import android.util.Slog;
-import android.view.Display;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.window.PictureInPictureSurfaceTransaction;
@@ -910,6 +909,14 @@
                             "task #" + taskInfo.taskId + " is always_on_top");
                     return;
                 }
+                if (TransitionUtil.isClosingType(change.getMode())
+                        && taskInfo != null && taskInfo.lastParentTaskIdBeforePip > 0) {
+                    // Pinned task is closing as a side effect of the removal of its original Task,
+                    // such transition should be handled by PiP. So cancel the merge here.
+                    cancel(false /* toHome */, false /* withScreenshots */,
+                            "task #" + taskInfo.taskId + " is removed with its original parent");
+                    return;
+                }
                 final boolean isRootTask = taskInfo != null
                         && TransitionInfo.isIndependent(change, info);
                 final boolean isRecentsTask = mRecentsTask != null
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 794f9d8..97ceecc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -820,6 +820,18 @@
     }
 
     @Test
+    fun minimizeTask_withInvalidDisplay_minimizesCorrectTask() {
+        repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 0)
+        repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 0)
+
+        repo.minimizeTask(displayId = INVALID_DISPLAY, taskId = 0)
+
+        assertThat(repo.isMinimizedTask(taskId = 0)).isTrue()
+        assertThat(repo.isMinimizedTask(taskId = 1)).isFalse()
+        assertThat(repo.isMinimizedTask(taskId = 2)).isFalse()
+    }
+
+    @Test
     fun unminimizeTask_unminimizesTask() {
         repo.minimizeTask(displayId = 0, taskId = 0)
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 8870846..2ddb1ac 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -1350,6 +1350,32 @@
   }
 
   @Test
+  fun moveTaskToFront_backgroundTask_launchesTask() {
+    val task = createTaskInfo(1)
+    whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null)
+
+    controller.moveTaskToFront(task.taskId)
+
+    val wct = getLatestWct(type = TRANSIT_OPEN)
+    assertThat(wct.hierarchyOps).hasSize(1)
+    wct.assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM)
+  }
+
+  @Test
+  fun moveTaskToFront_backgroundTaskBringsTasksOverLimit_minimizesBackTask() {
+    val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
+    val task = createTaskInfo(1001)
+    whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null)
+
+    controller.moveTaskToFront(task.taskId)
+
+    val wct = getLatestWct(type = TRANSIT_OPEN)
+    assertThat(wct.hierarchyOps.size).isEqualTo(2) // launch + minimize
+    wct.assertReorderAt(0, freeformTasks[0], toTop = false)
+    wct.assertLaunchTaskAt(1, task.taskId, WINDOWING_MODE_FREEFORM)
+  }
+
+  @Test
   fun moveToNextDisplay_noOtherDisplays() {
     whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY))
     val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
@@ -2075,11 +2101,8 @@
   }
 
   @Test
-  @DisableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
-  )
-  fun handleRequest_backTransition_singleTaskNoToken_noWallpaper_noBackNav_doesNotHandle() {
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+  fun handleRequest_backTransition_singleTaskNoToken_noWallpaper_doesNotHandle() {
     val task = setUpFreeformTask()
 
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
@@ -2112,8 +2135,7 @@
 
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_backTransition_singleTaskNoToken_noBackNav_doesNotHandle() {
+  fun handleRequest_backTransition_singleTaskNoToken_doesNotHandle() {
     val task = setUpFreeformTask()
 
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
@@ -2122,11 +2144,8 @@
   }
 
   @Test
-  @DisableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
-  )
-  fun handleRequest_backTransition_singleTaskWithToken_noWallpaper_noBackNav_doesNotHandle() {
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+  fun handleRequest_backTransition_singleTaskWithToken_noWallpaper_doesNotHandle() {
     val task = setUpFreeformTask()
 
     taskRepository.wallpaperActivityToken = MockToken().token()
@@ -2149,11 +2168,8 @@
   }
 
   @Test
-  @DisableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
-  )
-  fun handleRequest_backTransition_multipleTasks_noWallpaper_noBackNav_doesNotHandle() {
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+  fun handleRequest_backTransition_multipleTasks_noWallpaper_doesNotHandle() {
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
 
@@ -2165,7 +2181,7 @@
 
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_backTransition_multipleTasks_noBackNav_doesNotHandle() {
+  fun handleRequest_backTransition_multipleTasks_doesNotHandle() {
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
 
@@ -2211,11 +2227,8 @@
   }
 
   @Test
-  @EnableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
-  )
-  fun handleRequest_backTransition_nonMinimizadTask_withWallpaper_withBackNav_removesWallpaper() {
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+  fun handleRequest_backTransition_nonMinimizadTask_withWallpaper_removesWallpaper() {
     val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
@@ -2231,11 +2244,8 @@
   }
 
   @Test
-  @DisableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
-  )
-  fun handleRequest_closeTransition_singleTaskNoToken_noWallpaper_noBackNav_doesNotHandle() {
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+  fun handleRequest_closeTransition_singleTaskNoToken_noWallpaper_doesNotHandle() {
     val task = setUpFreeformTask()
 
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
@@ -2244,22 +2254,8 @@
   }
 
   @Test
-  @EnableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
-  )
-  fun handleRequest_closeTransition_singleTaskNoToken_withWallpaper_withBackNav_removesTask() {
-    val task = setUpFreeformTask()
-
-    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
-
-    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, task.token)
-  }
-
-  @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_closeTransition_singleTaskNoToken_noBackNav_doesNotHandle() {
+  fun handleRequest_closeTransition_singleTaskNoToken_doesNotHandle() {
     val task = setUpFreeformTask()
 
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
@@ -2268,11 +2264,8 @@
   }
 
   @Test
-  @DisableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
-  )
-  fun handleRequest_closeTransition_singleTaskWithToken_noWallpaper_noBackNav_doesNotHandle() {
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+  fun handleRequest_closeTransition_singleTaskWithToken_noWallpaper_doesNotHandle() {
     val task = setUpFreeformTask()
 
     taskRepository.wallpaperActivityToken = MockToken().token()
@@ -2282,26 +2275,8 @@
   }
 
   @Test
-  @EnableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
-  )
-  fun handleRequest_closeTransition_singleTaskWithToken_removesWallpaperAndTask() {
-    val task = setUpFreeformTask()
-    val wallpaperToken = MockToken().token()
-
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
-
-    // Should create remove wallpaper transaction
-    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
-    result.assertRemoveAt(index = 1, task.token)
-  }
-
-  @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_closeTransition_singleTaskWithToken_withWallpaper_noBackNav_removesWallpaper() {
+  fun handleRequest_closeTransition_singleTaskWithToken_withWallpaper_removesWallpaper() {
     val task = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
 
@@ -2313,11 +2288,8 @@
   }
 
   @Test
-  @DisableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
-  )
-  fun handleRequest_closeTransition_multipleTasks_noWallpaper_noBackNav_doesNotHandle() {
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+  fun handleRequest_closeTransition_multipleTasks_noWallpaper_doesNotHandle() {
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
 
@@ -2328,25 +2300,8 @@
   }
 
   @Test
-  @EnableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
-  )
-  fun handleRequest_closeTransition_multipleTasks_withWallpaper_withBackNav_removesTask() {
-    val task1 = setUpFreeformTask()
-    setUpFreeformTask()
-
-    taskRepository.wallpaperActivityToken = MockToken().token()
-    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
-
-    assertNotNull(result, "Should handle request")
-    result.assertRemoveAt(index = 0, task1.token)
-  }
-
-  @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_closeTransition_multipleTasksFlagEnabled_noBackNav_doesNotHandle() {
+  fun handleRequest_closeTransition_multipleTasksFlagEnabled_doesNotHandle() {
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
 
@@ -2357,28 +2312,8 @@
   }
 
   @Test
-  @EnableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
-  )
-  fun handleRequest_closeTransition_multipleTasksSingleNonClosing_removesWallpaperAndTask() {
-    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val wallpaperToken = MockToken().token()
-
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
-    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
-
-    // Should create remove wallpaper transaction
-    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
-    result.assertRemoveAt(index = 1, task1.token)
-  }
-
-  @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_closeTransition_multipleTasksSingleNonClosing_noBackNav_removesWallpaper() {
+  fun handleRequest_closeTransition_multipleTasksSingleNonClosing_removesWallpaper() {
     val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
@@ -2392,28 +2327,8 @@
   }
 
   @Test
-  @EnableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
-  )
-  fun handleRequest_closeTransition_multipleTasksOneNonMinimized_removesWallpaperAndTask() {
-    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val wallpaperToken = MockToken().token()
-
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
-    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
-
-    // Should create remove wallpaper transaction
-    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
-    result.assertRemoveAt(index = 1, task1.token)
-  }
-
-  @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_noBackNav_removesWallpaper() {
+  fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_removesWallpaper() {
     val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
@@ -2427,11 +2342,8 @@
   }
 
   @Test
-  @EnableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
-  )
-  fun handleRequest_closeTransition_minimizadTask_withWallpaper_withBackNav_removesWallpaper() {
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+  fun handleRequest_closeTransition_minimizadTask_withWallpaper_removesWallpaper() {
     val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
index 045e077..bc5ae97 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
@@ -291,7 +291,7 @@
                 desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded(
                         displayId = DEFAULT_DISPLAY,
                         wct = wct,
-                        newFrontTaskInfo = setUpFreeformTask())
+                        newFrontTaskId = setUpFreeformTask().taskId)
 
         assertThat(minimizedTaskId).isNull()
         assertThat(wct.hierarchyOps).isEmpty() // No reordering operations added
@@ -307,7 +307,7 @@
                 desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded(
                         displayId = DEFAULT_DISPLAY,
                         wct = wct,
-                        newFrontTaskInfo = setUpFreeformTask())
+                        newFrontTaskId = setUpFreeformTask().taskId)
 
         assertThat(minimizedTaskId).isEqualTo(tasks.first())
         assertThat(wct.hierarchyOps.size).isEqualTo(1)
@@ -325,7 +325,7 @@
                 desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded(
                         displayId = 0,
                         wct = wct,
-                        newFrontTaskInfo = setUpFreeformTask())
+                        newFrontTaskId = setUpFreeformTask().taskId)
 
         assertThat(minimizedTaskId).isNull()
         assertThat(wct.hierarchyOps).isEmpty() // No reordering operations added
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
index 763d015..3b2c7e6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
@@ -18,15 +18,19 @@
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.Display.INVALID_DISPLAY;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
+import android.platform.test.annotations.EnableFlags;
 import android.view.SurfaceControl;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -139,6 +143,40 @@
         verify(mLaunchAdjacentController).setLaunchAdjacentEnabled(true);
     }
 
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+    public void onTaskVanished_nonClosingTask_isMinimized() {
+        ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder()
+                .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+        task.isVisible = true;
+
+        mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl);
+
+        task.isVisible = false;
+        task.displayId = INVALID_DISPLAY;
+        mFreeformTaskListener.onTaskVanished(task);
+
+        verify(mDesktopModeTaskRepository).minimizeTask(task.displayId, task.taskId);
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+    public void onTaskVanished_closingTask_isNotMinimized() {
+        ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder()
+                .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+        task.isVisible = true;
+
+        mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl);
+
+        when(mDesktopModeTaskRepository.isClosingTask(task.taskId)).thenReturn(true);
+        task.isVisible = false;
+        task.displayId = INVALID_DISPLAY;
+        mFreeformTaskListener.onTaskVanished(task);
+
+        verify(mDesktopModeTaskRepository, never()).minimizeTask(task.displayId, task.taskId);
+        verify(mDesktopModeTaskRepository).removeFreeformTask(task.displayId, task.taskId);
+    }
+
     @After
     public void tearDown() {
         mMockitoSession.finishMocking();
diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java
index 9899e4e..83a4dd5 100644
--- a/media/java/android/media/RoutingSessionInfo.java
+++ b/media/java/android/media/RoutingSessionInfo.java
@@ -22,7 +22,6 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.content.res.Resources;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -57,8 +56,6 @@
                 }
             };
 
-    private static final String TAG = "RoutingSessionInfo";
-
     private static final String KEY_GROUP_ROUTE = "androidx.mediarouter.media.KEY_GROUP_ROUTE";
     private static final String KEY_VOLUME_HANDLING = "volumeHandling";
 
@@ -142,15 +139,7 @@
         mVolume = builder.mVolume;
 
         mIsSystemSession = builder.mIsSystemSession;
-
-        boolean volumeAdjustmentForRemoteGroupSessions = Resources.getSystem().getBoolean(
-                com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions);
-        mVolumeHandling =
-                defineVolumeHandling(
-                        mIsSystemSession,
-                        builder.mVolumeHandling,
-                        mSelectedRoutes,
-                        volumeAdjustmentForRemoteGroupSessions);
+        mVolumeHandling = builder.mVolumeHandling;
 
         mControlHints = updateVolumeHandlingInHints(builder.mControlHints, mVolumeHandling);
         mTransferReason = builder.mTransferReason;
@@ -207,20 +196,6 @@
         return controlHints;
     }
 
-    @MediaRoute2Info.PlaybackVolume
-    private static int defineVolumeHandling(
-            boolean isSystemSession,
-            @MediaRoute2Info.PlaybackVolume int volumeHandling,
-            List<String> selectedRoutes,
-            boolean volumeAdjustmentForRemoteGroupSessions) {
-        if (!isSystemSession
-                && !volumeAdjustmentForRemoteGroupSessions
-                && selectedRoutes.size() > 1) {
-            return MediaRoute2Info.PLAYBACK_VOLUME_FIXED;
-        }
-        return volumeHandling;
-    }
-
     @NonNull
     private static String ensureString(@Nullable String str) {
         return str != null ? str : "";
diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/RoutingSessionInfoTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/RoutingSessionInfoTest.java
index 3955ff0..5f5058d 100644
--- a/media/tests/MediaRouter/src/com/android/mediaroutertest/RoutingSessionInfoTest.java
+++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/RoutingSessionInfoTest.java
@@ -18,8 +18,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import android.content.res.Resources;
-import android.media.MediaRoute2Info;
 import android.media.RoutingSessionInfo;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -95,24 +93,4 @@
         assertThat(sessionInfoWithProviderId2.getTransferableRoutes())
                 .isEqualTo(sessionInfoWithProviderId.getTransferableRoutes());
     }
-
-    @Test
-    public void testGetVolumeHandlingGroupSession() {
-        RoutingSessionInfo sessionInfo = new RoutingSessionInfo.Builder(
-                TEST_ID, TEST_CLIENT_PACKAGE_NAME)
-                .setName(TEST_NAME)
-                .addSelectedRoute(TEST_ROUTE_ID_0)
-                .addSelectedRoute(TEST_ROUTE_ID_2)
-                .setVolumeHandling(MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE)
-                .build();
-
-        boolean volumeAdjustmentForRemoteGroupSessions = Resources.getSystem().getBoolean(
-                com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions);
-
-        int expectedResult = volumeAdjustmentForRemoteGroupSessions
-                ? MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE :
-                MediaRoute2Info.PLAYBACK_VOLUME_FIXED;
-
-        assertThat(sessionInfo.getVolumeHandling()).isEqualTo(expectedResult);
-    }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index dc9e267..56de096 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -132,7 +132,6 @@
             state = state,
             modifier = modifier.fillMaxSize(),
             swipeSourceDetector = viewModel.edgeDetector,
-            gestureFilter = viewModel::shouldFilterGesture,
         ) {
             sceneByKey.forEach { (sceneKey, scene) ->
                 scene(
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index c8adac0..5fa5db8 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -124,10 +124,6 @@
         overSlop: Float,
         pointersDown: Int,
     ): DragController {
-        if (startedPosition != null && layoutImpl.gestureFilter(startedPosition)) {
-            return NoOpDragController
-        }
-
         if (overSlop == 0f) {
             val oldDragController = dragController
             check(oldDragController != null && oldDragController.isDrivingTransition) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index dc3135d..5ddc284 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -42,10 +42,8 @@
 import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
 import androidx.compose.ui.node.DelegatingNode
 import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.node.ObserverModifierNode
 import androidx.compose.ui.node.PointerInputModifierNode
 import androidx.compose.ui.node.currentValueOf
-import androidx.compose.ui.node.observeReads
 import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.Velocity
@@ -79,7 +77,6 @@
 @Stable
 internal fun Modifier.multiPointerDraggable(
     orientation: Orientation,
-    enabled: () -> Boolean,
     startDragImmediately: (startedPosition: Offset) -> Boolean,
     onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
     onFirstPointerDown: () -> Unit = {},
@@ -89,7 +86,6 @@
     this.then(
         MultiPointerDraggableElement(
             orientation,
-            enabled,
             startDragImmediately,
             onDragStarted,
             onFirstPointerDown,
@@ -100,7 +96,6 @@
 
 private data class MultiPointerDraggableElement(
     private val orientation: Orientation,
-    private val enabled: () -> Boolean,
     private val startDragImmediately: (startedPosition: Offset) -> Boolean,
     private val onDragStarted:
         (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
@@ -111,7 +106,6 @@
     override fun create(): MultiPointerDraggableNode =
         MultiPointerDraggableNode(
             orientation = orientation,
-            enabled = enabled,
             startDragImmediately = startDragImmediately,
             onDragStarted = onDragStarted,
             onFirstPointerDown = onFirstPointerDown,
@@ -121,7 +115,6 @@
 
     override fun update(node: MultiPointerDraggableNode) {
         node.orientation = orientation
-        node.enabled = enabled
         node.startDragImmediately = startDragImmediately
         node.onDragStarted = onDragStarted
         node.onFirstPointerDown = onFirstPointerDown
@@ -131,7 +124,6 @@
 
 internal class MultiPointerDraggableNode(
     orientation: Orientation,
-    enabled: () -> Boolean,
     var startDragImmediately: (startedPosition: Offset) -> Boolean,
     var onDragStarted:
         (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
@@ -142,21 +134,10 @@
     DelegatingNode(),
     PointerInputModifierNode,
     CompositionLocalConsumerModifierNode,
-    ObserverModifierNode,
     SpaceVectorConverter {
     private val pointerTracker = delegate(SuspendingPointerInputModifierNode { pointerTracker() })
     private val pointerInput = delegate(SuspendingPointerInputModifierNode { pointerInput() })
     private val velocityTracker = VelocityTracker()
-    private var previousEnabled: Boolean = false
-
-    var enabled: () -> Boolean = enabled
-        set(value) {
-            // Reset the pointer input whenever enabled changed.
-            if (value != field) {
-                field = value
-                pointerInput.resetPointerInputHandler()
-            }
-        }
 
     private var converter = SpaceVectorConverter(orientation)
 
@@ -178,21 +159,6 @@
             }
         }
 
-    override fun onAttach() {
-        previousEnabled = enabled()
-        onObservedReadsChanged()
-    }
-
-    override fun onObservedReadsChanged() {
-        observeReads {
-            val newEnabled = enabled()
-            if (newEnabled != previousEnabled) {
-                pointerInput.resetPointerInputHandler()
-            }
-            previousEnabled = newEnabled
-        }
-    }
-
     override fun onCancelPointerInput() {
         pointerTracker.onCancelPointerInput()
         pointerInput.onCancelPointerInput()
@@ -254,9 +220,7 @@
                         velocityTracker.resetTracking()
                         velocityTracker.addPointerInputChange(firstPointerDown)
                         startedPosition = firstPointerDown.position
-                        if (enabled()) {
-                            onFirstPointerDown()
-                        }
+                        onFirstPointerDown()
                     }
 
                     // Changes with at least one pointer
@@ -295,10 +259,6 @@
     }
 
     private suspend fun PointerInputScope.pointerInput() {
-        if (!enabled()) {
-            return
-        }
-
         val currentContext = currentCoroutineContext()
         awaitPointerEventScope {
             while (currentContext.isActive) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 6e89814..cec8883 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -47,9 +47,6 @@
  * @param state the state of this layout.
  * @param swipeSourceDetector the edge detector used to detect which edge a swipe is started from,
  *   if any.
- * @param gestureFilter decides whether a drag gesture that started at the given start position
- *   should be filtered. If the lambda returns `true`, the drag gesture will be ignored. If it
- *   returns `false`, the drag gesture will be handled.
  * @param transitionInterceptionThreshold used during a scene transition. For the scene to be
  *   intercepted, the progress value must be above the threshold, and below (1 - threshold).
  * @param builder the configuration of the different scenes and overlays of this layout.
@@ -60,7 +57,6 @@
     modifier: Modifier = Modifier,
     swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
     swipeDetector: SwipeDetector = DefaultSwipeDetector,
-    gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter,
     @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0.05f,
     builder: SceneTransitionLayoutScope.() -> Unit,
 ) {
@@ -69,7 +65,6 @@
         modifier,
         swipeSourceDetector,
         swipeDetector,
-        gestureFilter,
         transitionInterceptionThreshold,
         onLayoutImpl = null,
         builder,
@@ -621,7 +616,6 @@
     modifier: Modifier = Modifier,
     swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
     swipeDetector: SwipeDetector = DefaultSwipeDetector,
-    gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter,
     transitionInterceptionThreshold: Float = 0f,
     onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null,
     builder: SceneTransitionLayoutScope.() -> Unit,
@@ -638,7 +632,6 @@
                 transitionInterceptionThreshold = transitionInterceptionThreshold,
                 builder = builder,
                 animationScope = animationScope,
-                gestureFilter = gestureFilter,
             )
             .also { onLayoutImpl?.invoke(it) }
     }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 9e7be37..65c4043 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -31,7 +31,6 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.ApproachLayoutModifierNode
 import androidx.compose.ui.layout.ApproachMeasureScope
 import androidx.compose.ui.layout.LookaheadScope
@@ -71,7 +70,6 @@
      * animations.
      */
     internal val animationScope: CoroutineScope,
-    internal val gestureFilter: (startedPosition: Offset) -> Boolean,
 ) {
     /**
      * The map of [Scene]s.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
index f758102..54ee783 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
@@ -17,7 +17,6 @@
 package com.android.compose.animation.scene
 
 import androidx.compose.runtime.Stable
-import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.pointer.PointerInputChange
 
 /** {@link SwipeDetector} helps determine whether a swipe gestured has occurred. */
@@ -32,8 +31,6 @@
 
 val DefaultSwipeDetector = PassthroughSwipeDetector()
 
-val DefaultGestureFilter = { _: Offset -> false }
-
 /** An {@link SwipeDetector} implementation that recognizes a swipe on any input. */
 class PassthroughSwipeDetector : SwipeDetector {
     override fun detectSwipe(change: PointerInputChange): Boolean {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index 98d4aaa..d201be9 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -41,7 +41,28 @@
     draggableHandler: DraggableHandlerImpl,
     swipeDetector: SwipeDetector,
 ): Modifier {
-    return this.then(SwipeToSceneElement(draggableHandler, swipeDetector))
+    return if (draggableHandler.enabled()) {
+        this.then(SwipeToSceneElement(draggableHandler, swipeDetector))
+    } else {
+        this
+    }
+}
+
+private fun DraggableHandlerImpl.enabled(): Boolean {
+    return isDrivingTransition || contentForSwipes().shouldEnableSwipes(orientation)
+}
+
+private fun DraggableHandlerImpl.contentForSwipes(): Content {
+    return layoutImpl.contentForUserActions()
+}
+
+/** Whether swipe should be enabled in the given [orientation]. */
+private fun Content.shouldEnableSwipes(orientation: Orientation): Boolean {
+    if (userActions.isEmpty()) {
+        return false
+    }
+
+    return userActions.keys.any { it is Swipe.Resolved && it.direction.orientation == orientation }
 }
 
 private data class SwipeToSceneElement(
@@ -64,7 +85,6 @@
         delegate(
             MultiPointerDraggableNode(
                 orientation = draggableHandler.orientation,
-                enabled = ::enabled,
                 startDragImmediately = ::startDragImmediately,
                 onDragStarted = draggableHandler::onDragStarted,
                 onFirstPointerDown = ::onFirstPointerDown,
@@ -124,22 +144,6 @@
 
     override fun onCancelPointerInput() = multiPointerDraggableNode.onCancelPointerInput()
 
-    private fun enabled(): Boolean {
-        return draggableHandler.isDrivingTransition ||
-            contentForSwipes().shouldEnableSwipes(multiPointerDraggableNode.orientation)
-    }
-
-    private fun contentForSwipes(): Content {
-        return draggableHandler.layoutImpl.contentForUserActions()
-    }
-
-    /** Whether swipe should be enabled in the given [orientation]. */
-    private fun Content.shouldEnableSwipes(orientation: Orientation): Boolean {
-        return userActions.keys.any {
-            it is Swipe.Resolved && it.direction.orientation == orientation
-        }
-    }
-
     private fun startDragImmediately(startedPosition: Offset): Boolean {
         // Immediately start the drag if the user can't swipe in the other direction and the gesture
         // handler can intercept it.
@@ -152,7 +156,7 @@
                 Orientation.Vertical -> Orientation.Horizontal
                 Orientation.Horizontal -> Orientation.Vertical
             }
-        return contentForSwipes().shouldEnableSwipes(oppositeOrientation)
+        return draggableHandler.contentForSwipes().shouldEnableSwipes(oppositeOrientation)
     }
 }
 
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 16dc0d5..dd4f99f 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -109,8 +109,6 @@
 
         val transitionInterceptionThreshold = 0.05f
 
-        var gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter
-
         private val layoutImpl =
             SceneTransitionLayoutImpl(
                     state = layoutState,
@@ -123,7 +121,6 @@
                     // Use testScope and not backgroundScope here because backgroundScope does not
                     // work well with advanceUntilIdle(), which is used by some tests.
                     animationScope = testScope,
-                    gestureFilter = { startedPosition -> gestureFilter.invoke(startedPosition) },
                 )
                 .apply { setContentsAndLayoutTargetSizeForTest(LAYOUT_SIZE) }
 
@@ -352,13 +349,6 @@
     }
 
     @Test
-    fun onDragStarted_doesNotStartTransition_whenGestureFiltered() = runGestureTest {
-        gestureFilter = { _ -> true }
-        onDragStarted(overSlop = down(fractionOfScreen = 0.1f), expectedConsumedOverSlop = 0f)
-        assertIdle(currentScene = SceneA)
-    }
-
-    @Test
     fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest {
         val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f))
         assertTransition(currentScene = SceneA)
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
index 493f3a1..c8f6e6d 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
@@ -45,6 +45,7 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Velocity
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.modifiers.thenIf
 import com.android.compose.nestedscroll.SuspendedValue
 import com.google.common.truth.Truth.assertThat
 import kotlin.properties.Delegates
@@ -94,19 +95,20 @@
             Box(
                 Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() })
                     .nestedScrollDispatcher()
-                    .multiPointerDraggable(
-                        orientation = Orientation.Vertical,
-                        enabled = { enabled },
-                        startDragImmediately = { false },
-                        onDragStarted = { _, _, _ ->
-                            started = true
-                            SimpleDragController(
-                                onDrag = { dragged = true },
-                                onStop = { stopped = true },
-                            )
-                        },
-                        dispatcher = defaultDispatcher,
-                    )
+                    .thenIf(enabled) {
+                        Modifier.multiPointerDraggable(
+                            orientation = Orientation.Vertical,
+                            startDragImmediately = { false },
+                            onDragStarted = { _, _, _ ->
+                                started = true
+                                SimpleDragController(
+                                    onDrag = { dragged = true },
+                                    onStop = { stopped = true },
+                                )
+                            },
+                            dispatcher = defaultDispatcher,
+                        )
+                    }
             )
         }
 
@@ -164,7 +166,6 @@
                     .nestedScrollDispatcher()
                     .multiPointerDraggable(
                         orientation = Orientation.Vertical,
-                        enabled = { true },
                         // We want to start a drag gesture immediately
                         startDragImmediately = { true },
                         onDragStarted = { _, _, _ ->
@@ -238,7 +239,6 @@
                     .nestedScrollDispatcher()
                     .multiPointerDraggable(
                         orientation = Orientation.Vertical,
-                        enabled = { true },
                         startDragImmediately = { false },
                         onDragStarted = { _, _, _ ->
                             started = true
@@ -358,7 +358,6 @@
                     .nestedScrollDispatcher()
                     .multiPointerDraggable(
                         orientation = Orientation.Vertical,
-                        enabled = { true },
                         startDragImmediately = { false },
                         onDragStarted = { _, _, _ ->
                             started = true
@@ -464,7 +463,6 @@
                     .nestedScrollDispatcher()
                     .multiPointerDraggable(
                         orientation = Orientation.Vertical,
-                        enabled = { true },
                         startDragImmediately = { false },
                         onDragStarted = { _, _, _ ->
                             verticalStarted = true
@@ -477,7 +475,6 @@
                     )
                     .multiPointerDraggable(
                         orientation = Orientation.Horizontal,
-                        enabled = { true },
                         startDragImmediately = { false },
                         onDragStarted = { _, _, _ ->
                             horizontalStarted = true
@@ -570,7 +567,6 @@
                     .nestedScrollDispatcher()
                     .multiPointerDraggable(
                         orientation = Orientation.Vertical,
-                        enabled = { true },
                         startDragImmediately = { false },
                         swipeDetector =
                             object : SwipeDetector {
@@ -672,7 +668,6 @@
                     .nestedScrollDispatcher()
                     .multiPointerDraggable(
                         orientation = Orientation.Vertical,
-                        enabled = { true },
                         startDragImmediately = { false },
                         onDragStarted = { _, _, _ ->
                             SimpleDragController(
@@ -744,7 +739,6 @@
                     .nestedScrollDispatcher()
                     .multiPointerDraggable(
                         orientation = Orientation.Vertical,
-                        enabled = { true },
                         startDragImmediately = { false },
                         onDragStarted = { _, _, _ ->
                             SimpleDragController(
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index 25e8713..ce64628 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -22,11 +22,15 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
@@ -36,8 +40,11 @@
 import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTextEquals
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performClick
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.test.swipeWithVelocity
 import androidx.compose.ui.unit.Density
@@ -844,4 +851,29 @@
         assertThat(transition.progress).isEqualTo(1f)
         assertThat(availableOnPostScroll).isEqualTo(ovescrollPx)
     }
+
+    @Test
+    fun sceneWithoutSwipesDoesNotConsumeGestures() {
+        val buttonTag = "button"
+
+        rule.setContent {
+            Box {
+                var count by remember { mutableStateOf(0) }
+                Button(onClick = { count++ }, Modifier.testTag(buttonTag).align(Alignment.Center)) {
+                    Text("Count: $count")
+                }
+
+                SceneTransitionLayout(remember { MutableSceneTransitionLayoutState(SceneA) }) {
+                    scene(SceneA) { Box(Modifier.fillMaxSize()) }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 0")
+
+        // Click on the root at its center, where the button is located. Clicks should go through
+        // the STL and reach the button given that there is no swipes for the current scene.
+        repeat(3) { rule.onRoot().performClick() }
+        rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 3")
+    }
 }
diff --git a/packages/SystemUI/customization/Android.bp b/packages/SystemUI/customization/Android.bp
index c399abc..81d92fa 100644
--- a/packages/SystemUI/customization/Android.bp
+++ b/packages/SystemUI/customization/Android.bp
@@ -36,6 +36,7 @@
         "SystemUIPluginLib",
         "SystemUIUnfoldLib",
         "kotlinx_coroutines",
+        "monet",
         "dagger2",
         "jsr330",
     ],
diff --git a/packages/SystemUI/customization/res/values/ids.xml b/packages/SystemUI/customization/res/values/ids.xml
index 5eafbfc..ec466f0 100644
--- a/packages/SystemUI/customization/res/values/ids.xml
+++ b/packages/SystemUI/customization/res/values/ids.xml
@@ -6,4 +6,13 @@
     <item type="id" name="weather_clock_weather_icon" />
     <item type="id" name="weather_clock_temperature" />
     <item type="id" name="weather_clock_alarm_dnd" />
+
+    <item type="id" name="HOUR_DIGIT_PAIR"/>
+    <item type="id" name="MINUTE_DIGIT_PAIR"/>
+    <item type="id" name="HOUR_FIRST_DIGIT"/>
+    <item type="id" name="HOUR_SECOND_DIGIT"/>
+    <item type="id" name="MINUTE_FIRST_DIGIT"/>
+    <item type="id" name="MINUTE_SECOND_DIGIT"/>
+    <item type="id" name="TIME_FULL_FORMAT"/>
+    <item type="id" name="DATE_FORMAT"/>
 </resources>
\ No newline at end of file
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 1863cd8..9877406 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -62,6 +62,7 @@
     // implement the get method and ensure a value is returned before initialization is complete.
     private var logger = DEFAULT_LOGGER
         get() = field ?: DEFAULT_LOGGER
+
     var messageBuffer: MessageBuffer
         get() = logger.buffer
         set(value) {
@@ -123,24 +124,24 @@
                 attrs,
                 R.styleable.AnimatableClockView,
                 defStyleAttr,
-                defStyleRes
+                defStyleRes,
             )
 
         try {
             dozingWeightInternal =
                 animatableClockViewAttributes.getInt(
                     R.styleable.AnimatableClockView_dozeWeight,
-                    /* default = */ 100
+                    /* default = */ 100,
                 )
             lockScreenWeightInternal =
                 animatableClockViewAttributes.getInt(
                     R.styleable.AnimatableClockView_lockScreenWeight,
-                    /* default = */ 300
+                    /* default = */ 300,
                 )
             chargeAnimationDelay =
                 animatableClockViewAttributes.getInt(
                     R.styleable.AnimatableClockView_chargeAnimationDelay,
-                    /* default = */ 200
+                    /* default = */ 200,
                 )
         } finally {
             animatableClockViewAttributes.recycle()
@@ -151,14 +152,14 @@
                 attrs,
                 android.R.styleable.TextView,
                 defStyleAttr,
-                defStyleRes
+                defStyleRes,
             )
 
         try {
             isSingleLineInternal =
                 textViewAttributes.getBoolean(
                     android.R.styleable.TextView_singleLine,
-                    /* default = */ false
+                    /* default = */ false,
                 )
         } finally {
             textViewAttributes.recycle()
@@ -280,7 +281,7 @@
         text: CharSequence,
         start: Int,
         lengthBefore: Int,
-        lengthAfter: Int
+        lengthAfter: Int,
     ) {
         logger.d({ "onTextChanged($str1)" }) { str1 = text.toString() }
         super.onTextChanged(text, start, lengthBefore, lengthAfter)
@@ -305,7 +306,7 @@
             interpolator = null,
             duration = 0,
             delay = 0,
-            onAnimationEnd = null
+            onAnimationEnd = null,
         )
         setTextStyle(
             weight = lockScreenWeight,
@@ -314,7 +315,7 @@
             interpolator = null,
             duration = COLOR_ANIM_DURATION,
             delay = 0,
-            onAnimationEnd = null
+            onAnimationEnd = null,
         )
     }
 
@@ -327,7 +328,7 @@
             interpolator = null,
             duration = 0,
             delay = 0,
-            onAnimationEnd = null
+            onAnimationEnd = null,
         )
         setTextStyle(
             weight = lockScreenWeight,
@@ -336,7 +337,7 @@
             duration = APPEAR_ANIM_DURATION,
             interpolator = Interpolators.EMPHASIZED_DECELERATE,
             delay = 0,
-            onAnimationEnd = null
+            onAnimationEnd = null,
         )
     }
 
@@ -353,7 +354,7 @@
             interpolator = null,
             duration = 0,
             delay = 0,
-            onAnimationEnd = null
+            onAnimationEnd = null,
         )
         setTextStyle(
             weight = dozingWeightInternal,
@@ -362,7 +363,7 @@
             interpolator = Interpolators.EMPHASIZED_DECELERATE,
             duration = ANIMATION_DURATION_FOLD_TO_AOD.toLong(),
             delay = 0,
-            onAnimationEnd = null
+            onAnimationEnd = null,
         )
     }
 
@@ -381,7 +382,7 @@
                 interpolator = null,
                 duration = CHARGE_ANIM_DURATION_PHASE_1,
                 delay = 0,
-                onAnimationEnd = null
+                onAnimationEnd = null,
             )
         }
         setTextStyle(
@@ -391,7 +392,7 @@
             interpolator = null,
             duration = CHARGE_ANIM_DURATION_PHASE_0,
             delay = chargeAnimationDelay.toLong(),
-            onAnimationEnd = startAnimPhase2
+            onAnimationEnd = startAnimPhase2,
         )
     }
 
@@ -404,7 +405,7 @@
             interpolator = null,
             duration = DOZE_ANIM_DURATION,
             delay = 0,
-            onAnimationEnd = null
+            onAnimationEnd = null,
         )
     }
 
@@ -444,7 +445,7 @@
         interpolator: TimeInterpolator?,
         duration: Long,
         delay: Long,
-        onAnimationEnd: Runnable?
+        onAnimationEnd: Runnable?,
     ) {
         textAnimator?.let {
             it.setTextStyle(
@@ -454,7 +455,7 @@
                 duration = duration,
                 interpolator = interpolator,
                 delay = delay,
-                onAnimationEnd = onAnimationEnd
+                onAnimationEnd = onAnimationEnd,
             )
             it.glyphFilter = glyphFilter
         }
@@ -468,7 +469,7 @@
                         duration = duration,
                         interpolator = interpolator,
                         delay = delay,
-                        onAnimationEnd = onAnimationEnd
+                        onAnimationEnd = onAnimationEnd,
                     )
                     textAnimator.glyphFilter = glyphFilter
                 }
@@ -476,6 +477,7 @@
     }
 
     fun refreshFormat() = refreshFormat(DateFormat.is24HourFormat(context))
+
     fun refreshFormat(use24HourFormat: Boolean) {
         Patterns.update(context)
 
@@ -560,18 +562,11 @@
      * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means
      *   it finished moving.
      */
-    fun offsetGlyphsForStepClockAnimation(
-        distance: Float,
-        fraction: Float,
-    ) {
+    fun offsetGlyphsForStepClockAnimation(distance: Float, fraction: Float) {
         for (i in 0 until NUM_DIGITS) {
             val dir = if (isLayoutRtl) -1 else 1
             val digitFraction =
-                getDigitFraction(
-                    digit = i,
-                    isMovingToCenter = distance > 0,
-                    fraction = fraction,
-                )
+                getDigitFraction(digit = i, isMovingToCenter = distance > 0, fraction = fraction)
             val moveAmountForDigit = dir * distance * digitFraction
             glyphOffsets[i] = moveAmountForDigit
 
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt
new file mode 100644
index 0000000..d001ef96
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt
@@ -0,0 +1,448 @@
+/*
+ * 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.shared.clocks
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.content.res.Resources
+import android.graphics.Color
+import android.graphics.Typeface
+import android.graphics.drawable.Drawable
+import android.util.TypedValue
+import com.android.internal.graphics.ColorUtils
+import com.android.internal.graphics.cam.Cam
+import com.android.internal.graphics.cam.CamUtils
+import com.android.internal.policy.SystemBarUtils
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.monet.ColorScheme
+import com.android.systemui.monet.Style as MonetStyle
+import com.android.systemui.monet.TonalPalette
+import java.io.IOException
+import kotlin.math.abs
+
+class AssetLoader
+private constructor(
+    private val pluginCtx: Context,
+    private val sysuiCtx: Context,
+    private val baseDir: String,
+    var colorScheme: ColorScheme?,
+    var seedColor: Int?,
+    var overrideChroma: Float?,
+    val typefaceCache: TypefaceCache,
+    val getThemeSeedColor: (Context) -> Int,
+    messageBuffer: MessageBuffer,
+) {
+    val logger = Logger(messageBuffer, TAG)
+    private val resources =
+        listOf(
+            Pair(pluginCtx.resources, pluginCtx.packageName),
+            Pair(sysuiCtx.resources, sysuiCtx.packageName),
+        )
+
+    constructor(
+        pluginCtx: Context,
+        sysuiCtx: Context,
+        baseDir: String,
+        messageBuffer: MessageBuffer,
+        getThemeSeedColor: ((Context) -> Int)? = null,
+    ) : this(
+        pluginCtx,
+        sysuiCtx,
+        baseDir,
+        colorScheme = null,
+        seedColor = null,
+        overrideChroma = null,
+        typefaceCache =
+            TypefaceCache(messageBuffer) { Typeface.createFromAsset(pluginCtx.assets, it) },
+        getThemeSeedColor = getThemeSeedColor ?: Companion::getThemeSeedColor,
+        messageBuffer = messageBuffer,
+    )
+
+    fun listAssets(path: String): List<String> {
+        return pluginCtx.resources.assets.list("$baseDir$path")?.toList() ?: emptyList()
+    }
+
+    fun tryReadString(resStr: String): String? = tryRead(resStr, ::readString)
+
+    fun readString(resStr: String): String {
+        val resPair = resolveResourceId(resStr)
+        if (resPair == null) {
+            throw IOException("Failed to parse string: $resStr")
+        }
+
+        val (res, id) = resPair
+        return res.getString(id)
+    }
+
+    fun tryReadColor(resStr: String): Int? = tryRead(resStr, ::readColor)
+
+    fun readColor(resStr: String): Int {
+        if (resStr.startsWith("#")) {
+            return Color.parseColor(resStr)
+        }
+
+        val schemeColor = tryParseColorFromScheme(resStr)
+        if (schemeColor != null) {
+            logColor("ColorScheme: $resStr", schemeColor)
+            return checkChroma(schemeColor)
+        }
+
+        val result = resolveColorResourceId(resStr)
+        if (result == null) {
+            throw IOException("Failed to parse color: $resStr")
+        }
+
+        val (res, colorId, targetTone) = result
+        val color = res.getColor(colorId)
+        if (targetTone == null || TonalPalette.SHADE_KEYS.contains(targetTone.toInt())) {
+            logColor("Resources: $resStr", color)
+            return checkChroma(color)
+        } else {
+            val interpolatedColor =
+                ColorStateList.valueOf(color)
+                    .withLStar((1000f - targetTone) / 10f)
+                    .getDefaultColor()
+            logColor("Resources (interpolated tone): $resStr", interpolatedColor)
+            return checkChroma(interpolatedColor)
+        }
+    }
+
+    private fun checkChroma(color: Int): Int {
+        return overrideChroma?.let {
+            val cam = Cam.fromInt(color)
+            val tone = CamUtils.lstarFromInt(color)
+            val result = ColorUtils.CAMToColor(cam.hue, it, tone)
+            logColor("Chroma override", result)
+            result
+        } ?: color
+    }
+
+    private fun tryParseColorFromScheme(resStr: String): Int? {
+        val colorScheme = this.colorScheme
+        if (colorScheme == null) {
+            logger.w("No color scheme available")
+            return null
+        }
+
+        val (packageName, category, name) = parseResourceId(resStr)
+        if (packageName != "android" || category != "color") {
+            logger.w("Failed to parse package from $resStr")
+            return null
+        }
+
+        var parts = name.split('_')
+        if (parts.size != 3) {
+            logger.w("Failed to find palette and shade from $name")
+            return null
+        }
+        val (_, paletteKey, shadeKeyStr) = parts
+
+        val palette =
+            when (paletteKey) {
+                "accent1" -> colorScheme.accent1
+                "accent2" -> colorScheme.accent2
+                "accent3" -> colorScheme.accent3
+                "neutral1" -> colorScheme.neutral1
+                "neutral2" -> colorScheme.neutral2
+                else -> return null
+            }
+
+        if (shadeKeyStr.contains("+") || shadeKeyStr.contains("-")) {
+            val signIndex = shadeKeyStr.indexOfLast { it == '-' || it == '+' }
+            // Use the tone of the seed color if it was set explicitly.
+            var baseTone =
+                if (seedColor != null) colorScheme.seedTone.toFloat()
+                else shadeKeyStr.substring(0, signIndex).toFloatOrNull()
+            val diff = shadeKeyStr.substring(signIndex).toFloatOrNull()
+
+            if (baseTone == null) {
+                logger.w("Failed to parse base tone from $shadeKeyStr")
+                return null
+            }
+
+            if (diff == null) {
+                logger.w("Failed to parse relative tone from $shadeKeyStr")
+                return null
+            }
+            return palette.getAtTone(baseTone + diff)
+        } else {
+            val shadeKey = shadeKeyStr.toIntOrNull()
+            if (shadeKey == null) {
+                logger.w("Failed to parse tone from $shadeKeyStr")
+                return null
+            }
+            return palette.allShadesMapped.get(shadeKey) ?: palette.getAtTone(shadeKey.toFloat())
+        }
+    }
+
+    fun readFontAsset(resStr: String): Typeface = typefaceCache.getTypeface(resStr)
+
+    fun tryReadTextAsset(path: String?): String? = tryRead(path, ::readTextAsset)
+
+    fun readTextAsset(path: String): String {
+        return pluginCtx.resources.assets.open("$baseDir$path").use { stream ->
+            val buffer = ByteArray(stream.available())
+            stream.read(buffer)
+            String(buffer)
+        }
+    }
+
+    fun tryReadDrawableAsset(path: String?): Drawable? = tryRead(path, ::readDrawableAsset)
+
+    fun readDrawableAsset(path: String): Drawable {
+        var result: Drawable?
+
+        if (path.startsWith("@")) {
+            val pair = resolveResourceId(path)
+            if (pair == null) {
+                throw IOException("Failed to parse $path to an id")
+            }
+            val (res, id) = pair
+            result = res.getDrawable(id)
+        } else if (path.endsWith("xml")) {
+            // TODO(b/248609434): Support xml files in assets
+            throw IOException("Cannot load xml files from assets")
+        } else {
+            // Attempt to load as if it's a bitmap and directly loadable
+            result =
+                pluginCtx.resources.assets.open("$baseDir$path").use { stream ->
+                    Drawable.createFromResourceStream(
+                        pluginCtx.resources,
+                        TypedValue(),
+                        stream,
+                        null,
+                    )
+                }
+        }
+
+        return result ?: throw IOException("Failed to load: $baseDir$path")
+    }
+
+    fun parseResourceId(resStr: String): Triple<String?, String, String> {
+        if (!resStr.startsWith("@")) {
+            throw IOException("Invalid resource id: $resStr; Must start with '@'")
+        }
+
+        // Parse out resource string
+        val parts = resStr.drop(1).split('/', ':')
+        return when (parts.size) {
+            2 -> Triple(null, parts[0], parts[1])
+            3 -> Triple(parts[0], parts[1], parts[2])
+            else -> throw IOException("Failed to parse resource string: $resStr")
+        }
+    }
+
+    fun resolveColorResourceId(resStr: String): Triple<Resources, Int, Float?>? {
+        var (packageName, category, name) = parseResourceId(resStr)
+
+        // Convert relative tonal specifiers to standard
+        val relIndex = name.indexOfLast { it == '_' }
+        val isToneRelative = name.contains("-") || name.contains("+")
+        val targetTone =
+            if (packageName != "android") {
+                null
+            } else if (isToneRelative) {
+                val signIndex = name.indexOfLast { it == '-' || it == '+' }
+                val baseTone = name.substring(relIndex + 1, signIndex).toFloatOrNull()
+                var diff = name.substring(signIndex).toFloatOrNull()
+                if (baseTone == null || diff == null) {
+                    logger.w("Failed to parse relative tone from $name")
+                    return null
+                }
+                baseTone + diff
+            } else {
+                val absTone = name.substring(relIndex + 1).toFloatOrNull()
+                if (absTone == null) {
+                    logger.w("Failed to parse absolute tone from $name")
+                    return null
+                }
+                absTone
+            }
+
+        if (
+            targetTone != null &&
+                (isToneRelative || !TonalPalette.SHADE_KEYS.contains(targetTone.toInt()))
+        ) {
+            val closeTone = TonalPalette.SHADE_KEYS.minBy { abs(it - targetTone) }
+            val prevName = name
+            name = name.substring(0, relIndex + 1) + closeTone
+            logger.i("Converted $prevName to $name")
+        }
+
+        val result = resolveResourceId(packageName, category, name)
+        if (result == null) {
+            return null
+        }
+
+        val (res, resId) = result
+        return Triple(res, resId, targetTone)
+    }
+
+    fun resolveResourceId(resStr: String): Pair<Resources, Int>? {
+        val (packageName, category, name) = parseResourceId(resStr)
+        return resolveResourceId(packageName, category, name)
+    }
+
+    fun resolveResourceId(
+        packageName: String?,
+        category: String,
+        name: String,
+    ): Pair<Resources, Int>? {
+        for ((res, ctxPkgName) in resources) {
+            val result = res.getIdentifier(name, category, packageName ?: ctxPkgName)
+            if (result != 0) {
+                return Pair(res, result)
+            }
+        }
+        return null
+    }
+
+    private fun <TArg : Any, TRes : Any> tryRead(arg: TArg?, fn: (TArg) -> TRes): TRes? {
+        try {
+            if (arg == null) {
+                return null
+            }
+            return fn(arg)
+        } catch (ex: IOException) {
+            logger.w("Failed to read $arg", ex)
+            return null
+        }
+    }
+
+    fun assetExists(path: String): Boolean {
+        try {
+            if (path.startsWith("@")) {
+                val pair = resolveResourceId(path)
+                val colorPair = resolveColorResourceId(path)
+                return pair != null || colorPair != null
+            } else {
+                val stream = pluginCtx.resources.assets.open("$baseDir$path")
+                if (stream == null) {
+                    return false
+                }
+
+                stream.close()
+                return true
+            }
+        } catch (ex: IOException) {
+            return false
+        }
+    }
+
+    fun copy(messageBuffer: MessageBuffer? = null): AssetLoader =
+        AssetLoader(
+            pluginCtx,
+            sysuiCtx,
+            baseDir,
+            colorScheme,
+            seedColor,
+            overrideChroma,
+            typefaceCache,
+            getThemeSeedColor,
+            messageBuffer ?: logger.buffer,
+        )
+
+    fun setSeedColor(seedColor: Int?, style: MonetStyle?) {
+        this.seedColor = seedColor
+        refreshColorPalette(style)
+    }
+
+    fun refreshColorPalette(style: MonetStyle?) {
+        val seedColor =
+            this.seedColor ?: getThemeSeedColor(sysuiCtx).also { logColor("Theme Seed Color", it) }
+        this.colorScheme =
+            ColorScheme(
+                seedColor,
+                false, // darkTheme is not used for palette generation
+                style ?: MonetStyle.CLOCK,
+            )
+
+        // Enforce low chroma on output colors if low chroma theme is selected
+        this.overrideChroma = run {
+            val cam = colorScheme?.seed?.let { Cam.fromInt(it) }
+            if (cam != null && cam.chroma < LOW_CHROMA_LIMIT) {
+                return@run cam.chroma * LOW_CHROMA_SCALE
+            }
+            return@run null
+        }
+    }
+
+    fun getClockPaddingStart(): Int {
+        val result = resolveResourceId(null, "dimen", "clock_padding_start")
+        if (result != null) {
+            val (res, id) = result
+            return res.getDimensionPixelSize(id)
+        }
+        return -1
+    }
+
+    fun getStatusBarHeight(): Int {
+        val display = pluginCtx.getDisplayNoVerify()
+        if (display != null) {
+            return SystemBarUtils.getStatusBarHeight(pluginCtx.resources, display.cutout)
+        }
+
+        logger.w("No display available; falling back to android.R.dimen.status_bar_height")
+        val statusBarHeight = resolveResourceId("android", "dimen", "status_bar_height")
+        if (statusBarHeight != null) {
+            val (res, resId) = statusBarHeight
+            return res.getDimensionPixelSize(resId)
+        }
+
+        throw Exception("Could not fetch StatusBarHeight")
+    }
+
+    fun getResourcesId(name: String): Int = getResource("id", name) { _, id -> id }
+
+    fun getDimen(name: String): Int = getResource("dimen", name, Resources::getDimensionPixelSize)
+
+    fun getString(name: String): String = getResource("string", name, Resources::getString)
+
+    private fun <T> getResource(
+        category: String,
+        name: String,
+        getter: (res: Resources, id: Int) -> T,
+    ): T {
+        val result = resolveResourceId(null, category, name)
+        if (result != null) {
+            val (res, id) = result
+            if (id == -1) throw Exception("Cannot find id of $id from $TAG")
+            return getter(res, id)
+        }
+        throw Exception("Cannot find id of $name from $TAG")
+    }
+
+    private fun logColor(name: String, color: Int) {
+        if (DEBUG_COLOR) {
+            val cam = Cam.fromInt(color)
+            val tone = CamUtils.lstarFromInt(color)
+            logger.i("$name -> (hue: ${cam.hue}, chroma: ${cam.chroma}, tone: $tone)")
+        }
+    }
+
+    companion object {
+        private val DEBUG_COLOR = true
+        private val LOW_CHROMA_LIMIT = 15
+        private val LOW_CHROMA_SCALE = 1.5f
+        private val TAG = AssetLoader::class.simpleName!!
+
+        private fun getThemeSeedColor(ctx: Context): Int {
+            return ctx.resources.getColor(android.R.color.system_palette_key_color_primary_light)
+        }
+    }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockAnimation.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockAnimation.kt
new file mode 100644
index 0000000..5a04169
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockAnimation.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.shared.clocks
+
+object ClockAnimation {
+    const val NUM_CLOCK_FONT_ANIMATION_STEPS = 30
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt
new file mode 100644
index 0000000..f5e8432
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt
@@ -0,0 +1,288 @@
+/*
+ * 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.shared.clocks
+
+import android.graphics.Point
+import android.view.animation.Interpolator
+import com.android.app.animation.Interpolators
+import com.android.internal.annotations.Keep
+import com.android.systemui.monet.Style as MonetStyle
+import com.android.systemui.shared.clocks.view.HorizontalAlignment
+import com.android.systemui.shared.clocks.view.VerticalAlignment
+
+/** Data format for a simple asset-defined clock */
+@Keep
+data class ClockDesign(
+    val id: String,
+    val name: String? = null,
+    val description: String? = null,
+    val thumbnail: String? = null,
+    val large: ClockFace? = null,
+    val small: ClockFace? = null,
+    val colorPalette: MonetStyle? = null,
+)
+
+/** Describes a clock using layers */
+@Keep
+data class ClockFace(
+    val layers: List<ClockLayer> = listOf<ClockLayer>(),
+    val layerBounds: LayerBounds = LayerBounds.FIT,
+    val wallpaper: String? = null,
+    val faceLayout: DigitalFaceLayout? = null,
+    val pickerScale: ClockFaceScaleInPicker? = ClockFaceScaleInPicker(1.0f, 1.0f),
+)
+
+@Keep data class ClockFaceScaleInPicker(val scaleX: Float, val scaleY: Float)
+
+/** Base Type for a Clock Layer */
+@Keep
+interface ClockLayer {
+    /** Override of face LayerBounds setting for this layer */
+    val layerBounds: LayerBounds?
+}
+
+/** Clock layer that renders a static asset */
+@Keep
+data class AssetLayer(
+    /** Asset to render in this layer */
+    val asset: AssetReference,
+    override val layerBounds: LayerBounds? = null,
+) : ClockLayer
+
+/** Clock layer that renders the time (or a component of it) using numerals */
+@Keep
+data class DigitalHandLayer(
+    /** See SimpleDateFormat for timespec format info */
+    val timespec: DigitalTimespec,
+    val style: TextStyle,
+    // adoStyle concrete type must match style,
+    // cause styles will transition between style and aodStyle
+    val aodStyle: TextStyle?,
+    val timer: Int? = null,
+    override val layerBounds: LayerBounds? = null,
+    var faceLayout: DigitalFaceLayout? = null,
+    // we pass 12-hour format from json, which will be converted to 24-hour format in codes
+    val dateTimeFormat: String,
+    val alignment: DigitalAlignment?,
+    // ratio of margins to measured size, currently used for handwritten clocks
+    val marginRatio: DigitalMarginRatio? = DigitalMarginRatio(),
+) : ClockLayer
+
+/** Clock layer that renders the time (or a component of it) using numerals */
+@Keep
+data class ComposedDigitalHandLayer(
+    val customizedView: String? = null,
+    /** See SimpleDateFormat for timespec format info */
+    val digitalLayers: List<DigitalHandLayer> = listOf<DigitalHandLayer>(),
+    override val layerBounds: LayerBounds? = null,
+) : ClockLayer
+
+@Keep
+data class DigitalAlignment(
+    val horizontalAlignment: HorizontalAlignment?,
+    val verticalAlignment: VerticalAlignment?,
+)
+
+@Keep
+data class DigitalMarginRatio(
+    val left: Float = 0F,
+    val top: Float = 0F,
+    val right: Float = 0F,
+    val bottom: Float = 0F,
+)
+
+/** Clock layer which renders a component of the time using an analog hand */
+@Keep
+data class AnalogHandLayer(
+    val timespec: AnalogTimespec,
+    val tickMode: AnalogTickMode,
+    val asset: AssetReference,
+    val timer: Int? = null,
+    val clock_pivot: Point = Point(0, 0),
+    val asset_pivot: Point? = null,
+    val length: Float = 1f,
+    override val layerBounds: LayerBounds? = null,
+) : ClockLayer
+
+/** Clock layer which renders the time using an AVD */
+@Keep
+data class AnimatedHandLayer(
+    val timespec: AnalogTimespec,
+    val asset: AssetReference,
+    val timer: Int? = null,
+    override val layerBounds: LayerBounds? = null,
+) : ClockLayer
+
+/** A collection of asset references for use in different device modes */
+@Keep
+data class AssetReference(
+    val light: String,
+    val dark: String,
+    val doze: String? = null,
+    val lightTint: String? = null,
+    val darkTint: String? = null,
+    val dozeTint: String? = null,
+)
+
+/**
+ * Core TextStyling attributes for text clocks. Both color and sizing information can be applied to
+ * either subtype.
+ */
+@Keep
+interface TextStyle {
+    // fontSizeScale is a scale factor applied to the default clock's font size.
+    val fontSizeScale: Float?
+}
+
+/**
+ * This specifies a font and styling parameters for that font. This is rendered using a text view
+ * and the text animation classes used by the default clock. To ensure default value take effects,
+ * all parameters MUST have a default value
+ */
+@Keep
+data class FontTextStyle(
+    // Font to load and use in the TextView
+    val fontFamily: String? = null,
+    val lineHeight: Float? = null,
+    val borderWidth: String? = null,
+    // ratio of borderWidth / fontSize
+    val borderWidthScale: Float? = null,
+    // A color literal like `#FF00FF` or a color resource like `@android:color/system_accent1_100`
+    val fillColorLight: String? = null,
+    // A color literal like `#FF00FF` or a color resource like `@android:color/system_accent1_100`
+    val fillColorDark: String? = null,
+    override val fontSizeScale: Float? = null,
+    /**
+     * use `wdth` for width, `wght` for weight, 'opsz' for optical size single quote for tag name,
+     * and no quote for value separate different axis with `,` e.g. "'wght' 1000, 'wdth' 108, 'opsz'
+     * 90"
+     */
+    var fontVariation: String? = null,
+    // used when alternate in one font file is needed
+    var fontFeatureSettings: String? = null,
+    val renderType: RenderType = RenderType.STROKE_TEXT,
+    val outlineColor: String? = null,
+    val transitionDuration: Long = -1L,
+    val transitionInterpolator: InterpolatorEnum? = null,
+) : TextStyle
+
+/**
+ * As an alternative to using a font, we can instead render a digital clock using a set of drawables
+ * for each numeral, and optionally a colon. These drawables will be rendered directly after sizing
+ * and placing them. This may be easier than generating a font file in some cases, and is provided
+ * for ease of use. Unlike fonts, these are not localizable to other numeric systems (like Burmese).
+ */
+@Keep
+data class LottieTextStyle(
+    val numbers: List<String> = listOf(),
+    // Spacing between numbers, dimension string
+    val spacing: String = "0dp",
+    // Colon drawable may be omitted if unused in format spec
+    val colon: String? = null,
+    // key is keypath name to get strokes from lottie, value is the color name to query color in
+    // palette, e.g. @android:color/system_accent1_100
+    val fillColorLightMap: Map<String, String>? = null,
+    val fillColorDarkMap: Map<String, String>? = null,
+    override val fontSizeScale: Float? = null,
+    val paddingVertical: String = "0dp",
+    val paddingHorizontal: String = "0dp",
+) : TextStyle
+
+/** Layer sizing mode for the clockface or layer */
+enum class LayerBounds {
+    /**
+     * Sized so the larger dimension matches the allocated space. This results in some of the
+     * allocated space being unused.
+     */
+    FIT,
+
+    /**
+     * Sized so the smaller dimension matches the allocated space. This will clip some content to
+     * the edges of the space.
+     */
+    FILL,
+
+    /** Fills the allocated space exactly by stretching the layer */
+    STRETCH,
+}
+
+/** Ticking mode for analog hands. */
+enum class AnalogTickMode {
+    SWEEP,
+    TICK,
+}
+
+/** Timspec options for Analog Hands. Named for tick interval. */
+enum class AnalogTimespec {
+    SECONDS,
+    MINUTES,
+    HOURS,
+    HOURS_OF_DAY,
+    DAY_OF_WEEK,
+    DAY_OF_MONTH,
+    DAY_OF_YEAR,
+    WEEK,
+    MONTH,
+    TIMER,
+}
+
+enum class DigitalTimespec {
+    TIME_FULL_FORMAT,
+    DIGIT_PAIR,
+    FIRST_DIGIT,
+    SECOND_DIGIT,
+    DATE_FORMAT,
+}
+
+enum class DigitalFaceLayout {
+    // can only use HH_PAIR, MM_PAIR from DigitalTimespec
+    TWO_PAIRS_VERTICAL,
+    TWO_PAIRS_HORIZONTAL,
+    // can only use HOUR_FIRST_DIGIT, HOUR_SECOND_DIGIT, MINUTE_FIRST_DIGIT, MINUTE_SECOND_DIGIT
+    // from DigitalTimespec, used for tabular layout when the font doesn't support tnum
+    FOUR_DIGITS_ALIGN_CENTER,
+    FOUR_DIGITS_HORIZONTAL,
+}
+
+enum class RenderType {
+    CHANGE_WEIGHT,
+    HOLLOW_TEXT,
+    STROKE_TEXT,
+    OUTER_OUTLINE_TEXT,
+}
+
+enum class InterpolatorEnum(factory: () -> Interpolator) {
+    STANDARD({ Interpolators.STANDARD }),
+    EMPHASIZED({ Interpolators.EMPHASIZED });
+
+    val interpolator: Interpolator by lazy(factory)
+}
+
+fun generateDigitalLayerIdString(layer: DigitalHandLayer): String {
+    return if (
+        layer.timespec == DigitalTimespec.TIME_FULL_FORMAT ||
+            layer.timespec == DigitalTimespec.DATE_FORMAT
+    ) {
+        layer.timespec.toString()
+    } else {
+        if ("h" in layer.dateTimeFormat) {
+            "HOUR" + "_" + layer.timespec.toString()
+        } else {
+            "MINUTE" + "_" + layer.timespec.toString()
+        }
+    }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
index 954155d..9da3022 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
@@ -65,7 +65,7 @@
 private fun <TKey : Any, TVal : Any> ConcurrentHashMap<TKey, TVal>.concurrentGetOrPut(
     key: TKey,
     value: TVal,
-    onNew: (TVal) -> Unit
+    onNew: (TVal) -> Unit,
 ): TVal {
     val result = this.putIfAbsent(key, value)
     if (result == null) {
@@ -110,7 +110,7 @@
                 selfChange: Boolean,
                 uris: Collection<Uri>,
                 flags: Int,
-                userId: Int
+                userId: Int,
             ) {
                 scope.launch(bgDispatcher) { querySettings() }
             }
@@ -180,7 +180,7 @@
             override fun onPluginLoaded(
                 plugin: ClockProviderPlugin,
                 pluginContext: Context,
-                manager: PluginLifecycleManager<ClockProviderPlugin>
+                manager: PluginLifecycleManager<ClockProviderPlugin>,
             ) {
                 plugin.initialize(clockBuffers)
 
@@ -218,7 +218,7 @@
 
             override fun onPluginUnloaded(
                 plugin: ClockProviderPlugin,
-                manager: PluginLifecycleManager<ClockProviderPlugin>
+                manager: PluginLifecycleManager<ClockProviderPlugin>,
             ) {
                 for (clock in plugin.getClocks()) {
                     val id = clock.clockId
@@ -290,12 +290,12 @@
                         Settings.Secure.getStringForUser(
                             context.contentResolver,
                             Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
-                            ActivityManager.getCurrentUser()
+                            ActivityManager.getCurrentUser(),
                         )
                     } else {
                         Settings.Secure.getString(
                             context.contentResolver,
-                            Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE
+                            Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
                         )
                     }
 
@@ -320,13 +320,13 @@
                     context.contentResolver,
                     Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
                     json,
-                    ActivityManager.getCurrentUser()
+                    ActivityManager.getCurrentUser(),
                 )
             } else {
                 Settings.Secure.putString(
                     context.contentResolver,
                     Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
-                    json
+                    json,
                 )
             }
         } catch (ex: Exception) {
@@ -418,7 +418,7 @@
         pluginManager.addPluginListener(
             pluginListener,
             ClockProviderPlugin::class.java,
-            /*allowMultiple=*/ true
+            /*allowMultiple=*/ true,
         )
 
         scope.launch(bgDispatcher) { querySettings() }
@@ -427,7 +427,7 @@
                 Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
                 /*notifyForDescendants=*/ false,
                 settingObserver,
-                UserHandle.USER_ALL
+                UserHandle.USER_ALL,
             )
 
             ActivityManager.getService().registerUserSwitchObserver(userSwitchObserver, TAG)
@@ -435,7 +435,7 @@
             context.contentResolver.registerContentObserver(
                 Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
                 /*notifyForDescendants=*/ false,
-                settingObserver
+                settingObserver,
             )
         }
     }
@@ -504,7 +504,7 @@
         val isCurrent = currentClockId == info.metadata.clockId
         logger.log(
             if (isCurrent) LogLevel.INFO else LogLevel.DEBUG,
-            { "Connected $str1 @$str2" + if (bool1) " (Current Clock)" else "" }
+            { "Connected $str1 @$str2" + if (bool1) " (Current Clock)" else "" },
         ) {
             str1 = info.metadata.clockId
             str2 = info.manager.toString()
@@ -516,7 +516,7 @@
         val isCurrent = currentClockId == info.metadata.clockId
         logger.log(
             if (isCurrent) LogLevel.INFO else LogLevel.DEBUG,
-            { "Loaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" }
+            { "Loaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" },
         ) {
             str1 = info.metadata.clockId
             str2 = info.manager.toString()
@@ -532,7 +532,7 @@
         val isCurrent = currentClockId == info.metadata.clockId
         logger.log(
             if (isCurrent) LogLevel.WARNING else LogLevel.DEBUG,
-            { "Unloaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" }
+            { "Unloaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" },
         ) {
             str1 = info.metadata.clockId
             str2 = info.manager.toString()
@@ -548,7 +548,7 @@
         val isCurrent = currentClockId == info.metadata.clockId
         logger.log(
             if (isCurrent) LogLevel.INFO else LogLevel.DEBUG,
-            { "Disconnected $str1 @$str2" + if (bool1) " (Current Clock)" else "" }
+            { "Disconnected $str1 @$str2" + if (bool1) " (Current Clock)" else "" },
         ) {
             str1 = info.metadata.clockId
             str2 = info.manager.toString()
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index 4802e34..07191c6 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -34,7 +34,7 @@
     val layoutInflater: LayoutInflater,
     val resources: Resources,
     val hasStepClockAnimation: Boolean = false,
-    val migratedClocks: Boolean = false
+    val migratedClocks: Boolean = false,
 ) : ClockProvider {
     private var messageBuffers: ClockMessageBuffers? = null
 
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt
new file mode 100644
index 0000000..3869706
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.shared.clocks
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.TimeInterpolator
+import android.animation.ValueAnimator
+import android.graphics.Point
+
+class DigitTranslateAnimator(val updateCallback: () -> Unit) {
+    val DEFAULT_ANIMATION_DURATION = 500L
+    val updatedTranslate = Point(0, 0)
+
+    val baseTranslation = Point(0, 0)
+    var targetTranslation: Point? = null
+    val bounceAnimator: ValueAnimator =
+        ValueAnimator.ofFloat(1f).apply {
+            duration = DEFAULT_ANIMATION_DURATION
+            addUpdateListener {
+                updateTranslation(it.animatedFraction, updatedTranslate)
+                updateCallback()
+            }
+            addListener(
+                object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animation: Animator) {
+                        rebase()
+                    }
+
+                    override fun onAnimationCancel(animation: Animator) {
+                        rebase()
+                    }
+                }
+            )
+        }
+
+    fun rebase() {
+        baseTranslation.x = updatedTranslate.x
+        baseTranslation.y = updatedTranslate.y
+    }
+
+    fun animatePosition(
+        animate: Boolean = true,
+        delay: Long = 0,
+        duration: Long = -1L,
+        interpolator: TimeInterpolator? = null,
+        targetTranslation: Point? = null,
+        onAnimationEnd: Runnable? = null,
+    ) {
+        this.targetTranslation = targetTranslation ?: Point(0, 0)
+        if (animate) {
+            bounceAnimator.cancel()
+            bounceAnimator.startDelay = delay
+            bounceAnimator.duration =
+                if (duration == -1L) {
+                    DEFAULT_ANIMATION_DURATION
+                } else {
+                    duration
+                }
+            interpolator?.let { bounceAnimator.interpolator = it }
+            if (onAnimationEnd != null) {
+                val listener =
+                    object : AnimatorListenerAdapter() {
+                        override fun onAnimationEnd(animation: Animator) {
+                            onAnimationEnd.run()
+                            bounceAnimator.removeListener(this)
+                        }
+
+                        override fun onAnimationCancel(animation: Animator) {
+                            bounceAnimator.removeListener(this)
+                        }
+                    }
+                bounceAnimator.addListener(listener)
+            }
+            bounceAnimator.start()
+        } else {
+            // No animation is requested, thus set base and target state to the same state.
+            updateTranslation(1F, updatedTranslate)
+            rebase()
+            updateCallback()
+        }
+    }
+
+    fun updateTranslation(progress: Float, outPoint: Point) {
+        outPoint.x =
+            (baseTranslation.x + progress * (targetTranslation!!.x - baseTranslation.x)).toInt()
+        outPoint.y =
+            (baseTranslation.y + progress * (targetTranslation!!.y - baseTranslation.y)).toInt()
+    }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DimensionParser.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DimensionParser.kt
new file mode 100644
index 0000000..2be6c65
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DimensionParser.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.clocks
+
+import android.content.Context
+import android.util.TypedValue
+import java.util.regex.Pattern
+
+class DimensionParser(private val ctx: Context) {
+    fun convert(dimension: String?): Float? {
+        if (dimension == null) {
+            return null
+        }
+        return convert(dimension)
+    }
+
+    fun convert(dimension: String): Float {
+        val metrics = ctx.resources.displayMetrics
+        val (value, unit) = parse(dimension)
+        return TypedValue.applyDimension(unit, value, metrics)
+    }
+
+    fun parse(dimension: String): Pair<Float, Int> {
+        val matcher = parserPattern.matcher(dimension)
+        if (!matcher.matches()) {
+            throw NumberFormatException("Failed to parse '$dimension'")
+        }
+
+        val value =
+            matcher.group(1)?.toFloat() ?: throw NumberFormatException("Bad value in '$dimension'")
+        val unit =
+            dimensionMap.get(matcher.group(3) ?: "")
+                ?: throw NumberFormatException("Bad unit in '$dimension'")
+        return Pair(value, unit)
+    }
+
+    private companion object {
+        val parserPattern = Pattern.compile("(\\d+(\\.\\d+)?)([a-z]+)")
+        val dimensionMap =
+            mapOf(
+                "dp" to TypedValue.COMPLEX_UNIT_DIP,
+                "dip" to TypedValue.COMPLEX_UNIT_DIP,
+                "sp" to TypedValue.COMPLEX_UNIT_SP,
+                "px" to TypedValue.COMPLEX_UNIT_PX,
+                "pt" to TypedValue.COMPLEX_UNIT_PT,
+                "mm" to TypedValue.COMPLEX_UNIT_MM,
+                "in" to TypedValue.COMPLEX_UNIT_IN,
+            )
+    }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LogUtil.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LogUtil.kt
new file mode 100644
index 0000000..34cb4ef
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LogUtil.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.shared.clocks
+
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogcatOnlyMessageBuffer
+import com.android.systemui.log.core.Logger
+
+object LogUtil {
+    // Used when MessageBuffers are not provided by the host application
+    val DEFAULT_MESSAGE_BUFFER = LogcatOnlyMessageBuffer(LogLevel.INFO)
+
+    // Only intended for use during initialization steps where the correct logger doesn't exist yet
+    val FALLBACK_INIT_LOGGER = Logger(LogcatOnlyMessageBuffer(LogLevel.ERROR), "CLOCK_INIT")
+
+    // Debug is primarially used for tests, but can also be used for tracking down hard issues.
+    val DEBUG_MESSAGE_BUFFER = LogcatOnlyMessageBuffer(LogLevel.DEBUG)
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TypefaceCache.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TypefaceCache.kt
new file mode 100644
index 0000000..f5a9375
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TypefaceCache.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.shared.clocks
+
+import android.graphics.Typeface
+import com.android.systemui.animation.TypefaceVariantCache
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.core.MessageBuffer
+import java.lang.ref.ReferenceQueue
+import java.lang.ref.WeakReference
+
+class TypefaceCache(messageBuffer: MessageBuffer, val typefaceFactory: (String) -> Typeface) {
+    private val logger = Logger(messageBuffer, this::class.simpleName!!)
+
+    private data class CacheKey(val res: String, val fvar: String?)
+
+    private inner class WeakTypefaceRef(val key: CacheKey, typeface: Typeface) :
+        WeakReference<Typeface>(typeface, queue)
+
+    private var totalHits = 0
+
+    private var totalMisses = 0
+
+    private var totalEvictions = 0
+
+    // We use a map of WeakRefs here instead of an LruCache. This prevents needing to resize the
+    // cache depending on the number of distinct fonts used by a clock, as different clocks have
+    // different numbers of simultaneously loaded and configured fonts. Because our clocks tend to
+    // initialize a number of parallel views and animators, our usages of Typefaces overlap. As a
+    // result, once a typeface is no longer being used, it is unlikely to be recreated immediately.
+    private val cache = mutableMapOf<CacheKey, WeakTypefaceRef>()
+    private val queue = ReferenceQueue<Typeface>()
+
+    fun getTypeface(res: String): Typeface {
+        checkQueue()
+        val key = CacheKey(res, null)
+        cache.get(key)?.get()?.let {
+            logHit(key)
+            return it
+        }
+
+        logMiss(key)
+        val result = typefaceFactory(res)
+        cache.put(key, WeakTypefaceRef(key, result))
+        return result
+    }
+
+    fun getVariantCache(res: String): TypefaceVariantCache {
+        val baseTypeface = getTypeface(res)
+        return object : TypefaceVariantCache {
+            override fun getTypefaceForVariant(fvar: String?): Typeface? {
+                checkQueue()
+                val key = CacheKey(res, fvar)
+                cache.get(key)?.get()?.let {
+                    logHit(key)
+                    return it
+                }
+
+                logMiss(key)
+                return TypefaceVariantCache.createVariantTypeface(baseTypeface, fvar).also {
+                    cache.put(key, WeakTypefaceRef(key, it))
+                }
+            }
+        }
+    }
+
+    private fun logHit(key: CacheKey) {
+        totalHits++
+        if (DEBUG_HITS)
+            logger.i({ "HIT: $str1; Total: $int1" }) {
+                str1 = key.toString()
+                int1 = totalHits
+            }
+    }
+
+    private fun logMiss(key: CacheKey) {
+        totalMisses++
+        logger.w({ "MISS: $str1; Total: $int1" }) {
+            str1 = key.toString()
+            int1 = totalMisses
+        }
+    }
+
+    private fun logEviction(key: CacheKey) {
+        totalEvictions++
+        logger.i({ "EVICTED: $str1; Total: $int1" }) {
+            str1 = key.toString()
+            int1 = totalEvictions
+        }
+    }
+
+    private fun checkQueue() =
+        generateSequence { queue.poll() }
+            .filterIsInstance<WeakTypefaceRef>()
+            .forEach {
+                logEviction(it.key)
+                cache.remove(it.key)
+            }
+
+    companion object {
+        private val DEBUG_HITS = false
+    }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt
new file mode 100644
index 0000000..eb72346
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt
@@ -0,0 +1,180 @@
+/*
+ * 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.shared.clocks.view
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Point
+import android.view.View
+import android.widget.FrameLayout
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.plugins.clocks.AlarmData
+import com.android.systemui.plugins.clocks.WeatherData
+import com.android.systemui.plugins.clocks.ZenData
+import com.android.systemui.shared.clocks.AssetLoader
+import com.android.systemui.shared.clocks.LogUtil
+import java.util.Locale
+
+abstract class DigitalClockFaceView(ctx: Context, messageBuffer: MessageBuffer) : FrameLayout(ctx) {
+    protected val logger = Logger(messageBuffer, this::class.simpleName!!)
+        get() = field ?: LogUtil.FALLBACK_INIT_LOGGER
+
+    abstract var digitalClockTextViewMap: MutableMap<Int, SimpleDigitalClockTextView>
+
+    @VisibleForTesting
+    var isAnimationEnabled = true
+        set(value) {
+            field = value
+            digitalClockTextViewMap.forEach { _, view -> view.isAnimationEnabled = value }
+        }
+
+    var dozeFraction: Float = 0F
+        set(value) {
+            field = value
+            digitalClockTextViewMap.forEach { _, view -> view.dozeFraction = field }
+        }
+
+    val dozeControlState = DozeControlState()
+
+    var isReactiveTouchInteractionEnabled = false
+        set(value) {
+            field = value
+        }
+
+    open val text: String?
+        get() = null
+
+    open fun refreshTime() = logger.d("refreshTime()")
+
+    override fun invalidate() {
+        logger.d("invalidate()")
+        super.invalidate()
+    }
+
+    override fun requestLayout() {
+        logger.d("requestLayout()")
+        super.requestLayout()
+    }
+
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        logger.d("onMeasure()")
+        calculateSize(widthMeasureSpec, heightMeasureSpec)?.let { setMeasuredDimension(it.x, it.y) }
+            ?: run { super.onMeasure(widthMeasureSpec, heightMeasureSpec) }
+        calculateLeftTopPosition()
+        dozeControlState.animateReady = true
+    }
+
+    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+        logger.d("onLayout()")
+        super.onLayout(changed, left, top, right, bottom)
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        text?.let { logger.d({ "onDraw($str1)" }) { str1 = it } } ?: run { logger.d("onDraw()") }
+        super.onDraw(canvas)
+    }
+
+    /*
+     * Called in onMeasure to generate width/height overrides to the normal measuring logic. A null
+     * result causes the normal view measuring logic to execute.
+     */
+    protected open fun calculateSize(widthMeasureSpec: Int, heightMeasureSpec: Int): Point? = null
+
+    protected open fun calculateLeftTopPosition() {}
+
+    override fun addView(child: View?) {
+        if (child == null) return
+        logger.d({ "addView($str1 @$int1)" }) {
+            str1 = child::class.simpleName!!
+            int1 = child.id
+        }
+        super.addView(child)
+        if (child is SimpleDigitalClockTextView) {
+            digitalClockTextViewMap[child.id] = child
+        }
+        child.setWillNotDraw(true)
+    }
+
+    open fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
+        digitalClockTextViewMap.forEach { _, view -> view.animateDoze(isDozing, isAnimated) }
+    }
+
+    open fun animateCharge() {
+        digitalClockTextViewMap.forEach { _, view -> view.animateCharge() }
+    }
+
+    open fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {}
+
+    fun updateColors(assets: AssetLoader, isRegionDark: Boolean) {
+        digitalClockTextViewMap.forEach { _, view -> view.updateColors(assets, isRegionDark) }
+        invalidate()
+    }
+
+    fun onFontSettingChanged(fontSizePx: Float) {
+        digitalClockTextViewMap.forEach { _, view -> view.applyTextSize(fontSizePx) }
+    }
+
+    open val hasCustomWeatherDataDisplay
+        get() = false
+
+    open val hasCustomPositionUpdatedAnimation
+        get() = false
+
+    /** True if it's large weather clock, will use weatherBlueprint in compose */
+    open val useCustomClockScene
+        get() = false
+
+    // TODO: implement ClockEventUnion?
+    open fun onLocaleChanged(locale: Locale) {}
+
+    open fun onWeatherDataChanged(data: WeatherData) {}
+
+    open fun onAlarmDataChanged(data: AlarmData) {}
+
+    open fun onZenDataChanged(data: ZenData) {}
+
+    open fun onPickerCarouselSwiping(swipingFraction: Float) {}
+
+    open fun isAlignedWithScreen(): Boolean = false
+
+    /**
+     * animateDoze needs correct translate value, which is calculated in onMeasure so we need to
+     * delay this animation when we get correct values
+     */
+    class DozeControlState {
+        var animateDoze: () -> Unit = {}
+            set(value) {
+                if (animateReady) {
+                    value()
+                    field = {}
+                } else {
+                    field = value
+                }
+            }
+
+        var animateReady = false
+            set(value) {
+                if (value) {
+                    animateDoze()
+                    animateDoze = {}
+                }
+                field = value
+            }
+    }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
new file mode 100644
index 0000000..c29c8da
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
@@ -0,0 +1,260 @@
+/*
+ * 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.shared.clocks.view
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Point
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.widget.RelativeLayout
+import com.android.app.animation.Interpolators
+import com.android.systemui.customization.R
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.shared.clocks.AssetLoader
+import com.android.systemui.shared.clocks.DigitTranslateAnimator
+import com.android.systemui.shared.clocks.FontTextStyle
+import kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.min
+
+fun clamp(value: Float, minVal: Float, maxVal: Float): Float = max(min(value, maxVal), minVal)
+
+class FlexClockView(context: Context, val assetLoader: AssetLoader, messageBuffer: MessageBuffer) :
+    DigitalClockFaceView(context, messageBuffer) {
+    override var digitalClockTextViewMap = mutableMapOf<Int, SimpleDigitalClockTextView>()
+    val digitLeftTopMap = mutableMapOf<Int, Point>()
+    var maxSingleDigitHeight = -1
+    var maxSingleDigitWidth = -1
+    val lockscreenTranslate = Point(0, 0)
+    val aodTranslate = Point(0, 0)
+
+    init {
+        setWillNotDraw(false)
+        layoutParams =
+            RelativeLayout.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT,
+            )
+    }
+
+    private var prevX = 0f
+    private var prevY = 0f
+    private var isDown = false
+
+    // TODO(b/340253296): Genericize; json spec
+    private var wght = 603f
+    private var wdth = 100f
+
+    // TODO(b/340253296): Json spec
+    private val MAX_WGHT = 950f
+    private val MIN_WGHT = 50f
+    private val WGHT_SCALE = 0.5f
+
+    private val MAX_WDTH = 150f
+    private val MIN_WDTH = 0f
+    private val WDTH_SCALE = 0.2f
+
+    override fun onTouchEvent(evt: MotionEvent): Boolean {
+        // TODO(b/340253296): implement on DigitalClockFaceView?
+        if (!isReactiveTouchInteractionEnabled) {
+            return super.onTouchEvent(evt)
+        }
+
+        when (evt.action) {
+            MotionEvent.ACTION_DOWN -> {
+                isDown = true
+                prevX = evt.x
+                prevY = evt.y
+                return true
+            }
+
+            MotionEvent.ACTION_MOVE -> {
+                if (!isDown) {
+                    return super.onTouchEvent(evt)
+                }
+
+                wdth = clamp(wdth + (evt.x - prevX) * WDTH_SCALE, MIN_WDTH, MAX_WDTH)
+                wght = clamp(wght + (evt.y - prevY) * WGHT_SCALE, MIN_WGHT, MAX_WGHT)
+                prevX = evt.x
+                prevY = evt.y
+
+                // TODO(b/340253296): Genericize; json spec
+                val fvar = "'wght' $wght, 'wdth' $wdth, 'opsz' 144, 'ROND' 100"
+                digitalClockTextViewMap.forEach { (_, view) ->
+                    val textStyle = view.textStyle as FontTextStyle
+                    textStyle.fontVariation = fvar
+                    view.applyStyles(assetLoader, textStyle, view.aodStyle)
+                }
+
+                requestLayout()
+                invalidate()
+                return true
+            }
+
+            MotionEvent.ACTION_UP -> {
+                isDown = false
+                return true
+            }
+        }
+
+        return super.onTouchEvent(evt)
+    }
+
+    override fun addView(child: View?) {
+        super.addView(child)
+        (child as SimpleDigitalClockTextView).digitTranslateAnimator =
+            DigitTranslateAnimator(::invalidate)
+    }
+
+    protected override fun calculateSize(widthMeasureSpec: Int, heightMeasureSpec: Int): Point {
+        digitalClockTextViewMap.forEach { (_, textView) ->
+            textView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
+        }
+        val textView = digitalClockTextViewMap[R.id.HOUR_FIRST_DIGIT]!!
+        maxSingleDigitHeight = textView.measuredHeight
+        maxSingleDigitWidth = textView.measuredWidth
+        aodTranslate.x = -(maxSingleDigitWidth * AOD_HORIZONTAL_TRANSLATE_RATIO).toInt()
+        aodTranslate.y = (maxSingleDigitHeight * AOD_VERTICAL_TRANSLATE_RATIO).toInt()
+        return Point(
+            ((maxSingleDigitWidth + abs(aodTranslate.x)) * 2),
+            ((maxSingleDigitHeight + abs(aodTranslate.y)) * 2),
+        )
+    }
+
+    protected override fun calculateLeftTopPosition() {
+        digitLeftTopMap[R.id.HOUR_FIRST_DIGIT] = Point(0, 0)
+        digitLeftTopMap[R.id.HOUR_SECOND_DIGIT] = Point(maxSingleDigitWidth, 0)
+        digitLeftTopMap[R.id.MINUTE_FIRST_DIGIT] = Point(0, maxSingleDigitHeight)
+        digitLeftTopMap[R.id.MINUTE_SECOND_DIGIT] = Point(maxSingleDigitWidth, maxSingleDigitHeight)
+        digitLeftTopMap.forEach { _, point ->
+            point.x += abs(aodTranslate.x)
+            point.y += abs(aodTranslate.y)
+        }
+    }
+
+    override fun refreshTime() {
+        super.refreshTime()
+        digitalClockTextViewMap.forEach { (_, textView) -> textView.refreshText() }
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        super.onDraw(canvas)
+        digitalClockTextViewMap.forEach { (id, _) ->
+            val textView = digitalClockTextViewMap[id]!!
+            canvas.translate(digitLeftTopMap[id]!!.x.toFloat(), digitLeftTopMap[id]!!.y.toFloat())
+            textView.draw(canvas)
+            canvas.translate(-digitLeftTopMap[id]!!.x.toFloat(), -digitLeftTopMap[id]!!.y.toFloat())
+        }
+    }
+
+    override fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
+        dozeControlState.animateDoze = {
+            super.animateDoze(isDozing, isAnimated)
+            if (maxSingleDigitHeight == -1) {
+                measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
+            }
+            digitalClockTextViewMap.forEach { (id, textView) ->
+                textView.digitTranslateAnimator?.let {
+                    if (!isDozing) {
+                        it.animatePosition(
+                            animate = isAnimated && isAnimationEnabled,
+                            interpolator = Interpolators.EMPHASIZED,
+                            duration = AOD_TRANSITION_DURATION,
+                            targetTranslation =
+                                updateDirectionalTargetTranslate(id, lockscreenTranslate),
+                        )
+                    } else {
+                        it.animatePosition(
+                            animate = isAnimated && isAnimationEnabled,
+                            interpolator = Interpolators.EMPHASIZED,
+                            duration = AOD_TRANSITION_DURATION,
+                            onAnimationEnd = null,
+                            targetTranslation = updateDirectionalTargetTranslate(id, aodTranslate),
+                        )
+                    }
+                }
+            }
+        }
+    }
+
+    override fun animateCharge() {
+        super.animateCharge()
+        digitalClockTextViewMap.forEach { (id, textView) ->
+            textView.digitTranslateAnimator?.let {
+                it.animatePosition(
+                    animate = isAnimationEnabled,
+                    interpolator = Interpolators.EMPHASIZED,
+                    duration = CHARGING_TRANSITION_DURATION,
+                    onAnimationEnd = {
+                        it.animatePosition(
+                            animate = isAnimationEnabled,
+                            interpolator = Interpolators.EMPHASIZED,
+                            duration = CHARGING_TRANSITION_DURATION,
+                            targetTranslation =
+                                updateDirectionalTargetTranslate(
+                                    id,
+                                    if (dozeFraction == 1F) aodTranslate else lockscreenTranslate,
+                                ),
+                        )
+                    },
+                    targetTranslation =
+                        updateDirectionalTargetTranslate(
+                            id,
+                            if (dozeFraction == 1F) lockscreenTranslate else aodTranslate,
+                        ),
+                )
+            }
+        }
+    }
+
+    companion object {
+        val AOD_TRANSITION_DURATION = 750L
+        val CHARGING_TRANSITION_DURATION = 300L
+
+        val AOD_HORIZONTAL_TRANSLATE_RATIO = 0.15F
+        val AOD_VERTICAL_TRANSLATE_RATIO = 0.075F
+
+        // Use the sign of targetTranslation to control the direction of digit translation
+        fun updateDirectionalTargetTranslate(id: Int, targetTranslation: Point): Point {
+            val outPoint = Point(targetTranslation)
+            when (id) {
+                R.id.HOUR_FIRST_DIGIT -> {
+                    outPoint.x *= -1
+                    outPoint.y *= -1
+                }
+
+                R.id.HOUR_SECOND_DIGIT -> {
+                    outPoint.x *= 1
+                    outPoint.y *= -1
+                }
+
+                R.id.MINUTE_FIRST_DIGIT -> {
+                    outPoint.x *= -1
+                    outPoint.y *= 1
+                }
+
+                R.id.MINUTE_SECOND_DIGIT -> {
+                    outPoint.x *= 1
+                    outPoint.y *= 1
+                }
+            }
+            return outPoint
+        }
+    }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
new file mode 100644
index 0000000..74617b1
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
@@ -0,0 +1,654 @@
+/*
+ * 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.shared.clocks.view
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Point
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import android.graphics.Rect
+import android.text.Layout
+import android.text.TextPaint
+import android.util.AttributeSet
+import android.util.Log
+import android.util.MathUtils
+import android.util.TypedValue
+import android.view.View.MeasureSpec.AT_MOST
+import android.view.View.MeasureSpec.EXACTLY
+import android.view.animation.Interpolator
+import android.widget.TextView
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.animation.TextAnimator
+import com.android.systemui.animation.TypefaceVariantCache
+import com.android.systemui.customization.R
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.shared.clocks.AssetLoader
+import com.android.systemui.shared.clocks.ClockAnimation
+import com.android.systemui.shared.clocks.DigitTranslateAnimator
+import com.android.systemui.shared.clocks.DimensionParser
+import com.android.systemui.shared.clocks.FontTextStyle
+import com.android.systemui.shared.clocks.LogUtil
+import com.android.systemui.shared.clocks.RenderType
+import com.android.systemui.shared.clocks.TextStyle
+import java.lang.Thread
+import kotlin.math.ceil
+import kotlin.math.max
+import kotlin.math.min
+
+private val TAG = SimpleDigitalClockTextView::class.simpleName!!
+
+@SuppressLint("AppCompatCustomView")
+open class SimpleDigitalClockTextView(
+    ctx: Context,
+    messageBuffer: MessageBuffer,
+    attrs: AttributeSet? = null,
+) : TextView(ctx, attrs), SimpleDigitalClockView {
+    val lockScreenPaint = TextPaint()
+    override lateinit var textStyle: FontTextStyle
+    lateinit var aodStyle: FontTextStyle
+    private val parser = DimensionParser(ctx)
+    var maxSingleDigitHeight = -1
+    var maxSingleDigitWidth = -1
+    var digitTranslateAnimator: DigitTranslateAnimator? = null
+    var aodFontSizePx: Float = -1F
+    var isVertical: Boolean = false
+
+    // Store the font size when there's no height constraint as a reference when adjusting font size
+    private var lastUnconstrainedTextSize: Float = Float.MAX_VALUE
+    // Calculated by height of styled text view / text size
+    // Used as a factor to calculate a smaller font size when text height is constrained
+    @VisibleForTesting var fontSizeAdjustFactor = 1F
+
+    private val initThread = Thread.currentThread()
+
+    // textBounds is the size of text in LS, which only measures current text in lockscreen style
+    var textBounds = Rect()
+    // prevTextBounds and targetTextBounds are to deal with dozing animation between LS and AOD
+    // especially for the textView which has different bounds during the animation
+    // prevTextBounds holds the state we are transitioning from
+    private val prevTextBounds = Rect()
+    // targetTextBounds holds the state we are interpolating to
+    private val targetTextBounds = Rect()
+    protected val logger = Logger(messageBuffer, this::class.simpleName!!)
+        get() = field ?: LogUtil.FALLBACK_INIT_LOGGER
+
+    private var aodDozingInterpolator: Interpolator? = null
+
+    @VisibleForTesting lateinit var textAnimator: TextAnimator
+    @VisibleForTesting var outlineAnimator: TextAnimator? = null
+    // used for hollow style for AOD version
+    // because stroke style for some fonts have some unwanted inner strokes
+    // we want to draw this layer on top to oclude them
+    @VisibleForTesting var innerAnimator: TextAnimator? = null
+
+    lateinit var typefaceCache: TypefaceVariantCache
+        private set
+
+    private fun setTypefaceCache(value: TypefaceVariantCache) {
+        typefaceCache = value
+        if (this::textAnimator.isInitialized) {
+            textAnimator.typefaceCache = value
+        }
+        outlineAnimator?.typefaceCache = value
+        innerAnimator?.typefaceCache = value
+    }
+
+    @VisibleForTesting
+    var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator = { layout, invalidateCb ->
+        TextAnimator(layout, ClockAnimation.NUM_CLOCK_FONT_ANIMATION_STEPS, invalidateCb).also {
+            if (this::typefaceCache.isInitialized) {
+                it.typefaceCache = typefaceCache
+            }
+        }
+    }
+
+    override var verticalAlignment: VerticalAlignment = VerticalAlignment.CENTER
+    override var horizontalAlignment: HorizontalAlignment = HorizontalAlignment.LEFT
+    override var isAnimationEnabled = true
+    override var dozeFraction: Float = 0F
+        set(value) {
+            field = value
+            invalidate()
+        }
+
+    // Have to passthrough to unify View with SimpleDigitalClockView
+    override var text: String
+        get() = super.getText().toString()
+        set(value) = super.setText(value)
+
+    var textBorderWidth = 0F
+    var aodBorderWidth = 0F
+    var baselineFromMeasure = 0
+
+    var textFillColor: Int? = null
+    var textOutlineColor = TEXT_OUTLINE_DEFAULT_COLOR
+    var aodFillColor = AOD_DEFAULT_COLOR
+    var aodOutlineColor = AOD_OUTLINE_DEFAULT_COLOR
+
+    override fun updateColors(assets: AssetLoader, isRegionDark: Boolean) {
+        val fillColor = if (isRegionDark) textStyle.fillColorLight else textStyle.fillColorDark
+        textFillColor =
+            fillColor?.let { assets.readColor(it) }
+                ?: assets.seedColor
+                ?: getDefaultColor(assets, isRegionDark)
+        // for NumberOverlapView to read correct color
+        lockScreenPaint.color = textFillColor as Int
+        textStyle.outlineColor?.let { textOutlineColor = assets.readColor(it) }
+            ?: run { textOutlineColor = TEXT_OUTLINE_DEFAULT_COLOR }
+        (aodStyle.fillColorLight ?: aodStyle.fillColorDark)?.let {
+            aodFillColor = assets.readColor(it)
+        } ?: run { aodFillColor = AOD_DEFAULT_COLOR }
+        aodStyle.outlineColor?.let { aodOutlineColor = assets.readColor(it) }
+            ?: run { aodOutlineColor = AOD_OUTLINE_DEFAULT_COLOR }
+        if (dozeFraction < 1f) {
+            textAnimator.setTextStyle(color = textFillColor, animate = false)
+            outlineAnimator?.setTextStyle(color = textOutlineColor, animate = false)
+        }
+        invalidate()
+    }
+
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        logger.d("onMeasure()")
+        if (isVertical) {
+            // use at_most to avoid apply measuredWidth from last measuring to measuredHeight
+            // cause we use max to setMeasuredDimension
+            super.onMeasure(
+                MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), AT_MOST),
+                MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), AT_MOST),
+            )
+        } else {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+        }
+
+        val layout = this.layout
+        if (layout != null) {
+            if (!this::textAnimator.isInitialized) {
+                textAnimator = textAnimatorFactory(layout, ::invalidate)
+                outlineAnimator = textAnimatorFactory(layout) {}
+                innerAnimator = textAnimatorFactory(layout) {}
+                setInterpolatorPaint()
+            } else {
+                textAnimator.updateLayout(layout)
+                outlineAnimator?.updateLayout(layout)
+                innerAnimator?.updateLayout(layout)
+            }
+            baselineFromMeasure = layout.getLineBaseline(0)
+        } else {
+            val currentThread = Thread.currentThread()
+            Log.wtf(
+                TAG,
+                "TextView.getLayout() is null after measure! " +
+                    "currentThread=$currentThread; initThread=$initThread",
+            )
+        }
+
+        var expectedWidth: Int
+        var expectedHeight: Int
+
+        if (MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) {
+            // For view which has fixed height, e.g. small clock,
+            // we should always return the size required from parent view
+            expectedHeight = heightMeasureSpec
+        } else {
+            expectedHeight =
+                MeasureSpec.makeMeasureSpec(
+                    if (isSingleDigit()) {
+                        maxSingleDigitHeight
+                    } else {
+                        textBounds.height() + 2 * lockScreenPaint.strokeWidth.toInt()
+                    },
+                    MeasureSpec.getMode(measuredHeight),
+                )
+        }
+        if (MeasureSpec.getMode(widthMeasureSpec) == EXACTLY) {
+            expectedWidth = widthMeasureSpec
+        } else {
+            expectedWidth =
+                MeasureSpec.makeMeasureSpec(
+                    if (isSingleDigit()) {
+                        maxSingleDigitWidth
+                    } else {
+                        max(
+                            textBounds.width() + 2 * lockScreenPaint.strokeWidth.toInt(),
+                            MeasureSpec.getSize(measuredWidth),
+                        )
+                    },
+                    MeasureSpec.getMode(measuredWidth),
+                )
+        }
+
+        if (isVertical) {
+            expectedWidth = expectedHeight.also { expectedHeight = expectedWidth }
+        }
+        setMeasuredDimension(expectedWidth, expectedHeight)
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        if (isVertical) {
+            canvas.save()
+            canvas.translate(0F, measuredHeight.toFloat())
+            canvas.rotate(-90F)
+        }
+        logger.d({ "onDraw(); ls: $str1; aod: $str2;" }) {
+            str1 = textAnimator.textInterpolator.shapedText
+            str2 = outlineAnimator?.textInterpolator?.shapedText
+        }
+        val translation = getLocalTranslation()
+        canvas.translate(translation.x.toFloat(), translation.y.toFloat())
+        digitTranslateAnimator?.let {
+            canvas.translate(it.updatedTranslate.x.toFloat(), it.updatedTranslate.y.toFloat())
+        }
+
+        if (aodStyle.renderType == RenderType.HOLLOW_TEXT) {
+            canvas.saveLayer(
+                -translation.x.toFloat(),
+                -translation.y.toFloat(),
+                (-translation.x + measuredWidth).toFloat(),
+                (-translation.y + measuredHeight).toFloat(),
+                null,
+            )
+            outlineAnimator?.draw(canvas)
+            canvas.saveLayer(
+                -translation.x.toFloat(),
+                -translation.y.toFloat(),
+                (-translation.x + measuredWidth).toFloat(),
+                (-translation.y + measuredHeight).toFloat(),
+                Paint().also { it.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) },
+            )
+            innerAnimator?.draw(canvas)
+            canvas.restore()
+            canvas.restore()
+        } else if (aodStyle.renderType != RenderType.CHANGE_WEIGHT) {
+            outlineAnimator?.draw(canvas)
+        }
+        textAnimator.draw(canvas)
+
+        digitTranslateAnimator?.let {
+            canvas.translate(-it.updatedTranslate.x.toFloat(), -it.updatedTranslate.y.toFloat())
+        }
+        canvas.translate(-translation.x.toFloat(), -translation.y.toFloat())
+        if (isVertical) {
+            canvas.restore()
+        }
+    }
+
+    override fun invalidate() {
+        logger.d("invalidate()")
+        super.invalidate()
+        (parent as? DigitalClockFaceView)?.invalidate()
+    }
+
+    override fun refreshTime() {
+        logger.d("refreshTime()")
+        refreshText()
+    }
+
+    override fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
+        if (!this::textAnimator.isInitialized) {
+            return
+        }
+        val fvar = if (isDozing) aodStyle.fontVariation else textStyle.fontVariation
+        textAnimator.setTextStyle(
+            animate = isAnimated && isAnimationEnabled,
+            color = if (isDozing) aodFillColor else textFillColor,
+            textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize,
+            fvar = fvar,
+            duration = aodStyle.transitionDuration,
+            interpolator = aodDozingInterpolator,
+        )
+        updateTextBoundsForTextAnimator()
+        outlineAnimator?.setTextStyle(
+            animate = isAnimated && isAnimationEnabled,
+            color = if (isDozing) aodOutlineColor else textOutlineColor,
+            textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize,
+            fvar = fvar,
+            strokeWidth = if (isDozing) aodBorderWidth else textBorderWidth,
+            duration = aodStyle.transitionDuration,
+            interpolator = aodDozingInterpolator,
+        )
+        innerAnimator?.setTextStyle(
+            animate = isAnimated && isAnimationEnabled,
+            color = Color.WHITE,
+            textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize,
+            fvar = fvar,
+            duration = aodStyle.transitionDuration,
+            interpolator = aodDozingInterpolator,
+        )
+    }
+
+    override fun animateCharge() {
+        if (!this::textAnimator.isInitialized || textAnimator.isRunning()) {
+            // Skip charge animation if dozing animation is already playing.
+            return
+        }
+        logger.d("animateCharge()")
+        val middleFvar = if (dozeFraction == 0F) aodStyle.fontVariation else textStyle.fontVariation
+        val endFvar = if (dozeFraction == 0F) textStyle.fontVariation else aodStyle.fontVariation
+        val startAnimPhase2 = Runnable {
+            textAnimator.setTextStyle(fvar = endFvar, animate = isAnimationEnabled)
+            outlineAnimator?.setTextStyle(fvar = endFvar, animate = isAnimationEnabled)
+            innerAnimator?.setTextStyle(fvar = endFvar, animate = isAnimationEnabled)
+            updateTextBoundsForTextAnimator()
+        }
+        textAnimator.setTextStyle(
+            fvar = middleFvar,
+            animate = isAnimationEnabled,
+            onAnimationEnd = startAnimPhase2,
+        )
+        outlineAnimator?.setTextStyle(fvar = middleFvar, animate = isAnimationEnabled)
+        innerAnimator?.setTextStyle(fvar = middleFvar, animate = isAnimationEnabled)
+        updateTextBoundsForTextAnimator()
+    }
+
+    fun refreshText() {
+        lockScreenPaint.getTextBounds(text, 0, text.length, textBounds)
+        if (this::textAnimator.isInitialized) {
+            textAnimator.textInterpolator.targetPaint.getTextBounds(
+                text,
+                0,
+                text.length,
+                targetTextBounds,
+            )
+        }
+        if (layout == null) {
+            requestLayout()
+        } else {
+            textAnimator.updateLayout(layout)
+            outlineAnimator?.updateLayout(layout)
+            innerAnimator?.updateLayout(layout)
+        }
+    }
+
+    private fun isSingleDigit(): Boolean {
+        return id == R.id.HOUR_FIRST_DIGIT ||
+            id == R.id.HOUR_SECOND_DIGIT ||
+            id == R.id.MINUTE_FIRST_DIGIT ||
+            id == R.id.MINUTE_SECOND_DIGIT
+    }
+
+    private fun updateInterpolatedTextBounds(): Rect {
+        val interpolatedTextBounds = Rect()
+        if (textAnimator.animator.animatedFraction != 1.0f && textAnimator.animator.isRunning) {
+            interpolatedTextBounds.left =
+                MathUtils.lerp(
+                        prevTextBounds.left,
+                        targetTextBounds.left,
+                        textAnimator.animator.animatedValue as Float,
+                    )
+                    .toInt()
+
+            interpolatedTextBounds.right =
+                MathUtils.lerp(
+                        prevTextBounds.right,
+                        targetTextBounds.right,
+                        textAnimator.animator.animatedValue as Float,
+                    )
+                    .toInt()
+
+            interpolatedTextBounds.top =
+                MathUtils.lerp(
+                        prevTextBounds.top,
+                        targetTextBounds.top,
+                        textAnimator.animator.animatedValue as Float,
+                    )
+                    .toInt()
+
+            interpolatedTextBounds.bottom =
+                MathUtils.lerp(
+                        prevTextBounds.bottom,
+                        targetTextBounds.bottom,
+                        textAnimator.animator.animatedValue as Float,
+                    )
+                    .toInt()
+        } else {
+            interpolatedTextBounds.set(targetTextBounds)
+        }
+        return interpolatedTextBounds
+    }
+
+    private fun updateXtranslation(inPoint: Point, interpolatedTextBounds: Rect): Point {
+        val viewWidth = if (isVertical) measuredHeight else measuredWidth
+        when (horizontalAlignment) {
+            HorizontalAlignment.LEFT -> {
+                inPoint.x = lockScreenPaint.strokeWidth.toInt() - interpolatedTextBounds.left
+            }
+            HorizontalAlignment.RIGHT -> {
+                inPoint.x =
+                    viewWidth - interpolatedTextBounds.right - lockScreenPaint.strokeWidth.toInt()
+            }
+            HorizontalAlignment.CENTER -> {
+                inPoint.x =
+                    (viewWidth - interpolatedTextBounds.width()) / 2 - interpolatedTextBounds.left
+            }
+        }
+        return inPoint
+    }
+
+    // translation of reference point of text
+    // used for translation when calling textInterpolator
+    fun getLocalTranslation(): Point {
+        val viewHeight = if (isVertical) measuredWidth else measuredHeight
+        val interpolatedTextBounds = updateInterpolatedTextBounds()
+        val localTranslation = Point(0, 0)
+        val correctedBaseline = if (baseline != -1) baseline else baselineFromMeasure
+        // get the change from current baseline to expected baseline
+        when (verticalAlignment) {
+            VerticalAlignment.CENTER -> {
+                localTranslation.y =
+                    ((viewHeight - interpolatedTextBounds.height()) / 2 -
+                        interpolatedTextBounds.top -
+                        correctedBaseline)
+            }
+            VerticalAlignment.TOP -> {
+                localTranslation.y =
+                    (-interpolatedTextBounds.top + lockScreenPaint.strokeWidth - correctedBaseline)
+                        .toInt()
+            }
+            VerticalAlignment.BOTTOM -> {
+                localTranslation.y =
+                    viewHeight -
+                        interpolatedTextBounds.bottom -
+                        lockScreenPaint.strokeWidth.toInt() -
+                        correctedBaseline
+            }
+            VerticalAlignment.BASELINE -> {
+                localTranslation.y = -lockScreenPaint.strokeWidth.toInt()
+            }
+        }
+
+        return updateXtranslation(localTranslation, interpolatedTextBounds)
+    }
+
+    override fun applyStyles(assets: AssetLoader, textStyle: TextStyle, aodStyle: TextStyle?) {
+        this.textStyle = textStyle as FontTextStyle
+        val typefaceName = "fonts/" + textStyle.fontFamily
+        setTypefaceCache(assets.typefaceCache.getVariantCache(typefaceName))
+        lockScreenPaint.strokeJoin = Paint.Join.ROUND
+        lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(textStyle.fontVariation)
+        textStyle.fontFeatureSettings?.let {
+            lockScreenPaint.fontFeatureSettings = it
+            fontFeatureSettings = it
+        }
+        typeface = lockScreenPaint.typeface
+        textStyle.lineHeight?.let { lineHeight = it.toInt() }
+        // borderWidth in textStyle and aodStyle is used to draw,
+        // strokeWidth in lockScreenPaint is used to measure and get enough space for the text
+        textStyle.borderWidth?.let { textBorderWidth = parser.convert(it) }
+
+        if (aodStyle != null && aodStyle is FontTextStyle) {
+            this.aodStyle = aodStyle
+        } else {
+            this.aodStyle = textStyle.copy()
+        }
+        this.aodStyle.transitionInterpolator?.let { aodDozingInterpolator = it.interpolator }
+        aodBorderWidth = parser.convert(this.aodStyle.borderWidth ?: DEFAULT_AOD_STROKE_WIDTH)
+        lockScreenPaint.strokeWidth = ceil(max(textBorderWidth, aodBorderWidth))
+        measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
+        setInterpolatorPaint()
+        recomputeMaxSingleDigitSizes()
+        invalidate()
+    }
+
+    // When constrainedByHeight is on, targetFontSizePx is the constrained height of textView
+    override fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean) {
+        val adjustedFontSizePx = adjustFontSize(targetFontSizePx, constrainedByHeight)
+        val fontSizePx = adjustedFontSizePx * (textStyle.fontSizeScale ?: 1f)
+        aodFontSizePx =
+            adjustedFontSizePx * (aodStyle.fontSizeScale ?: textStyle.fontSizeScale ?: 1f)
+        if (fontSizePx > 0) {
+            setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx)
+            lockScreenPaint.textSize = textSize
+            lockScreenPaint.getTextBounds(text, 0, text.length, textBounds)
+            targetTextBounds.set(textBounds)
+        }
+        if (!constrainedByHeight) {
+            val lastUnconstrainedHeight = textBounds.height() + lockScreenPaint.strokeWidth * 2
+            fontSizeAdjustFactor = lastUnconstrainedHeight / lastUnconstrainedTextSize
+        }
+        textStyle.borderWidthScale?.let {
+            textBorderWidth = fontSizePx * it
+            if (dozeFraction < 1.0F) {
+                outlineAnimator?.setTextStyle(strokeWidth = textBorderWidth, animate = false)
+            }
+        }
+        aodStyle.borderWidthScale?.let {
+            aodBorderWidth = fontSizePx * it
+            if (dozeFraction > 0.0F) {
+                outlineAnimator?.setTextStyle(strokeWidth = aodBorderWidth, animate = false)
+            }
+        }
+
+        lockScreenPaint.strokeWidth = ceil(max(textBorderWidth, aodBorderWidth))
+        recomputeMaxSingleDigitSizes()
+
+        if (this::textAnimator.isInitialized) {
+            textAnimator.setTextStyle(textSize = lockScreenPaint.textSize, animate = false)
+        }
+        outlineAnimator?.setTextStyle(textSize = lockScreenPaint.textSize, animate = false)
+        innerAnimator?.setTextStyle(textSize = lockScreenPaint.textSize, animate = false)
+    }
+
+    private fun recomputeMaxSingleDigitSizes() {
+        val rectForCalculate = Rect()
+        maxSingleDigitHeight = 0
+        maxSingleDigitWidth = 0
+
+        for (i in 0..9) {
+            lockScreenPaint.getTextBounds(i.toString(), 0, 1, rectForCalculate)
+            maxSingleDigitHeight = max(maxSingleDigitHeight, rectForCalculate.height())
+            maxSingleDigitWidth = max(maxSingleDigitWidth, rectForCalculate.width())
+        }
+        maxSingleDigitWidth += 2 * lockScreenPaint.strokeWidth.toInt()
+        maxSingleDigitHeight += 2 * lockScreenPaint.strokeWidth.toInt()
+    }
+
+    // called without animation, can be used to set the initial state of animator
+    private fun setInterpolatorPaint() {
+        if (this::textAnimator.isInitialized) {
+            // set initial style
+            textAnimator.textInterpolator.targetPaint.set(lockScreenPaint)
+            textAnimator.textInterpolator.onTargetPaintModified()
+            textAnimator.setTextStyle(
+                fvar = textStyle.fontVariation,
+                textSize = lockScreenPaint.textSize,
+                color = textFillColor,
+                animate = false,
+            )
+        }
+
+        if (outlineAnimator != null) {
+            outlineAnimator!!
+                .textInterpolator
+                .targetPaint
+                .set(
+                    TextPaint(lockScreenPaint).also {
+                        it.style =
+                            if (aodStyle.renderType == RenderType.HOLLOW_TEXT)
+                                Paint.Style.FILL_AND_STROKE
+                            else Paint.Style.STROKE
+                    }
+                )
+            outlineAnimator!!.textInterpolator.onTargetPaintModified()
+            outlineAnimator!!.setTextStyle(
+                fvar = aodStyle.fontVariation,
+                textSize = lockScreenPaint.textSize,
+                color = Color.TRANSPARENT,
+                animate = false,
+            )
+        }
+
+        if (innerAnimator != null) {
+            innerAnimator!!
+                .textInterpolator
+                .targetPaint
+                .set(TextPaint(lockScreenPaint).also { it.style = Paint.Style.FILL })
+            innerAnimator!!.textInterpolator.onTargetPaintModified()
+            innerAnimator!!.setTextStyle(
+                fvar = aodStyle.fontVariation,
+                textSize = lockScreenPaint.textSize,
+                color = Color.WHITE,
+                animate = false,
+            )
+        }
+    }
+
+    /* Called after textAnimator.setTextStyle
+     * textAnimator.setTextStyle will update targetPaint,
+     * and rebase if previous animator is canceled
+     * so basePaint will store the state we transition from
+     * and targetPaint will store the state we transition to
+     */
+    private fun updateTextBoundsForTextAnimator() {
+        textAnimator.textInterpolator.basePaint.getTextBounds(text, 0, text.length, prevTextBounds)
+        textAnimator.textInterpolator.targetPaint.getTextBounds(
+            text,
+            0,
+            text.length,
+            targetTextBounds,
+        )
+    }
+
+    /*
+     * Adjust text size to adapt to large display / font size
+     * where the text view will be constrained by height
+     */
+    private fun adjustFontSize(targetFontSizePx: Float?, constrainedByHeight: Boolean): Float {
+        return if (constrainedByHeight) {
+            min((targetFontSizePx ?: 0F) / fontSizeAdjustFactor, lastUnconstrainedTextSize)
+        } else {
+            lastUnconstrainedTextSize = targetFontSizePx ?: 1F
+            lastUnconstrainedTextSize
+        }
+    }
+
+    companion object {
+        val DEFAULT_AOD_STROKE_WIDTH = "2dp"
+        val TEXT_OUTLINE_DEFAULT_COLOR = Color.TRANSPARENT
+        val AOD_DEFAULT_COLOR = Color.TRANSPARENT
+        val AOD_OUTLINE_DEFAULT_COLOR = Color.WHITE
+        private val DEFAULT_LIGHT_COLOR = "@android:color/system_accent1_100+0"
+        private val DEFAULT_DARK_COLOR = "@android:color/system_accent2_600+0"
+
+        fun getDefaultColor(assets: AssetLoader, isRegionDark: Boolean) =
+            assets.readColor(if (isRegionDark) DEFAULT_LIGHT_COLOR else DEFAULT_DARK_COLOR)
+    }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt
new file mode 100644
index 0000000..bbd2d3d
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.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.systemui.shared.clocks.view
+
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.shared.clocks.AssetLoader
+import com.android.systemui.shared.clocks.TextStyle
+
+interface SimpleDigitalClockView {
+    var text: String
+    var verticalAlignment: VerticalAlignment
+    var horizontalAlignment: HorizontalAlignment
+    var dozeFraction: Float
+    val textStyle: TextStyle
+    @VisibleForTesting var isAnimationEnabled: Boolean
+
+    fun applyStyles(assets: AssetLoader, textStyle: TextStyle, aodStyle: TextStyle?)
+
+    fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean = false)
+
+    fun updateColors(assets: AssetLoader, isRegionDark: Boolean)
+
+    fun refreshTime()
+
+    fun animateCharge()
+
+    fun animateDoze(isDozing: Boolean, isAnimated: Boolean)
+}
+
+enum class VerticalAlignment {
+    TOP,
+    BOTTOM,
+    BASELINE, // default
+    CENTER,
+}
+
+enum class HorizontalAlignment {
+    LEFT,
+    RIGHT,
+    CENTER, // default
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
index 8f9e238..8b13411 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
@@ -145,7 +145,7 @@
 
             fakeInputManager.addPhysicalKeyboard(
                 PHYSICAL_NOT_FULL_KEYBOARD_ID,
-                isFullKeyboard = false
+                isFullKeyboard = false,
             )
             assertThat(isKeyboardConnected).isFalse()
 
@@ -223,7 +223,7 @@
             backlightListenerCaptor.value.onBacklightChanged(
                 current = 1,
                 max = 5,
-                triggeredByKeyPress = false
+                triggeredByKeyPress = false,
             )
             assertThat(backlight).isNull()
         }
@@ -239,7 +239,7 @@
             backlightListenerCaptor.value.onBacklightChanged(
                 current = 1,
                 max = 5,
-                triggeredByKeyPress = true
+                triggeredByKeyPress = true,
             )
             assertThat(backlight).isNotNull()
         }
@@ -318,15 +318,75 @@
         }
     }
 
+    @Test
+    fun connectedKeyboards_emitsAllKeyboards() {
+        testScope.runTest {
+            val firstKeyboard = Keyboard(vendorId = 1, productId = 1)
+            val secondKeyboard = Keyboard(vendorId = 2, productId = 2)
+            captureDeviceListener()
+            val keyboards by collectLastValueImmediately(underTest.connectedKeyboards)
+
+            fakeInputManager.addPhysicalKeyboard(
+                PHYSICAL_FULL_KEYBOARD_ID,
+                vendorId = firstKeyboard.vendorId,
+                productId = firstKeyboard.productId,
+            )
+            assertThat(keyboards)
+                .containsExactly(Keyboard(firstKeyboard.vendorId, firstKeyboard.productId))
+
+            fakeInputManager.addPhysicalKeyboard(
+                ANOTHER_PHYSICAL_FULL_KEYBOARD_ID,
+                vendorId = secondKeyboard.vendorId,
+                productId = secondKeyboard.productId,
+            )
+            assertThat(keyboards)
+                .containsExactly(
+                    Keyboard(firstKeyboard.vendorId, firstKeyboard.productId),
+                    Keyboard(secondKeyboard.vendorId, secondKeyboard.productId),
+                )
+        }
+    }
+
+    @Test
+    fun connectedKeyboards_emitsOnlyFullPhysicalKeyboards() {
+        testScope.runTest {
+            captureDeviceListener()
+            val keyboards by collectLastValueImmediately(underTest.connectedKeyboards)
+
+            fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
+            fakeInputManager.addDevice(VIRTUAL_FULL_KEYBOARD_ID, SOURCE_KEYBOARD)
+            fakeInputManager.addPhysicalKeyboard(
+                PHYSICAL_NOT_FULL_KEYBOARD_ID,
+                isFullKeyboard = false,
+            )
+
+            assertThat(keyboards).hasSize(1)
+        }
+    }
+
+    @Test
+    fun connectedKeyboards_emitsOnlyConnectedKeyboards() {
+        testScope.runTest {
+            captureDeviceListener()
+            val keyboards by collectLastValueImmediately(underTest.connectedKeyboards)
+
+            fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
+            fakeInputManager.addPhysicalKeyboard(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
+            fakeInputManager.removeDevice(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
+
+            assertThat(keyboards).hasSize(1)
+        }
+    }
+
     private fun KeyboardBacklightListener.onBacklightChanged(
         current: Int,
         max: Int,
-        triggeredByKeyPress: Boolean = true
+        triggeredByKeyPress: Boolean = true,
     ) {
         onKeyboardBacklightChanged(
             /* deviceId= */ 0,
             TestBacklightState(current, max),
-            triggeredByKeyPress
+            triggeredByKeyPress,
         )
     }
 
@@ -343,7 +403,7 @@
 
     private class TestBacklightState(
         private val brightnessLevel: Int,
-        private val maxBrightnessLevel: Int
+        private val maxBrightnessLevel: Int,
     ) : KeyboardBacklightState() {
         override fun getBrightnessLevel() = brightnessLevel
 
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index aabfbd1..65c01ed 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -710,9 +710,16 @@
     @Override
     public void onShareButtonTapped() {
         if (clipboardSharedTransitions()) {
-            if (mClipboardModel.getType() != ClipboardModel.Type.OTHER) {
-                finishWithSharedTransition(CLIPBOARD_OVERLAY_SHARE_TAPPED,
-                        IntentCreator.getShareIntent(mClipboardModel.getClipData(), mContext));
+            switch (mClipboardModel.getType()) {
+                case TEXT:
+                case URI:
+                    finish(CLIPBOARD_OVERLAY_SHARE_TAPPED,
+                            IntentCreator.getShareIntent(mClipboardModel.getClipData(), mContext));
+                    break;
+                case IMAGE:
+                    finishWithSharedTransition(CLIPBOARD_OVERLAY_SHARE_TAPPED,
+                            IntentCreator.getShareIntent(mClipboardModel.getClipData(), mContext));
+                    break;
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
index 5a008bd..7711c48 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
@@ -38,7 +38,7 @@
 constructor(
     @Background private val backgroundHandler: Handler,
     @Background private val backgroundScope: CoroutineScope,
-    private val inputManager: InputManager
+    private val inputManager: InputManager,
 ) {
 
     sealed interface DeviceChange
@@ -50,11 +50,11 @@
     data object FreshStart : DeviceChange
 
     /**
-     * Emits collection of all currently connected keyboards and what was the last [DeviceChange].
-     * It emits collection so that every new subscriber to this SharedFlow can get latest state of
-     * all keyboards. Otherwise we might get into situation where subscriber timing on
-     * initialization matter and later subscriber will only get latest device and will miss all
-     * previous devices.
+     * Emits collection of all currently connected input devices and what was the last
+     * [DeviceChange]. It emits collection so that every new subscriber to this SharedFlow can get
+     * latest state of all input devices. Otherwise we might get into situation where subscriber
+     * timing on initialization matter and later subscriber will only get latest device and will
+     * miss all previous devices.
      */
     // TODO(b/351984587): Replace with StateFlow
     @SuppressLint("SharedFlowCreation")
@@ -79,11 +79,7 @@
                 inputManager.registerInputDeviceListener(listener, backgroundHandler)
                 awaitClose { inputManager.unregisterInputDeviceListener(listener) }
             }
-            .shareIn(
-                scope = backgroundScope,
-                started = SharingStarted.Lazily,
-                replay = 1,
-            )
+            .shareIn(scope = backgroundScope, started = SharingStarted.Lazily, replay = 1)
 
     private fun <T> SendChannel<T>.sendWithLogging(element: T) {
         trySendWithFailureLogging(element, TAG)
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt
index f49cfdd..021c069 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt
@@ -50,6 +50,8 @@
     private val _newlyConnectedKeyboard: MutableStateFlow<Keyboard?> = MutableStateFlow(null)
     override val newlyConnectedKeyboard: Flow<Keyboard> = _newlyConnectedKeyboard.filterNotNull()
 
+    override val connectedKeyboards: Flow<Set<Keyboard>> = MutableStateFlow(emptySet())
+
     init {
         Log.i(TAG, "initializing shell command $COMMAND")
         commandRegistry.registerCommand(COMMAND) { KeyboardCommand() }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
index a20dfa5..3329fe2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
@@ -61,6 +61,9 @@
      */
     val newlyConnectedKeyboard: Flow<Keyboard>
 
+    /** Emits set of currently connected keyboards */
+    val connectedKeyboards: Flow<Set<Keyboard>>
+
     /**
      * Emits [BacklightModel] whenever user changes backlight level from keyboard press. Can only
      * happen when physical keyboard is connected
@@ -74,7 +77,7 @@
 constructor(
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val inputManager: InputManager,
-    inputDeviceRepository: InputDeviceRepository
+    inputDeviceRepository: InputDeviceRepository,
 ) : KeyboardRepository {
 
     @FlowPreview
@@ -93,6 +96,13 @@
             .mapNotNull { deviceIdToKeyboard(it) }
             .flowOn(backgroundDispatcher)
 
+    override val connectedKeyboards: Flow<Set<Keyboard>> =
+        inputDeviceRepository.deviceChange
+            .map { (deviceIds, _) -> deviceIds }
+            .map { deviceIds -> deviceIds.filter { isPhysicalFullKeyboard(it) } }
+            .distinctUntilChanged()
+            .map { deviceIds -> deviceIds.mapNotNull { deviceIdToKeyboard(it) }.toSet() }
+
     override val isAnyKeyboardConnected: Flow<Boolean> =
         inputDeviceRepository.deviceChange
             .map { (ids, _) -> ids.any { id -> isPhysicalFullKeyboard(id) } }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 4f47536..f83548d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -994,7 +994,9 @@
         // be dropped, causing the shade expansion to fail silently. Since the shade doesn't open,
         // it doesn't become visible, and the bounds will never update. Therefore, we must detect
         // the incorrect bounds here and force the update so that touches are routed correctly.
-        if (SceneContainerFlag.isEnabled() && mWindowRootView.getVisibility() == View.INVISIBLE) {
+        if (SceneContainerFlag.isEnabled()
+                && mWindowRootView != null
+                && mWindowRootView.getVisibility() == View.INVISIBLE) {
             Rect bounds = newConfig.windowConfiguration.getBounds();
             if (mWindowRootView.getWidth() != bounds.width()) {
                 mLogger.logConfigChangeWidthAdjust(mWindowRootView.getWidth(), bounds.width());
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index 079c72f..1f92bc1 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -37,11 +37,8 @@
 import android.media.AudioSystem;
 import android.media.IAudioService;
 import android.media.IVolumeController;
-import android.media.MediaRoute2Info;
 import android.media.MediaRouter2Manager;
-import android.media.RoutingSessionInfo;
 import android.media.VolumePolicy;
-import android.media.session.MediaController;
 import android.media.session.MediaController.PlaybackInfo;
 import android.media.session.MediaSession.Token;
 import android.net.Uri;
@@ -88,7 +85,6 @@
 
 import java.io.PrintWriter;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
@@ -217,7 +213,7 @@
                 VolumeDialogControllerImpl.class.getSimpleName());
         mWorker = new W(mWorkerLooper);
         mRouter2Manager = MediaRouter2Manager.getInstance(mContext);
-        mMediaSessionsCallbacksW = new MediaSessionsCallbacks(mContext);
+        mMediaSessionsCallbacksW = new MediaSessionsCallbacks();
         mMediaSessions = createMediaSessions(mContext, mWorkerLooper, mMediaSessionsCallbacksW);
         mAudioSharingInteractor = audioSharingInteractor;
         mJavaAdapter = javaAdapter;
@@ -1360,16 +1356,9 @@
         private final HashMap<Token, Integer> mRemoteStreams = new HashMap<>();
 
         private int mNextStream = DYNAMIC_STREAM_REMOTE_START_INDEX;
-        private final boolean mVolumeAdjustmentForRemoteGroupSessions;
-
-        public MediaSessionsCallbacks(Context context) {
-            mVolumeAdjustmentForRemoteGroupSessions = context.getResources().getBoolean(
-                    com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions);
-        }
 
         @Override
         public void onRemoteUpdate(Token token, String name, PlaybackInfo pi) {
-            if (showForSession(token)) {
                 addStream(token, "onRemoteUpdate");
 
                 int stream = 0;
@@ -1396,12 +1385,10 @@
                     Log.d(TAG, "onRemoteUpdate: " + name + ": " + ss.level + " of " + ss.levelMax);
                     mCallbacks.onStateChanged(mState);
                 }
-            }
         }
 
         @Override
         public void onRemoteVolumeChanged(Token token, int flags) {
-            if (showForSession(token)) {
                 addStream(token, "onRemoteVolumeChanged");
                 int stream = 0;
                 synchronized (mRemoteStreams) {
@@ -1420,27 +1407,27 @@
                 if (showUI) {
                     onShowRequestedW(Events.SHOW_REASON_REMOTE_VOLUME_CHANGED);
                 }
-            }
         }
 
         @Override
         public void onRemoteRemoved(Token token) {
-            if (showForSession(token)) {
-                int stream = 0;
-                synchronized (mRemoteStreams) {
-                    if (!mRemoteStreams.containsKey(token)) {
-                        Log.d(TAG, "onRemoteRemoved: stream doesn't exist, "
-                                + "aborting remote removed for token:" + token.toString());
-                        return;
-                    }
-                    stream = mRemoteStreams.get(token);
+            int stream;
+            synchronized (mRemoteStreams) {
+                if (!mRemoteStreams.containsKey(token)) {
+                    Log.d(
+                            TAG,
+                            "onRemoteRemoved: stream doesn't exist, "
+                                    + "aborting remote removed for token:"
+                                    + token.toString());
+                    return;
                 }
-                mState.states.remove(stream);
-                if (mState.activeStream == stream) {
-                    updateActiveStreamW(-1);
-                }
-                mCallbacks.onStateChanged(mState);
+                stream = mRemoteStreams.get(token);
             }
+            mState.states.remove(stream);
+            if (mState.activeStream == stream) {
+                updateActiveStreamW(-1);
+            }
+            mCallbacks.onStateChanged(mState);
         }
 
         public void setStreamVolume(int stream, int level) {
@@ -1449,39 +1436,7 @@
                 Log.w(TAG, "setStreamVolume: No token found for stream: " + stream);
                 return;
             }
-            if (showForSession(token)) {
-                mMediaSessions.setVolume(token, level);
-            }
-        }
-
-        private boolean showForSession(Token token) {
-            if (mVolumeAdjustmentForRemoteGroupSessions) {
-                if (DEBUG) {
-                    Log.d(TAG, "Volume adjustment for remote group sessions allowed,"
-                            + " showForSession: true");
-                }
-                return true;
-            }
-            MediaController ctr = new MediaController(mContext, token);
-            String packageName = ctr.getPackageName();
-            List<RoutingSessionInfo> sessions =
-                    mRouter2Manager.getRoutingSessions(packageName);
-            if (DEBUG) {
-                Log.d(TAG, "Found " + sessions.size() + " routing sessions for package name "
-                        + packageName);
-            }
-            for (RoutingSessionInfo session : sessions) {
-                if (DEBUG) {
-                    Log.d(TAG, "Found routingSessionInfo: " + session);
-                }
-                if (!session.isSystemSession()
-                        && session.getVolumeHandling() != MediaRoute2Info.PLAYBACK_VOLUME_FIXED) {
-                    return true;
-                }
-            }
-
-            Log.d(TAG, "No routing session for " + packageName);
-            return false;
+            mMediaSessions.setVolume(token, level);
         }
 
         private Token findToken(int stream) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
index 5fc1971..8075d11 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
@@ -104,6 +104,8 @@
 
     @Mock
     private Animator mAnimator;
+    @Mock
+    private Animator mEndAnimator;
     private ArgumentCaptor<Animator.AnimatorListener> mAnimatorListenerCaptor =
             ArgumentCaptor.forClass(Animator.AnimatorListener.class);
 
@@ -123,7 +125,7 @@
         MockitoAnnotations.initMocks(this);
 
         when(mClipboardOverlayView.getEnterAnimation()).thenReturn(mAnimator);
-        when(mClipboardOverlayView.getExitAnimation()).thenReturn(mAnimator);
+        when(mClipboardOverlayView.getExitAnimation()).thenReturn(mEndAnimator);
         when(mClipboardOverlayView.getFadeOutAnimation()).thenReturn(mAnimator);
         when(mClipboardOverlayWindow.getWindowInsets()).thenReturn(
                 getImeInsets(new Rect(0, 0, 0, 0)));
@@ -318,11 +320,11 @@
         mOverlayController.setClipData(mSampleClipData, "");
 
         mCallbacks.onShareButtonTapped();
-        verify(mAnimator).addListener(mAnimatorListenerCaptor.capture());
-        mAnimatorListenerCaptor.getValue().onAnimationEnd(mAnimator);
+        verify(mEndAnimator).addListener(mAnimatorListenerCaptor.capture());
+        mAnimatorListenerCaptor.getValue().onAnimationEnd(mEndAnimator);
 
         verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "");
-        verify(mClipboardOverlayView, times(1)).getFadeOutAnimation();
+        verify(mClipboardOverlayView, times(1)).getExitAnimation();
     }
 
     @Test
@@ -343,8 +345,8 @@
         initController();
 
         mCallbacks.onDismissButtonTapped();
-        verify(mAnimator).addListener(mAnimatorListenerCaptor.capture());
-        mAnimatorListenerCaptor.getValue().onAnimationEnd(mAnimator);
+        verify(mEndAnimator).addListener(mAnimatorListenerCaptor.capture());
+        mAnimatorListenerCaptor.getValue().onAnimationEnd(mEndAnimator);
 
         // package name is null since we haven't actually set a source for this test
         verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, null);
@@ -403,14 +405,18 @@
 
         mOverlayController.setClipData(mSampleClipData, "first.package");
         mCallbacks.onShareButtonTapped();
+        verify(mEndAnimator).addListener(mAnimatorListenerCaptor.capture());
+        mAnimatorListenerCaptor.getValue().onAnimationEnd(mEndAnimator);
 
         mOverlayController.setClipData(mSampleClipData, "second.package");
         mCallbacks.onShareButtonTapped();
+        verify(mEndAnimator, times(2)).addListener(mAnimatorListenerCaptor.capture());
+        mAnimatorListenerCaptor.getValue().onAnimationEnd(mEndAnimator);
 
-        verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "first.package");
-        verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "second.package");
         verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHOWN_EXPANDED, 0, "first.package");
+        verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "first.package");
         verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHOWN_EXPANDED, 0, "second.package");
+        verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "second.package");
         verifyNoMoreInteractions(mUiEventLogger);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt
new file mode 100644
index 0000000..040a9e9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.shared.clocks.FontTextStyle
+import com.android.systemui.shared.clocks.LogUtil
+import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class SimpleDigitalClockTextViewTest : SysuiTestCase() {
+    private val messageBuffer = LogUtil.DEBUG_MESSAGE_BUFFER
+    private lateinit var underTest: SimpleDigitalClockTextView
+    private val defaultLargeClockTextSize = 500F
+    private val smallerTextSize = 300F
+    private val largerTextSize = 800F
+    private val firstMeasureTextSize = 100F
+
+    @Before
+    fun setup() {
+        underTest = SimpleDigitalClockTextView(context, messageBuffer)
+        underTest.textStyle = FontTextStyle()
+        underTest.aodStyle = FontTextStyle()
+        underTest.text = "0"
+        underTest.applyTextSize(defaultLargeClockTextSize)
+    }
+
+    @Test
+    fun applySmallerConstrainedTextSize_applyConstrainedTextSize() {
+        underTest.applyTextSize(smallerTextSize, constrainedByHeight = true)
+        assertEquals(smallerTextSize, underTest.textSize * underTest.fontSizeAdjustFactor)
+    }
+
+    @Test
+    fun applyLargerConstrainedTextSize_applyUnconstrainedTextSize() {
+        underTest.applyTextSize(largerTextSize, constrainedByHeight = true)
+        assertEquals(defaultLargeClockTextSize, underTest.textSize)
+    }
+
+    @Test
+    fun applyFirstMeasureConstrainedTextSize_getConstrainedTextSize() {
+        underTest.applyTextSize(firstMeasureTextSize, constrainedByHeight = true)
+        underTest.applyTextSize(smallerTextSize, constrainedByHeight = true)
+        assertEquals(smallerTextSize, underTest.textSize * underTest.fontSizeAdjustFactor)
+    }
+
+    @Test
+    fun applySmallFirstMeasureConstrainedSizeAndLargerConstrainedTextSize_applyDefaultSize() {
+        underTest.applyTextSize(firstMeasureTextSize, constrainedByHeight = true)
+        underTest.applyTextSize(largerTextSize, constrainedByHeight = true)
+        assertEquals(defaultLargeClockTextSize, underTest.textSize)
+    }
+
+    @Test
+    fun applyFirstMeasureConstrainedTextSize_applyUnconstrainedTextSize() {
+        underTest.applyTextSize(firstMeasureTextSize, constrainedByHeight = true)
+        underTest.applyTextSize(defaultLargeClockTextSize)
+        assertEquals(defaultLargeClockTextSize, underTest.textSize)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
index f62beeb..beba0f0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
@@ -133,10 +133,6 @@
         when(mRingerModeInternalLiveData.getValue()).thenReturn(-1);
         when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
         when(mUserTracker.getUserContext()).thenReturn(mContext);
-        // Enable group volume adjustments
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions,
-                true);
 
         mCallback = mock(VolumeDialogControllerImpl.C.class);
         mThreadFactory.setLooper(TestableLooper.get(this).getLooper());
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt
index b37cac1..ba31683 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt
@@ -19,8 +19,10 @@
 
 import com.android.systemui.keyboard.data.model.Keyboard
 import com.android.systemui.keyboard.shared.model.BacklightModel
+import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.consumeAsFlow
 import kotlinx.coroutines.flow.filterNotNull
 
 class FakeKeyboardRepository : KeyboardRepository {
@@ -32,8 +34,14 @@
     // filtering to make sure backlight doesn't have default initial value
     override val backlight: Flow<BacklightModel> = _backlightState.filterNotNull()
 
-    private val _newlyConnectedKeyboard: MutableStateFlow<Keyboard?> = MutableStateFlow(null)
-    override val newlyConnectedKeyboard: Flow<Keyboard> = _newlyConnectedKeyboard.filterNotNull()
+    // implemented as channel because original implementation is modeling events: it doesn't hold
+    // state so it won't always emit once connected. And it's bad if some tests depend on that
+    // incorrect behaviour.
+    private val _newlyConnectedKeyboard: Channel<Keyboard> = Channel()
+    override val newlyConnectedKeyboard: Flow<Keyboard> = _newlyConnectedKeyboard.consumeAsFlow()
+
+    private val _connectedKeyboards: MutableStateFlow<Set<Keyboard>> = MutableStateFlow(setOf())
+    override val connectedKeyboards: Flow<Set<Keyboard>> = _connectedKeyboards
 
     fun setBacklight(state: BacklightModel) {
         _backlightState.value = state
@@ -43,7 +51,14 @@
         _isAnyKeyboardConnected.value = connected
     }
 
+    fun setConnectedKeyboards(keyboards: Set<Keyboard>) {
+        _connectedKeyboards.value = keyboards
+        _isAnyKeyboardConnected.value = keyboards.isNotEmpty()
+    }
+
     fun setNewlyConnectedKeyboard(keyboard: Keyboard) {
-        _newlyConnectedKeyboard.value = keyboard
+        _newlyConnectedKeyboard.trySend(keyboard)
+        _connectedKeyboards.value += keyboard
+        _isAnyKeyboardConnected.value = true
     }
 }
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index 0a9109b..d752429 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -16,7 +16,6 @@
 
 package com.android.server.media;
 
-import static android.media.MediaRoute2Info.PLAYBACK_VOLUME_FIXED;
 import static android.media.VolumeProvider.VOLUME_CONTROL_ABSOLUTE;
 import static android.media.VolumeProvider.VOLUME_CONTROL_FIXED;
 import static android.media.VolumeProvider.VOLUME_CONTROL_RELATIVE;
@@ -48,9 +47,7 @@
 import android.media.AudioManager;
 import android.media.AudioSystem;
 import android.media.MediaMetadata;
-import android.media.MediaRouter2Manager;
 import android.media.Rating;
-import android.media.RoutingSessionInfo;
 import android.media.VolumeProvider;
 import android.media.session.ISession;
 import android.media.session.ISessionCallback;
@@ -186,7 +183,6 @@
     private final MediaSessionService mService;
     private final UriGrantsManagerInternal mUgmInternal;
     private final Context mContext;
-    private final boolean mVolumeAdjustmentForRemoteGroupSessions;
 
     private final ForegroundServiceDelegationOptions mForegroundServiceDelegationOptions;
 
@@ -311,8 +307,6 @@
         mAudioAttrs = DEFAULT_ATTRIBUTES;
         mPolicies = policies;
         mUgmInternal = LocalServices.getService(UriGrantsManagerInternal.class);
-        mVolumeAdjustmentForRemoteGroupSessions = mContext.getResources().getBoolean(
-                com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions);
 
         mForegroundServiceDelegationOptions = createForegroundServiceDelegationOptions();
 
@@ -659,49 +653,7 @@
             }
             return false;
         }
-        if (mVolumeAdjustmentForRemoteGroupSessions) {
-            if (DEBUG) {
-                Slog.d(
-                        TAG,
-                        "Volume adjustment for remote group sessions allowed so MediaSessionRecord"
-                                + " can handle volume key");
-            }
-            return true;
-        }
-        // See b/228021646 for details.
-        MediaRouter2Manager mRouter2Manager = MediaRouter2Manager.getInstance(mContext);
-        List<RoutingSessionInfo> sessions = mRouter2Manager.getRoutingSessions(mPackageName);
-        boolean foundNonSystemSession = false;
-        boolean remoteSessionAllowVolumeAdjustment = true;
-        if (DEBUG) {
-            Slog.d(
-                    TAG,
-                    "Found "
-                            + sessions.size()
-                            + " routing sessions for package name "
-                            + mPackageName);
-        }
-        for (RoutingSessionInfo session : sessions) {
-            if (DEBUG) {
-                Slog.d(TAG, "Found routingSessionInfo: " + session);
-            }
-            if (!session.isSystemSession()) {
-                foundNonSystemSession = true;
-                if (session.getVolumeHandling() == PLAYBACK_VOLUME_FIXED) {
-                    remoteSessionAllowVolumeAdjustment = false;
-                }
-            }
-        }
-        if (!foundNonSystemSession) {
-            if (DEBUG) {
-                Slog.d(
-                        TAG,
-                        "Package " + mPackageName
-                                + " has a remote media session but no associated routing session");
-            }
-        }
-
-        return foundNonSystemSession && remoteSessionAllowVolumeAdjustment;
+        return true;
     }
 
     @Override
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index 1346a29..dc48242 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -1282,11 +1282,9 @@
         boolean updateHintAllowedByProcState(boolean allowed) {
             synchronized (this) {
                 if (allowed && !mUpdateAllowedByProcState && !mShouldForcePause) {
-                    Slogf.e(TAG, "ADPF IS GETTING RESUMED? UID: " + mUid + " TAG: " + mTag);
                     resume();
                 }
                 if (!allowed && mUpdateAllowedByProcState) {
-                    Slogf.e(TAG, "ADPF IS GETTING PAUSED? UID: " + mUid + " TAG: " + mTag);
                     pause();
                 }
                 mUpdateAllowedByProcState = allowed;