Merge "[flexiglass] Add NotifIconContainer to Lockscreen" into main
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index a395c1a..89ea852 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -153,6 +153,7 @@
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.function.Predicate;
 
 /**
@@ -712,6 +713,24 @@
         }
 
         public abstract void writeToParcel(Parcel dest, int flags);
+
+        /**
+         * Override to return true if this Action can be serialized to Protobuf, and implement
+         * writeToProto / createFromProto.
+         *
+         * If this returns false, then the action will be omitted from RemoteViews previews created
+         * with createPreviewFromProto / writePreviewToProto.
+         *
+         * Because Parcelables should not be serialized to disk, any action that contains an Intent,
+         * PendingIntent, or Bundle should return false here.
+         */
+        public boolean canWriteToProto() {
+            return false;
+        }
+
+        public void writeToProto(ProtoOutputStream out, Context context, Resources appResources) {
+            throw new UnsupportedOperationException();
+        }
     }
 
     /**
@@ -1506,6 +1525,7 @@
             switch (in.getFieldNumber()) {
                 case (int) RemoteViewsProto.RemoteCollectionCache.ENTRIES:
                     final LongSparseArray<Object> entry = new LongSparseArray<>();
+
                     final long entryToken = in.start(
                             RemoteViewsProto.RemoteCollectionCache.ENTRIES);
                     while (in.nextField() != NO_MORE_FIELDS) {
@@ -1533,10 +1553,12 @@
                         }
                     }
                     in.end(entryToken);
+
                     checkContainsKeys(entry,
                             new long[]{RemoteViewsProto.RemoteCollectionCache.Entry.ID,
                                     RemoteViewsProto.RemoteCollectionCache.Entry.URI,
                                     RemoteViewsProto.RemoteCollectionCache.Entry.ITEMS});
+
                     entries.add(entry);
                     break;
                 default:
@@ -2247,6 +2269,62 @@
         public int getActionTag() {
             return BITMAP_REFLECTION_ACTION_TAG;
         }
+
+        @Override
+        public boolean canWriteToProto() {
+            return true;
+        }
+
+        @Override
+        public void writeToProto(ProtoOutputStream out, Context context, Resources appResources) {
+            final long token = out.start(RemoteViewsProto.Action.BITMAP_REFLECTION_ACTION);
+            out.write(RemoteViewsProto.BitmapReflectionAction.VIEW_ID,
+                    appResources.getResourceName(mViewId));
+            out.write(RemoteViewsProto.BitmapReflectionAction.METHOD_NAME, mMethodName);
+            out.write(RemoteViewsProto.BitmapReflectionAction.BITMAP_ID, mBitmapId);
+            out.end(token);
+        }
+    }
+
+    private PendingResources<Action> createFromBitmapReflectionActionFromProto(ProtoInputStream in)
+            throws Exception {
+        final LongSparseArray<Object> values = new LongSparseArray<>();
+
+        final long token = in.start(RemoteViewsProto.Action.BITMAP_REFLECTION_ACTION);
+        while (in.nextField() != NO_MORE_FIELDS) {
+            switch (in.getFieldNumber()) {
+                case (int) RemoteViewsProto.BitmapReflectionAction.VIEW_ID:
+                    values.put(RemoteViewsProto.BitmapReflectionAction.VIEW_ID,
+                            in.readString(RemoteViewsProto.BitmapReflectionAction.VIEW_ID));
+                    break;
+                case (int) RemoteViewsProto.BitmapReflectionAction.METHOD_NAME:
+                    values.put(RemoteViewsProto.BitmapReflectionAction.METHOD_NAME,
+                            in.readString(RemoteViewsProto.BitmapReflectionAction.METHOD_NAME));
+                    break;
+                case (int) RemoteViewsProto.BitmapReflectionAction.BITMAP_ID:
+                    values.put(RemoteViewsProto.BitmapReflectionAction.BITMAP_ID,
+                            in.readInt(RemoteViewsProto.BitmapReflectionAction.BITMAP_ID));
+                    break;
+                default:
+                    Log.w(LOG_TAG, "Unhandled field while reading RemoteViews proto!\n"
+                            + ProtoUtils.currentFieldToString(in));
+            }
+        }
+        in.end(token);
+
+        checkContainsKeys(values, new long[]{RemoteViewsProto.BitmapReflectionAction.VIEW_ID,
+                RemoteViewsProto.BitmapReflectionAction.METHOD_NAME});
+
+        return (context, resources, rootData, depth) -> {
+            int viewId = getAsIdentifier(resources, values,
+                    RemoteViewsProto.BitmapReflectionAction.VIEW_ID);
+            return new BitmapReflectionAction(viewId,
+                    (String) values.get(RemoteViewsProto.BitmapReflectionAction.METHOD_NAME),
+                    rootData.mBitmapCache.getBitmapForId(
+                            (int) values.get(RemoteViewsProto.BitmapReflectionAction.BITMAP_ID,
+                                    0)));
+        };
+
     }
 
     /**
@@ -2560,6 +2638,268 @@
         public int getActionTag() {
             return REFLECTION_ACTION_TAG;
         }
+
+        @Override
+        public boolean canWriteToProto() {
+            return true;
+        }
+
+        @Override
+        public void writeToProto(ProtoOutputStream out, Context context, Resources appResources) {
+            final long token = out.start(RemoteViewsProto.Action.REFLECTION_ACTION);
+            out.write(RemoteViewsProto.ReflectionAction.VIEW_ID,
+                    appResources.getResourceName(mViewId));
+            out.write(RemoteViewsProto.ReflectionAction.METHOD_NAME, mMethodName);
+            out.write(RemoteViewsProto.ReflectionAction.PARAMETER_TYPE, mType);
+            if (this.mValue != null) {
+                switch (this.mType) {
+                    case BOOLEAN:
+                        // ProtoOutputStream will omit this write if the value is false
+                        out.write(RemoteViewsProto.ReflectionAction.BOOLEAN_VALUE,
+                                (boolean) this.mValue);
+                        break;
+                    case BYTE:
+                        out.write(RemoteViewsProto.ReflectionAction.BYTE_VALUE,
+                                new byte[]{(byte) this.mValue});
+                        break;
+                    case SHORT:
+                        out.write(RemoteViewsProto.ReflectionAction.SHORT_VALUE,
+                                (short) this.mValue);
+                        break;
+                    case INT:
+                        out.write(RemoteViewsProto.ReflectionAction.INT_VALUE, (int) this.mValue);
+                        break;
+                    case LONG:
+                        out.write(RemoteViewsProto.ReflectionAction.LONG_VALUE, (long) this.mValue);
+                        break;
+                    case FLOAT:
+                        out.write(RemoteViewsProto.ReflectionAction.FLOAT_VALUE,
+                                (float) this.mValue);
+                        break;
+                    case DOUBLE:
+                        out.write(RemoteViewsProto.ReflectionAction.DOUBLE_VALUE,
+                                (double) this.mValue);
+                        break;
+                    case CHAR:
+                        out.write(RemoteViewsProto.ReflectionAction.CHAR_VALUE,
+                                (Character) this.mValue);
+                        break;
+                    case STRING:
+                        out.write(RemoteViewsProto.ReflectionAction.STRING_VALUE,
+                                (String) this.mValue);
+                        break;
+                    case CHAR_SEQUENCE:
+                        long csToken = out.start(
+                                RemoteViewsProto.ReflectionAction.CHAR_SEQUENCE_VALUE);
+                        RemoteViewsSerializers.writeCharSequenceToProto(out,
+                                (CharSequence) this.mValue);
+                        out.end(csToken);
+                        break;
+                    case URI:
+                        out.write(RemoteViewsProto.ReflectionAction.URI_VALUE,
+                                ((Uri) this.mValue).toString());
+                        break;
+                    case BITMAP:
+                        final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+                        ((Bitmap) this.mValue).compress(Bitmap.CompressFormat.WEBP_LOSSLESS, 100,
+                                bytes);
+                        out.write(RemoteViewsProto.ReflectionAction.BITMAP_VALUE,
+                                bytes.toByteArray());
+                        break;
+                    case BLEND_MODE:
+                        out.write(RemoteViewsProto.ReflectionAction.BLEND_MODE_VALUE,
+                                BlendMode.toValue((BlendMode) this.mValue));
+                        break;
+                    case COLOR_STATE_LIST:
+                        writeColorStateListToProto(out, (ColorStateList) this.mValue,
+                                RemoteViewsProto.ReflectionAction.COLOR_STATE_LIST_VALUE);
+                        break;
+                    case ICON:
+                        writeIconToProto(out, appResources, (Icon) this.mValue,
+                                RemoteViewsProto.ReflectionAction.ICON_VALUE);
+                        break;
+                    case BUNDLE:
+                    case INTENT:
+                    default:
+                        break;
+                }
+            }
+            out.end(token);
+        }
+
+        public static PendingResources<Action> createFromProto(ProtoInputStream in)
+                throws Exception {
+            final LongSparseArray<Object> values = new LongSparseArray<>();
+
+            final long token = in.start(RemoteViewsProto.Action.REFLECTION_ACTION);
+            while (in.nextField() != NO_MORE_FIELDS) {
+                switch (in.getFieldNumber()) {
+                    case (int) RemoteViewsProto.ReflectionAction.VIEW_ID:
+                        values.put(RemoteViewsProto.ReflectionAction.VIEW_ID,
+                                in.readString(RemoteViewsProto.ReflectionAction.VIEW_ID));
+                        break;
+                    case (int) RemoteViewsProto.ReflectionAction.METHOD_NAME:
+                        values.put(RemoteViewsProto.ReflectionAction.METHOD_NAME,
+                                in.readString(RemoteViewsProto.ReflectionAction.METHOD_NAME));
+                        break;
+                    case (int) RemoteViewsProto.ReflectionAction.PARAMETER_TYPE:
+                        values.put(RemoteViewsProto.ReflectionAction.PARAMETER_TYPE,
+                                in.readInt(RemoteViewsProto.ReflectionAction.PARAMETER_TYPE));
+                        break;
+                    case (int) RemoteViewsProto.ReflectionAction.BOOLEAN_VALUE:
+                        values.put(RemoteViewsProto.ReflectionAction.BOOLEAN_VALUE,
+                                in.readBoolean(RemoteViewsProto.ReflectionAction.BOOLEAN_VALUE));
+                        break;
+                    case (int) RemoteViewsProto.ReflectionAction.BYTE_VALUE:
+                        values.put(RemoteViewsProto.ReflectionAction.BYTE_VALUE,
+                                in.readBytes(RemoteViewsProto.ReflectionAction.BYTE_VALUE));
+                        break;
+                    case (int) RemoteViewsProto.ReflectionAction.SHORT_VALUE:
+                        values.put(RemoteViewsProto.ReflectionAction.SHORT_VALUE,
+                                (short) in.readInt(RemoteViewsProto.ReflectionAction.SHORT_VALUE));
+                        break;
+                    case (int) RemoteViewsProto.ReflectionAction.INT_VALUE:
+                        values.put(RemoteViewsProto.ReflectionAction.INT_VALUE,
+                                in.readInt(RemoteViewsProto.ReflectionAction.INT_VALUE));
+                        break;
+                    case (int) RemoteViewsProto.ReflectionAction.LONG_VALUE:
+                        values.put(RemoteViewsProto.ReflectionAction.LONG_VALUE,
+                                in.readLong(RemoteViewsProto.ReflectionAction.LONG_VALUE));
+                        break;
+                    case (int) RemoteViewsProto.ReflectionAction.FLOAT_VALUE:
+                        values.put(RemoteViewsProto.ReflectionAction.FLOAT_VALUE,
+                                in.readFloat(RemoteViewsProto.ReflectionAction.FLOAT_VALUE));
+                        break;
+                    case (int) RemoteViewsProto.ReflectionAction.DOUBLE_VALUE:
+                        values.put(RemoteViewsProto.ReflectionAction.DOUBLE_VALUE,
+                                in.readDouble(RemoteViewsProto.ReflectionAction.DOUBLE_VALUE));
+                        break;
+                    case (int) RemoteViewsProto.ReflectionAction.CHAR_VALUE:
+                        values.put(RemoteViewsProto.ReflectionAction.CHAR_VALUE,
+                                (char) in.readInt(RemoteViewsProto.ReflectionAction.CHAR_VALUE));
+                        break;
+                    case (int) RemoteViewsProto.ReflectionAction.STRING_VALUE:
+                        values.put(RemoteViewsProto.ReflectionAction.STRING_VALUE,
+                                in.readString(RemoteViewsProto.ReflectionAction.STRING_VALUE));
+                        break;
+                    case (int) RemoteViewsProto.ReflectionAction.CHAR_SEQUENCE_VALUE:
+                        values.put(RemoteViewsProto.ReflectionAction.CHAR_SEQUENCE_VALUE,
+                                createCharSequenceFromProto(in,
+                                        RemoteViewsProto.ReflectionAction.CHAR_SEQUENCE_VALUE));
+                        break;
+                    case (int) RemoteViewsProto.ReflectionAction.URI_VALUE:
+                        values.put(RemoteViewsProto.ReflectionAction.URI_VALUE,
+                                in.readString(RemoteViewsProto.ReflectionAction.URI_VALUE));
+                        break;
+                    case (int) RemoteViewsProto.ReflectionAction.BITMAP_VALUE:
+                        byte[] bitmapData = in.readBytes(
+                                RemoteViewsProto.ReflectionAction.BITMAP_VALUE);
+                        values.put(RemoteViewsProto.ReflectionAction.BITMAP_VALUE,
+                                BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length));
+                        break;
+                    case (int) RemoteViewsProto.ReflectionAction.COLOR_STATE_LIST_VALUE:
+                        values.put(RemoteViewsProto.ReflectionAction.COLOR_STATE_LIST_VALUE,
+                                createColorStateListFromProto(in,
+                                        RemoteViewsProto.ReflectionAction.COLOR_STATE_LIST_VALUE));
+                        break;
+                    case (int) RemoteViewsProto.ReflectionAction.ICON_VALUE:
+                        values.put(RemoteViewsProto.ReflectionAction.ICON_VALUE,
+                                createIconFromProto(in,
+                                        RemoteViewsProto.ReflectionAction.ICON_VALUE));
+                        break;
+                    case (int) RemoteViewsProto.ReflectionAction.BLEND_MODE_VALUE:
+                        values.put(RemoteViewsProto.ReflectionAction.BLEND_MODE_VALUE,
+                                BlendMode.fromValue(in.readInt(
+                                        RemoteViewsProto.ReflectionAction.BLEND_MODE_VALUE)));
+                        break;
+                    default:
+                        Log.w(LOG_TAG, "Unhandled field while reading RemoteViews proto!\n"
+                                + ProtoUtils.currentFieldToString(in));
+                }
+            }
+            in.end(token);
+
+            checkContainsKeys(values, new long[]{RemoteViewsProto.ReflectionAction.VIEW_ID,
+                    RemoteViewsProto.ReflectionAction.METHOD_NAME,
+                    RemoteViewsProto.ReflectionAction.PARAMETER_TYPE});
+
+            return (context, resources, rootData, depth) -> {
+                int viewId = getAsIdentifier(resources, values,
+                        RemoteViewsProto.ReflectionAction.VIEW_ID);
+                Object value = null;
+                int parameterType = (int) values.get(
+                        RemoteViewsProto.ReflectionAction.PARAMETER_TYPE);
+                switch (parameterType) {
+                    case BOOLEAN:
+                        value = (boolean) values.get(
+                                RemoteViewsProto.ReflectionAction.BOOLEAN_VALUE, false);
+                        break;
+                    case BYTE:
+                        byte[] bytes = (byte[]) values.get(
+                                RemoteViewsProto.ReflectionAction.BYTE_VALUE);
+                        if (bytes != null && bytes.length > 0) {
+                            value = bytes[0];
+                        }
+                        break;
+                    case SHORT:
+                        value = (short) values.get(RemoteViewsProto.ReflectionAction.SHORT_VALUE,
+                                0);
+                        break;
+                    case INT:
+                        value = (int) values.get(RemoteViewsProto.ReflectionAction.INT_VALUE, 0);
+                        break;
+                    case LONG:
+                        value = (long) values.get(RemoteViewsProto.ReflectionAction.LONG_VALUE, 0);
+                        break;
+                    case FLOAT:
+                        value = (float) values.get(RemoteViewsProto.ReflectionAction.FLOAT_VALUE,
+                                0);
+                        break;
+                    case DOUBLE:
+                        value = (double) values.get(RemoteViewsProto.ReflectionAction.DOUBLE_VALUE,
+                                0);
+                        break;
+                    case CHAR:
+                        value = (char) values.get(RemoteViewsProto.ReflectionAction.CHAR_VALUE, 0);
+                        break;
+                    case STRING:
+                        value = (String) values.get(RemoteViewsProto.ReflectionAction.STRING_VALUE);
+                        break;
+                    case CHAR_SEQUENCE:
+                        value = (CharSequence) values.get(
+                                RemoteViewsProto.ReflectionAction.CHAR_SEQUENCE_VALUE);
+                        break;
+                    case URI:
+                        value = Uri.parse(
+                                (String) values.get(RemoteViewsProto.ReflectionAction.URI_VALUE));
+                        break;
+                    case BITMAP:
+                        value = (Bitmap) values.get(RemoteViewsProto.ReflectionAction.BITMAP_VALUE);
+                        break;
+                    case BLEND_MODE:
+                        value = (BlendMode) values.get(
+                                RemoteViewsProto.ReflectionAction.BLEND_MODE_VALUE);
+                        break;
+                    case COLOR_STATE_LIST:
+                        value = (ColorStateList) values.get(
+                                RemoteViewsProto.ReflectionAction.COLOR_STATE_LIST_VALUE);
+                        break;
+                    case ICON:
+                        value = ((PendingResources<Icon>) values.get(
+                                RemoteViewsProto.ReflectionAction.ICON_VALUE)).create(context,
+                                resources, rootData, depth);
+                        break;
+                    case BUNDLE:
+                    case INTENT:
+                    default:
+                        // omit the action for unsupported parameter types
+                        return null;
+                }
+                return new ReflectionAction(viewId,
+                        (String) values.get(RemoteViewsProto.ReflectionAction.METHOD_NAME),
+                        parameterType, value);
+            };
+        }
     }
 
     private static final class ResourceReflectionAction extends BaseReflectionAction {
@@ -2740,7 +3080,87 @@
         public int getActionTag() {
             return ATTRIBUTE_REFLECTION_ACTION_TAG;
         }
+
+        @Override
+        public boolean canWriteToProto() {
+            return true;
+        }
+
+        @Override
+        public void writeToProto(ProtoOutputStream out, Context context, Resources appResources) {
+            final long token = out.start(RemoteViewsProto.Action.ATTRIBUTE_REFLECTION_ACTION);
+            out.write(RemoteViewsProto.AttributeReflectionAction.VIEW_ID,
+                    appResources.getResourceName(mViewId));
+            out.write(RemoteViewsProto.AttributeReflectionAction.METHOD_NAME, mMethodName);
+            out.write(RemoteViewsProto.AttributeReflectionAction.PARAMETER_TYPE, mType);
+            out.write(RemoteViewsProto.AttributeReflectionAction.RESOURCE_TYPE, mResourceType);
+            if (mAttrId != 0) {
+                out.write(RemoteViewsProto.AttributeReflectionAction.ATTRIBUTE_ID,
+                        appResources.getResourceName(mAttrId));
+            }
+            out.end(token);
+        }
+
+        public static PendingResources<Action> createFromProto(ProtoInputStream in)
+                throws Exception {
+            final LongSparseArray<Object> values = new LongSparseArray<>();
+
+            final long token = in.start(RemoteViewsProto.Action.ATTRIBUTE_REFLECTION_ACTION);
+            while (in.nextField() != NO_MORE_FIELDS) {
+                switch (in.getFieldNumber()) {
+                    case (int) RemoteViewsProto.AttributeReflectionAction.VIEW_ID: {
+                        values.put(RemoteViewsProto.AttributeReflectionAction.VIEW_ID,
+                                in.readString(RemoteViewsProto.AttributeReflectionAction.VIEW_ID));
+                        break;
+                    }
+                    case (int) RemoteViewsProto.AttributeReflectionAction.METHOD_NAME:
+                        values.put(RemoteViewsProto.AttributeReflectionAction.METHOD_NAME,
+                                in.readString(
+                                        RemoteViewsProto.AttributeReflectionAction.METHOD_NAME));
+                        break;
+                    case (int) RemoteViewsProto.AttributeReflectionAction.ATTRIBUTE_ID:
+                        values.put(RemoteViewsProto.AttributeReflectionAction.ATTRIBUTE_ID,
+                                in.readString(
+                                        RemoteViewsProto.AttributeReflectionAction.ATTRIBUTE_ID));
+                        break;
+                    case (int) RemoteViewsProto.AttributeReflectionAction.PARAMETER_TYPE:
+                        values.put(RemoteViewsProto.AttributeReflectionAction.PARAMETER_TYPE,
+                                in.readInt(
+                                        RemoteViewsProto.AttributeReflectionAction.PARAMETER_TYPE));
+                        break;
+                    case (int) RemoteViewsProto.AttributeReflectionAction.RESOURCE_TYPE:
+                        values.put(RemoteViewsProto.AttributeReflectionAction.RESOURCE_TYPE,
+                                in.readInt(
+                                        RemoteViewsProto.AttributeReflectionAction.RESOURCE_TYPE));
+                        break;
+                    default:
+                        Log.w(LOG_TAG, "Unhandled field while reading RemoteViews proto!\n"
+                                + ProtoUtils.currentFieldToString(in));
+                }
+            }
+            in.end(token);
+
+            checkContainsKeys(values, new long[]{RemoteViewsProto.AttributeReflectionAction.VIEW_ID,
+                    RemoteViewsProto.AttributeReflectionAction.METHOD_NAME,
+                    RemoteViewsProto.AttributeReflectionAction.PARAMETER_TYPE,
+                    RemoteViewsProto.AttributeReflectionAction.RESOURCE_TYPE});
+
+            return (context, resources, rootData, depth) -> {
+                int viewId = getAsIdentifier(resources, values,
+                        RemoteViewsProto.AttributeReflectionAction.VIEW_ID);
+                int attributeId = (values.indexOfKey(
+                        RemoteViewsProto.AttributeReflectionAction.ATTRIBUTE_ID) >= 0)
+                        ? getAsIdentifier(resources, values,
+                        RemoteViewsProto.AttributeReflectionAction.ATTRIBUTE_ID) : 0;
+                return new AttributeReflectionAction(viewId,
+                        (String) values.get(RemoteViewsProto.AttributeReflectionAction.METHOD_NAME),
+                        (int) values.get(RemoteViewsProto.AttributeReflectionAction.PARAMETER_TYPE),
+                        (int) values.get(RemoteViewsProto.AttributeReflectionAction.RESOURCE_TYPE),
+                        attributeId);
+            };
+        }
     }
+
     private static final class ComplexUnitDimensionReflectionAction extends BaseReflectionAction {
         private final float mValue;
         @ComplexDimensionUnit
@@ -2794,6 +3214,101 @@
         public int getActionTag() {
             return COMPLEX_UNIT_DIMENSION_REFLECTION_ACTION_TAG;
         }
+
+        @Override
+        public boolean canWriteToProto() {
+            return true;
+        }
+
+        @Override
+        public void writeToProto(ProtoOutputStream out, Context context, Resources appResources) {
+            final long token = out.start(
+                    RemoteViewsProto.Action.COMPLEX_UNIT_DIMENSION_REFLECTION_ACTION);
+            out.write(RemoteViewsProto.ComplexUnitDimensionReflectionAction.VIEW_ID,
+                    appResources.getResourceName(mViewId));
+            out.write(RemoteViewsProto.ComplexUnitDimensionReflectionAction.METHOD_NAME,
+                    mMethodName);
+            out.write(RemoteViewsProto.ComplexUnitDimensionReflectionAction.PARAMETER_TYPE, mType);
+            out.write(RemoteViewsProto.ComplexUnitDimensionReflectionAction.DIMENSION_VALUE,
+                    mValue);
+            out.write(RemoteViewsProto.ComplexUnitDimensionReflectionAction.UNIT, mUnit);
+            out.end(token);
+        }
+
+        public static PendingResources<Action> createFromProto(ProtoInputStream in)
+                throws Exception {
+            final LongSparseArray<Object> values = new LongSparseArray<>();
+
+            final long token = in.start(
+                    RemoteViewsProto.Action.COMPLEX_UNIT_DIMENSION_REFLECTION_ACTION);
+            while (in.nextField() != NO_MORE_FIELDS) {
+                switch (in.getFieldNumber()) {
+                    case (int) RemoteViewsProto.ComplexUnitDimensionReflectionAction.VIEW_ID:
+                        values.put(RemoteViewsProto.ComplexUnitDimensionReflectionAction.VIEW_ID,
+                                in.readString(
+                                        RemoteViewsProto
+                                                .ComplexUnitDimensionReflectionAction.VIEW_ID));
+                        break;
+                    case (int) RemoteViewsProto.ComplexUnitDimensionReflectionAction.METHOD_NAME:
+                        values.put(
+                                RemoteViewsProto.ComplexUnitDimensionReflectionAction.METHOD_NAME,
+                                in.readString(
+                                        RemoteViewsProto
+                                                .ComplexUnitDimensionReflectionAction.METHOD_NAME));
+                        break;
+                    case (int) RemoteViewsProto.ComplexUnitDimensionReflectionAction.PARAMETER_TYPE:
+                        values.put(
+                                RemoteViewsProto
+                                        .ComplexUnitDimensionReflectionAction.PARAMETER_TYPE,
+                                in.readInt(
+                                        RemoteViewsProto
+                                                .ComplexUnitDimensionReflectionAction
+                                                .PARAMETER_TYPE));
+                        break;
+                    case (int) RemoteViewsProto
+                            .ComplexUnitDimensionReflectionAction.DIMENSION_VALUE:
+                        values.put(
+                                RemoteViewsProto
+                                        .ComplexUnitDimensionReflectionAction.DIMENSION_VALUE,
+                                in.readFloat(
+                                        RemoteViewsProto
+                                                .ComplexUnitDimensionReflectionAction
+                                                .DIMENSION_VALUE));
+                        break;
+                    case (int) RemoteViewsProto.ComplexUnitDimensionReflectionAction.UNIT:
+                        values.put(RemoteViewsProto.ComplexUnitDimensionReflectionAction.UNIT,
+                                in.readInt(
+                                        RemoteViewsProto
+                                                .ComplexUnitDimensionReflectionAction.UNIT));
+                        break;
+                    default:
+                        Log.w(LOG_TAG, "Unhandled field while reading RemoteViews proto!\n"
+                                + ProtoUtils.currentFieldToString(in));
+                }
+            }
+            in.end(token);
+
+            checkContainsKeys(values,
+                    new long[]{RemoteViewsProto.ComplexUnitDimensionReflectionAction.VIEW_ID,
+                            RemoteViewsProto.ComplexUnitDimensionReflectionAction.METHOD_NAME,
+                            RemoteViewsProto.ComplexUnitDimensionReflectionAction.PARAMETER_TYPE});
+
+            return (context, resources, rootData, depth) -> {
+                int viewId = getAsIdentifier(resources, values,
+                        RemoteViewsProto.ComplexUnitDimensionReflectionAction.VIEW_ID);
+                return new ComplexUnitDimensionReflectionAction(viewId, (String) values.get(
+                        RemoteViewsProto.ComplexUnitDimensionReflectionAction.METHOD_NAME),
+                        (int) values.get(
+                                RemoteViewsProto
+                                        .ComplexUnitDimensionReflectionAction.PARAMETER_TYPE),
+                        (float) values.get(
+                                RemoteViewsProto
+                                        .ComplexUnitDimensionReflectionAction.DIMENSION_VALUE,
+                                0),
+                        (int) values.get(RemoteViewsProto.ComplexUnitDimensionReflectionAction.UNIT,
+                                0));
+            };
+        }
     }
 
     private static final class NightModeReflectionAction extends BaseReflectionAction {
@@ -2868,6 +3383,145 @@
                 visitIconUri((Icon) mLightValue, visitor);
             }
         }
+
+        @Override
+        public boolean canWriteToProto() {
+            return true;
+        }
+
+        @Override
+        public void writeToProto(ProtoOutputStream out, Context context, Resources appResources) {
+            final long token = out.start(RemoteViewsProto.Action.NIGHT_MODE_REFLECTION_ACTION);
+            out.write(RemoteViewsProto.NightModeReflectionAction.VIEW_ID,
+                    appResources.getResourceName(mViewId));
+            out.write(RemoteViewsProto.NightModeReflectionAction.METHOD_NAME, mMethodName);
+            out.write(RemoteViewsProto.NightModeReflectionAction.PARAMETER_TYPE, mType);
+            switch (this.mType) {
+                case ICON:
+                    writeIconToProto(out, appResources, (Icon) mLightValue,
+                            RemoteViewsProto.NightModeReflectionAction.LIGHT_ICON);
+                    writeIconToProto(out, appResources, (Icon) mDarkValue,
+                            RemoteViewsProto.NightModeReflectionAction.DARK_ICON);
+                    break;
+                case COLOR_STATE_LIST:
+                    writeColorStateListToProto(out, (ColorStateList) mLightValue,
+                            RemoteViewsProto.NightModeReflectionAction.LIGHT_COLOR_STATE_LIST);
+                    writeColorStateListToProto(out, (ColorStateList) mDarkValue,
+                            RemoteViewsProto.NightModeReflectionAction.DARK_COLOR_STATE_LIST);
+                    break;
+                case INT:
+                    out.write(RemoteViewsProto.NightModeReflectionAction.LIGHT_INT,
+                            (int) mLightValue);
+                    out.write(RemoteViewsProto.NightModeReflectionAction.DARK_INT,
+                            (int) mDarkValue);
+                    break;
+            }
+            out.end(token);
+        }
+
+        public static PendingResources<Action> createFromProto(ProtoInputStream in)
+                throws Exception {
+            final LongSparseArray<Object> values = new LongSparseArray<>();
+
+            final long token = in.start(RemoteViewsProto.Action.NIGHT_MODE_REFLECTION_ACTION);
+            while (in.nextField() != NO_MORE_FIELDS) {
+                switch (in.getFieldNumber()) {
+                    case (int) RemoteViewsProto.NightModeReflectionAction.VIEW_ID:
+                        values.put(RemoteViewsProto.NightModeReflectionAction.VIEW_ID,
+                                in.readString(RemoteViewsProto.NightModeReflectionAction.VIEW_ID));
+                        break;
+                    case (int) RemoteViewsProto.NightModeReflectionAction.METHOD_NAME:
+                        values.put(RemoteViewsProto.NightModeReflectionAction.METHOD_NAME,
+                                in.readString(
+                                        RemoteViewsProto.NightModeReflectionAction.METHOD_NAME));
+                        break;
+                    case (int) RemoteViewsProto.NightModeReflectionAction.PARAMETER_TYPE:
+                        values.put(RemoteViewsProto.NightModeReflectionAction.PARAMETER_TYPE,
+                                in.readInt(
+                                        RemoteViewsProto.NightModeReflectionAction.PARAMETER_TYPE));
+                        break;
+                    case (int) RemoteViewsProto.NightModeReflectionAction.LIGHT_ICON:
+                        values.put(RemoteViewsProto.NightModeReflectionAction.LIGHT_ICON,
+                                createIconFromProto(in,
+                                        RemoteViewsProto.NightModeReflectionAction.LIGHT_ICON));
+                        break;
+                    case (int) RemoteViewsProto.NightModeReflectionAction.LIGHT_COLOR_STATE_LIST:
+                        values.put(
+                                RemoteViewsProto.NightModeReflectionAction.LIGHT_COLOR_STATE_LIST,
+                                createColorStateListFromProto(in,
+                                        RemoteViewsProto
+                                                .NightModeReflectionAction.LIGHT_COLOR_STATE_LIST));
+                        break;
+                    case (int) RemoteViewsProto.NightModeReflectionAction.LIGHT_INT:
+                        values.put(RemoteViewsProto.NightModeReflectionAction.LIGHT_INT,
+                                in.readInt(RemoteViewsProto.NightModeReflectionAction.LIGHT_INT));
+                        break;
+                    case (int) RemoteViewsProto.NightModeReflectionAction.DARK_ICON:
+                        values.put(RemoteViewsProto.NightModeReflectionAction.DARK_ICON,
+                                createIconFromProto(in,
+                                        RemoteViewsProto.NightModeReflectionAction.DARK_ICON));
+                        break;
+                    case (int) RemoteViewsProto.NightModeReflectionAction.DARK_COLOR_STATE_LIST:
+                        values.put(RemoteViewsProto.NightModeReflectionAction.DARK_COLOR_STATE_LIST,
+                                createColorStateListFromProto(in,
+                                        RemoteViewsProto
+                                                .NightModeReflectionAction.DARK_COLOR_STATE_LIST));
+                        break;
+                    case (int) RemoteViewsProto.NightModeReflectionAction.DARK_INT:
+                        values.put(RemoteViewsProto.NightModeReflectionAction.DARK_INT,
+                                in.readInt(RemoteViewsProto.NightModeReflectionAction.DARK_INT));
+                        break;
+                    default:
+                        Log.w(LOG_TAG, "Unhandled field while reading RemoteViews proto!\n"
+                                + ProtoUtils.currentFieldToString(in));
+                }
+            }
+            in.end(token);
+
+            checkContainsKeys(values, new long[]{RemoteViewsProto.NightModeReflectionAction.VIEW_ID,
+                    RemoteViewsProto.NightModeReflectionAction.METHOD_NAME,
+                    RemoteViewsProto.NightModeReflectionAction.PARAMETER_TYPE});
+
+            return (context, resources, rootData, depth) -> {
+                int viewId = getAsIdentifier(resources, values,
+                        RemoteViewsProto.NightModeReflectionAction.VIEW_ID);
+                String methodName = (String) values.get(
+                        RemoteViewsProto.NightModeReflectionAction.METHOD_NAME);
+                int parameterType = (int) values.get(
+                        RemoteViewsProto.NightModeReflectionAction.PARAMETER_TYPE);
+                switch (parameterType) {
+                    case ICON:
+                        PendingResources<Icon> pendingLightIcon =
+                                (PendingResources<Icon>) values.get(
+                                        RemoteViewsProto.NightModeReflectionAction.LIGHT_ICON);
+                        PendingResources<Icon> pendingDarkIcon =
+                                (PendingResources<Icon>) values.get(
+                                        RemoteViewsProto.NightModeReflectionAction.DARK_ICON);
+                        Icon lightIcon = pendingLightIcon != null ? pendingLightIcon.create(context,
+                                resources, rootData, depth) : null;
+                        Icon darkIcon = pendingDarkIcon != null ? pendingDarkIcon.create(context,
+                                resources, rootData, depth) : null;
+                        return new NightModeReflectionAction(viewId, methodName, parameterType,
+                                lightIcon, darkIcon);
+                    case COLOR_STATE_LIST:
+                        return new NightModeReflectionAction(viewId, methodName, parameterType,
+                                (ColorStateList) values.get(
+                                        RemoteViewsProto
+                                                .NightModeReflectionAction.LIGHT_COLOR_STATE_LIST),
+                                (ColorStateList) values.get(
+                                        RemoteViewsProto
+                                                .NightModeReflectionAction.DARK_COLOR_STATE_LIST));
+                    case INT:
+                        return new NightModeReflectionAction(viewId, methodName, parameterType,
+                                (int) values.get(
+                                        RemoteViewsProto.NightModeReflectionAction.LIGHT_INT, 0),
+                                (int) values.get(
+                                        RemoteViewsProto.NightModeReflectionAction.DARK_INT, 0));
+                    default:
+                        throw new RuntimeException("Unknown parameterType: " + parameterType);
+                }
+            };
+        }
     }
 
     /**
@@ -3353,6 +4007,46 @@
         public int mergeBehavior() {
             return MERGE_APPEND;
         }
+
+        @Override
+        public boolean canWriteToProto() {
+            return true;
+        }
+
+        @Override
+        public void writeToProto(ProtoOutputStream out, Context context, Resources appResources) {
+            final long token = out.start(RemoteViewsProto.Action.REMOVE_FROM_PARENT_ACTION);
+            out.write(RemoteViewsProto.RemoveFromParentAction.VIEW_ID,
+                    appResources.getResourceName(mViewId));
+            out.end(token);
+        }
+
+        public static PendingResources<Action> createFromProto(ProtoInputStream in)
+                throws Exception {
+            final LongSparseArray<Object> values = new LongSparseArray<>();
+
+            final long token = in.start(RemoteViewsProto.Action.REMOVE_FROM_PARENT_ACTION);
+            while (in.nextField() != NO_MORE_FIELDS) {
+                switch (in.getFieldNumber()) {
+                    case (int) RemoteViewsProto.RemoveFromParentAction.VIEW_ID:
+                        values.put(RemoteViewsProto.RemoveFromParentAction.VIEW_ID,
+                                in.readString(RemoteViewsProto.RemoveFromParentAction.VIEW_ID));
+                        break;
+                    default:
+                        Log.w(LOG_TAG, "Unhandled field while reading RemoteViews proto!\n"
+                                + ProtoUtils.currentFieldToString(in));
+                }
+            }
+            in.end(token);
+
+            checkContainsKeys(values, new long[]{RemoteViewsProto.RemoveFromParentAction.VIEW_ID});
+
+            return (context, resources, rootData, depth) -> {
+                int viewId = getAsIdentifier(resources, values,
+                        RemoteViewsProto.RemoveFromParentAction.VIEW_ID);
+                return new RemoveFromParentAction(viewId);
+            };
+        }
     }
 
     /**
@@ -3768,6 +4462,64 @@
         public String getUniqueKey() {
             return super.getUniqueKey() + mProperty;
         }
+
+        @Override
+        public boolean canWriteToProto() {
+            return true;
+        }
+
+        @Override
+        public void writeToProto(ProtoOutputStream out, Context context, Resources appResources) {
+            final long token = out.start(RemoteViewsProto.Action.LAYOUT_PARAM_ACTION);
+            out.write(RemoteViewsProto.LayoutParamAction.VIEW_ID,
+                    appResources.getResourceName(mViewId));
+            out.write(RemoteViewsProto.LayoutParamAction.PROPERTY, mProperty);
+            out.write(RemoteViewsProto.LayoutParamAction.LAYOUT_VALUE, mValue);
+            out.write(RemoteViewsProto.LayoutParamAction.VALUE_TYPE, mValueType);
+            out.end(token);
+        }
+
+        public static PendingResources<Action> createFromProto(ProtoInputStream in)
+                throws Exception {
+            final LongSparseArray<Object> values = new LongSparseArray<>();
+
+            final long token = in.start(RemoteViewsProto.Action.LAYOUT_PARAM_ACTION);
+            while (in.nextField() != NO_MORE_FIELDS) {
+                switch (in.getFieldNumber()) {
+                    case (int) RemoteViewsProto.LayoutParamAction.VIEW_ID:
+                        values.put(RemoteViewsProto.LayoutParamAction.VIEW_ID,
+                                in.readString(RemoteViewsProto.LayoutParamAction.VIEW_ID));
+                        break;
+                    case (int) RemoteViewsProto.LayoutParamAction.PROPERTY:
+                        values.put(RemoteViewsProto.LayoutParamAction.PROPERTY,
+                                in.readInt(RemoteViewsProto.LayoutParamAction.PROPERTY));
+                        break;
+                    case (int) RemoteViewsProto.LayoutParamAction.LAYOUT_VALUE:
+                        values.put(RemoteViewsProto.LayoutParamAction.LAYOUT_VALUE,
+                                in.readInt(RemoteViewsProto.LayoutParamAction.LAYOUT_VALUE));
+                        break;
+                    case (int) RemoteViewsProto.LayoutParamAction.VALUE_TYPE:
+                        values.put(RemoteViewsProto.LayoutParamAction.VALUE_TYPE,
+                                in.readInt(RemoteViewsProto.LayoutParamAction.VALUE_TYPE));
+                        break;
+                    default:
+                        Log.w(LOG_TAG, "Unhandled field while reading RemoteViews proto!\n"
+                                + ProtoUtils.currentFieldToString(in));
+                }
+            }
+            in.end(token);
+
+            checkContainsKeys(values, new long[]{RemoteViewsProto.LayoutParamAction.VIEW_ID});
+
+            return (context, resources, rootData, depth) -> {
+                int viewId = getAsIdentifier(resources, values,
+                        RemoteViewsProto.LayoutParamAction.VIEW_ID);
+                return new LayoutParamAction(viewId,
+                        (int) values.get(RemoteViewsProto.LayoutParamAction.PROPERTY, 0),
+                        (int) values.get(RemoteViewsProto.LayoutParamAction.LAYOUT_VALUE, 0),
+                        (int) values.get(RemoteViewsProto.LayoutParamAction.VALUE_TYPE, 0));
+            };
+        }
     }
 
     /**
@@ -7668,6 +8420,7 @@
             values.put(RemoteViewsProto.RemoteCollectionItems.IDS, new ArrayList<Long>());
             values.put(RemoteViewsProto.RemoteCollectionItems.VIEWS,
                     new ArrayList<PendingResources<RemoteViews>>());
+
             while (in.nextField() != NO_MORE_FIELDS) {
                 switch (in.getFieldNumber()) {
                     case (int) RemoteViewsProto.RemoteCollectionItems.IDS:
@@ -7704,6 +8457,7 @@
 
             checkContainsKeys(values,
                     new long[]{RemoteViewsProto.RemoteCollectionItems.VIEW_TYPE_COUNT});
+
             return (context, resources, rootData, depth) -> {
                 List<Long> idList = (List<Long>) values.get(
                         RemoteViewsProto.RemoteCollectionItems.IDS);
@@ -8149,6 +8903,16 @@
                 out.write(SizeFProto.HEIGHT, mIdealSize.getHeight());
                 out.end(token);
             }
+
+            if (mActions != null) {
+                for (Action action : mActions) {
+                    if (action.canWriteToProto()) {
+                        final long token = out.start(RemoteViewsProto.ACTIONS);
+                        action.writeToProto(out, context, appResources);
+                        out.end(token);
+                    }
+                }
+            }
         } else if (hasSizedRemoteViews()) {
             out.write(RemoteViewsProto.MODE, MODE_HAS_SIZED_REMOTEVIEWS);
             for (RemoteViews view : mSizedRemoteViews) {
@@ -8192,6 +8956,7 @@
             String mLayoutResName = null;
             String mLightBackgroundResName = null;
             String mViewResName = null;
+            final List<PendingResources<Action>> mActions = new ArrayList<>();
             final List<PendingResources<RemoteViews>> mSizedRemoteViews = new ArrayList<>();
             PendingResources<RemoteViews> mLandscapeViews = null;
             PendingResources<RemoteViews> mPortraitViews = null;
@@ -8230,6 +8995,14 @@
                     case (int) RemoteViewsProto.PROVIDER_INSTANCE_ID:
                         ref.mProviderInstanceId = in.readInt(RemoteViewsProto.PROVIDER_INSTANCE_ID);
                         break;
+                    case (int) RemoteViewsProto.ACTIONS:
+                        final long actionsToken = in.start(RemoteViewsProto.ACTIONS);
+                        final PendingResources<Action> action = createActionFromProto(ref.mRv, in);
+                        if (action != null) {
+                            ref.mActions.add(action);
+                        }
+                        in.end(actionsToken);
+                        break;
                     case (int) RemoteViewsProto.SIZED_REMOTEVIEWS:
                         final long sizedToken = in.start(RemoteViewsProto.SIZED_REMOTEVIEWS);
                         ref.mSizedRemoteViews.add(createFromProto(in));
@@ -8328,19 +9101,27 @@
                 }
             }
             if (ref.mPopulateRemoteCollectionCache != null) {
-                ref.mPopulateRemoteCollectionCache.create(context, resources, rootData, depth);
+                ref.mPopulateRemoteCollectionCache.create(appContext, appResources, rootData,
+                        depth);
             }
             if (ref.mProviderInstanceId != -1) {
                 rv.mProviderInstanceId = ref.mProviderInstanceId;
             }
             if (ref.mMode == MODE_NORMAL) {
                 rv.setIdealSize(ref.mIdealSize);
+                for (PendingResources<Action> pendingAction : ref.mActions) {
+                    Action action = pendingAction.create(appContext, appResources, rootData, depth);
+                    if (action != null) {
+                        rv.addAction(action);
+                    }
+                }
                 return rv;
             } else if (ref.mMode == MODE_HAS_SIZED_REMOTEVIEWS) {
                 List<RemoteViews> sizedViews = new ArrayList<>();
                 for (RemoteViews.PendingResources<RemoteViews> pendingViews :
                         ref.mSizedRemoteViews) {
-                    RemoteViews views = pendingViews.create(context, resources, rootData, depth);
+                    RemoteViews views = pendingViews.create(appContext, appResources, rootData,
+                            depth);
                     sizedViews.add(views);
                 }
                 rv.initializeSizedRemoteViews(sizedViews.iterator());
@@ -8349,8 +9130,8 @@
                 checkProtoResultNotNull(ref.mLandscapeViews, "Missing landscape views");
                 checkProtoResultNotNull(ref.mPortraitViews, "Missing portrait views");
                 RemoteViews parentRv = new RemoteViews(
-                        ref.mLandscapeViews.create(context, resources, rootData, depth),
-                        ref.mPortraitViews.create(context, resources, rootData, depth));
+                        ref.mLandscapeViews.create(appContext, appResources, rootData, depth),
+                        ref.mPortraitViews.create(appContext, appResources, rootData, depth));
                 parentRv.initializeFrom(/* src= */ rv, /* hierarchyRoot= */ rv);
                 return parentRv;
             } else {
@@ -8370,6 +9151,35 @@
                 throws Exception;
     }
 
+    @Nullable
+    private static PendingResources<Action> createActionFromProto(RemoteViews rv,
+            ProtoInputStream in) throws Exception {
+        int actionFieldId = in.nextField();
+        if (actionFieldId == NO_MORE_FIELDS) {
+            // action was omitted
+            return null;
+        }
+        switch (actionFieldId) {
+            case (int) RemoteViewsProto.Action.ATTRIBUTE_REFLECTION_ACTION:
+                return AttributeReflectionAction.createFromProto(in);
+            case (int) RemoteViewsProto.Action.BITMAP_REFLECTION_ACTION:
+                return rv.createFromBitmapReflectionActionFromProto(in);
+            case (int) RemoteViewsProto.Action.COMPLEX_UNIT_DIMENSION_REFLECTION_ACTION:
+                return ComplexUnitDimensionReflectionAction.createFromProto(in);
+            case (int) RemoteViewsProto.Action.LAYOUT_PARAM_ACTION:
+                return LayoutParamAction.createFromProto(in);
+            case (int) RemoteViewsProto.Action.NIGHT_MODE_REFLECTION_ACTION:
+                return NightModeReflectionAction.createFromProto(in);
+            case (int) RemoteViewsProto.Action.REFLECTION_ACTION:
+                return ReflectionAction.createFromProto(in);
+            case (int) RemoteViewsProto.Action.REMOVE_FROM_PARENT_ACTION:
+                return RemoveFromParentAction.createFromProto(in);
+            default:
+                throw new RuntimeException("Unhandled field while reading Action proto!\n"
+                        + ProtoUtils.currentFieldToString(in));
+        }
+    }
+
     private static void checkValidResource(int id, String message, String resName)
             throws Exception {
         if (id == 0) throw new Exception(message + ": " + resName);
@@ -8392,6 +9202,22 @@
         }
     }
 
+    private static int getAsIdentifier(Resources resources, LongSparseArray<?> array, long fieldId)
+            throws Exception {
+        String resName = (String) array.get(fieldId);
+        int id = resources.getIdentifier(resName, /* defType= */ null, /* defPackage= */ null);
+        checkValidResource(id, "Invalid id", resName);
+        return id;
+    }
+
+    private static int getAsIdentifier(Resources resources, SparseArray<?> array, int key)
+            throws Exception {
+        String resName = (String) array.get(key);
+        int id = resources.getIdentifier(resName, /* defType= */ null, /* defPackage= */ null);
+        checkValidResource(id, "Invalid id", resName);
+        return id;
+    }
+
     private static SizeF createSizeFFromProto(ProtoInputStream in) throws Exception {
         float width = 0;
         float height = 0;
@@ -8411,4 +9237,43 @@
 
         return new SizeF(width, height);
     }
+
+    private static void writeIconToProto(ProtoOutputStream out, Resources appResources, Icon icon,
+            long fieldId) {
+        long token = out.start(fieldId);
+        RemoteViewsSerializers.writeIconToProto(out, appResources, icon);
+        out.end(token);
+    }
+
+    private static PendingResources<Icon> createIconFromProto(ProtoInputStream in, long fieldId)
+            throws Exception {
+        long token = in.start(fieldId);
+        Function<Resources, Icon> icon = RemoteViewsSerializers.createIconFromProto(in);
+        in.end(token);
+        return (context, resources, rootData, depth) -> icon.apply(resources);
+    }
+
+    private static void writeColorStateListToProto(ProtoOutputStream out,
+            ColorStateList colorStateList, long fieldId) {
+        long token = out.start(fieldId);
+        colorStateList.writeToProto(out);
+        out.end(token);
+    }
+
+    private static ColorStateList createColorStateListFromProto(ProtoInputStream in, long fieldId)
+            throws Exception {
+        long token = in.start(fieldId);
+        ColorStateList colorStateList = ColorStateList.createFromProto(in);
+        in.end(token);
+        return colorStateList;
+    }
+
+    private static CharSequence createCharSequenceFromProto(ProtoInputStream in, long fieldId)
+            throws Exception {
+        long token = in.start(fieldId);
+        CharSequence cs = RemoteViewsSerializers.createCharSequenceFromProto(in);
+        in.end(token);
+        return cs;
+    }
+
 }
diff --git a/core/proto/android/widget/remoteviews.proto b/core/proto/android/widget/remoteviews.proto
index 5892396..47c97b0 100644
--- a/core/proto/android/widget/remoteviews.proto
+++ b/core/proto/android/widget/remoteviews.proto
@@ -54,6 +54,7 @@
     optional bool has_draw_instructions = 13;
     repeated bytes bitmap_cache = 14;
     optional RemoteCollectionCache remote_collection_cache = 15;
+    repeated Action actions = 16;
 
     message RemoteCollectionCache {
         message Entry {
@@ -288,6 +289,91 @@
             }
         }
     }
+
+    message Action {
+        oneof action {
+            AttributeReflectionAction attribute_reflection_action = 1;
+            BitmapReflectionAction bitmap_reflection_action = 2;
+            ComplexUnitDimensionReflectionAction complex_unit_dimension_reflection_action = 3;
+            LayoutParamAction layout_param_action = 4;
+            NightModeReflectionAction night_mode_reflection_action = 5;
+            ReflectionAction reflection_action = 6;
+            RemoveFromParentAction remove_from_parent_action = 7;
+        }
+    }
+
+    message AttributeReflectionAction {
+        optional string view_id = 1;
+        optional string method_name = 2;
+        optional int32 parameter_type = 3;
+        optional int32 resource_type = 4;
+        optional string attribute_id = 5;
+    }
+
+    message BitmapReflectionAction {
+        optional string view_id = 1;
+        optional string method_name = 2;
+        optional int32 bitmap_id = 3;
+    }
+
+    message ComplexUnitDimensionReflectionAction {
+        optional string view_id = 1;
+        optional string method_name = 2;
+        optional int32 parameter_type = 3;
+        optional float dimension_value = 4;
+        optional int32 unit = 5;
+    }
+
+    message LayoutParamAction {
+        optional string view_id = 1;
+        optional int32 property = 2;
+        optional int32 layout_value = 3;
+        optional int32 value_type = 4;
+    }
+
+    message NightModeReflectionAction {
+        optional string view_id = 1;
+        optional string method_name = 2;
+        optional int32 parameter_type = 3;
+        oneof light {
+            Icon light_icon = 4;
+            android.content.res.ColorStateListProto light_color_state_list = 5;
+            int32 light_int = 6;
+        }
+        oneof dark {
+            Icon dark_icon = 7;
+            android.content.res.ColorStateListProto dark_color_state_list = 8;
+            int32 dark_int = 9;
+        }
+    }
+
+    message ReflectionAction {
+        optional string view_id = 1;
+        optional string method_name = 2;
+        optional int32 parameter_type = 3;
+        oneof reflection_value {
+            bool boolean_value = 4;
+            bytes byte_value = 5;
+            int32 short_value = 6;
+            int32 int_value = 7;
+            int64 long_value = 8;
+            float float_value = 9;
+            double double_value = 10;
+            int32 char_value = 11;
+            string string_value = 12;
+            CharSequence char_sequence_value = 13;
+            string uri_value = 14;
+            bytes bitmap_value = 15;
+            android.content.res.ColorStateListProto color_state_list_value = 16;
+            Icon icon_value = 17;
+            int32 blend_mode_value = 18;
+            // Intent and Bundle values are excluded.
+        }
+    }
+
+    message RemoveFromParentAction {
+        optional string view_id = 1;
+    }
 }
 
 
diff --git a/core/tests/coretests/src/android/widget/RemoteViewsProtoTest.java b/core/tests/coretests/src/android/widget/RemoteViewsProtoTest.java
deleted file mode 100644
index 7c14032..0000000
--- a/core/tests/coretests/src/android/widget/RemoteViewsProtoTest.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * 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.widget;
-
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-
-import android.content.Context;
-import android.util.SizeF;
-import android.util.proto.ProtoInputStream;
-import android.util.proto.ProtoOutputStream;
-import android.view.View;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.frameworks.coretests.R;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.runner.RunWith;
-
-import java.util.Map;
-
-/**
- * Tests for RemoteViews.
- */
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class RemoteViewsProtoTest {
-
-    // This can point to any other package which exists on the device.
-    private static final String OTHER_PACKAGE = "com.android.systemui";
-
-    @Rule
-    public final ExpectedException exception = ExpectedException.none();
-
-    private Context mContext;
-    private String mPackage;
-    private LinearLayout mContainer;
-
-    @Before
-    public void setup() {
-        mContext = InstrumentationRegistry.getContext();
-        mPackage = mContext.getPackageName();
-        mContainer = new LinearLayout(mContext);
-    }
-
-    @Test
-    public void copy_canStillBeApplied() {
-        RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
-
-        RemoteViews clone = recreateFromProto(original);
-
-        clone.apply(mContext, mContainer);
-    }
-
-    @SuppressWarnings("ReturnValueIgnored")
-    @Test
-    public void clone_repeatedly() {
-        RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
-
-        recreateFromProto(original);
-        recreateFromProto(original);
-
-        original.apply(mContext, mContainer);
-    }
-
-    @Test
-    public void clone_chained() {
-        RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
-
-        RemoteViews clone = recreateFromProto(recreateFromProto(original));
-
-
-        clone.apply(mContext, mContainer);
-    }
-
-    @Test
-    public void landscapePortraitViews_lightBackgroundLayoutFlag() {
-        RemoteViews inner = new RemoteViews(mPackage, R.layout.remote_views_text);
-        inner.setLightBackgroundLayoutId(R.layout.remote_views_light_background_text);
-
-        RemoteViews parent = new RemoteViews(inner, inner);
-        parent.addFlags(RemoteViews.FLAG_USE_LIGHT_BACKGROUND_LAYOUT);
-
-        View view = recreateFromProto(parent).apply(mContext, mContainer);
-        assertNull(view.findViewById(R.id.text));
-        assertNotNull(view.findViewById(R.id.light_background_text));
-    }
-
-    @Test
-    public void sizedViews_lightBackgroundLayoutFlag() {
-        RemoteViews inner = new RemoteViews(mPackage, R.layout.remote_views_text);
-        inner.setLightBackgroundLayoutId(R.layout.remote_views_light_background_text);
-
-        RemoteViews parent = new RemoteViews(
-                Map.of(new SizeF(0, 0), inner, new SizeF(100, 100), inner));
-        parent.addFlags(RemoteViews.FLAG_USE_LIGHT_BACKGROUND_LAYOUT);
-
-        View view = recreateFromProto(parent).apply(mContext, mContainer);
-        assertNull(view.findViewById(R.id.text));
-        assertNotNull(view.findViewById(R.id.light_background_text));
-    }
-
-    @Test
-    public void nestedLandscapeViews() throws Exception {
-        RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test);
-        for (int i = 0; i < 10; i++) {
-            views = new RemoteViews(views, new RemoteViews(mPackage, R.layout.remote_views_test));
-        }
-        // writeTo/createFromProto works
-        recreateFromProto(views);
-
-        views = new RemoteViews(mPackage, R.layout.remote_views_test);
-        for (int i = 0; i < 11; i++) {
-            views = new RemoteViews(views, new RemoteViews(mPackage, R.layout.remote_views_test));
-        }
-        // writeTo/createFromProto fails
-        exception.expect(IllegalArgumentException.class);
-        recreateFromProtoNoRethrow(views);
-    }
-
-    private RemoteViews recreateFromProto(RemoteViews views) {
-        try {
-            return recreateFromProtoNoRethrow(views);
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    private RemoteViews recreateFromProtoNoRethrow(RemoteViews views) throws Exception {
-        ProtoOutputStream out = new ProtoOutputStream();
-        views.writePreviewToProto(mContext, out);
-        ProtoInputStream in = new ProtoInputStream(out.getBytes());
-        return RemoteViews.createPreviewFromProto(mContext, in);
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl
index ccfaab0..964e5fd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.recents;
 
-import android.app.ActivityManager;
 import android.graphics.GraphicBuffer;
 import android.view.IRemoteAnimationFinishedCallback;
 import android.view.RemoteAnimationTarget;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl
index 8021758..32c79a2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.recents;
 
-import android.app.ActivityManager;
 import android.graphics.Rect;
 import android.view.RemoteAnimationTarget;
 import android.window.TaskSnapshot;
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
index 8e0cdf8..712ddc8 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
@@ -85,7 +85,7 @@
 
     public TestModeBuilder(ZenMode previous) {
         mId = previous.getId();
-        mRule = previous.getRule();
+        mRule = new AutomaticZenRule.Builder(previous.getRule()).build();
 
         mConfigZenRule = new ZenModeConfig.ZenRule();
         mConfigZenRule.enabled = previous.getRule().isEnabled();
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index ad14035..02e8cd6 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -247,13 +247,6 @@
 }
 
 flag {
-    name: "haptic_brightness_slider"
-    namespace: "systemui"
-    description: "Adds haptic feedback to the brightness slider."
-    bug: "296467915"
-}
-
-flag {
     name: "unfold_animation_background_progress"
     namespace: "systemui"
     description: "Moves unfold animation progress calculation to a background thread"
@@ -1401,4 +1394,4 @@
    namespace: "systemui"
    description: "Allow non-touchscreen devices to bypass falsing"
    bug: "319809270"
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaContentPicker.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaContentPicker.kt
index 5dccb68..d523232 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaContentPicker.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaContentPicker.kt
@@ -22,29 +22,32 @@
 import com.android.compose.animation.scene.SceneTransitionLayoutState
 import com.android.compose.animation.scene.StaticElementContentPicker
 import com.android.compose.animation.scene.content.state.TransitionState
+import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.shared.flag.DualShade
 
 /** [ElementContentPicker] implementation for the media carousel object. */
 object MediaContentPicker : StaticElementContentPicker {
 
     override val contents =
         setOf(
+            Overlays.NotificationsShade,
+            Overlays.QuickSettingsShade,
             Scenes.Lockscreen,
             Scenes.Shade,
             Scenes.QuickSettings,
-            Scenes.QuickSettingsShade,
-            Scenes.Communal
+            Scenes.Communal,
         )
 
     override fun contentDuringTransition(
         element: ElementKey,
         transition: TransitionState.Transition,
         fromContentZIndex: Float,
-        toContentZIndex: Float
+        toContentZIndex: Float,
     ): ContentKey {
         return when {
             shouldElevateMedia(transition) -> {
-                Scenes.Shade
+                if (DualShade.isEnabled) Overlays.NotificationsShade else Scenes.Shade
             }
             transition.isTransitioningBetween(Scenes.Lockscreen, Scenes.Communal) -> {
                 Scenes.Lockscreen
@@ -52,6 +55,12 @@
             transition.isTransitioningBetween(Scenes.QuickSettings, Scenes.Shade) -> {
                 Scenes.QuickSettings
             }
+            transition.isTransitioningBetween(
+                Overlays.QuickSettingsShade,
+                Overlays.NotificationsShade,
+            ) -> {
+                Overlays.QuickSettingsShade
+            }
             transition.toContent in contents -> transition.toContent
             else -> {
                 check(transition.fromContent in contents) {
@@ -65,7 +74,8 @@
 
     /** Returns true when the media should be laid on top of the rest for the given [transition]. */
     fun shouldElevateMedia(transition: TransitionState.Transition): Boolean {
-        return transition.isTransitioningBetween(Scenes.Lockscreen, Scenes.Shade)
+        return transition.isTransitioningBetween(Scenes.Lockscreen, Scenes.Shade) ||
+            transition.isTransitioningBetween(Scenes.Lockscreen, Overlays.NotificationsShade)
     }
 }
 
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index 1061cc4..5a084db 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -828,15 +828,21 @@
 
     // Don't place the element in this content if this content is not part of the current element
     // transition.
-    if (content != transition.fromContent && content != transition.toContent) {
+    val isReplacingOverlay = transition is TransitionState.Transition.ReplaceOverlay
+    if (
+        content != transition.fromContent &&
+            content != transition.toContent &&
+            (!isReplacingOverlay || content != transition.currentScene)
+    ) {
         return false
     }
 
     // Place the element if it is not shared.
-    if (
-        transition.fromContent !in element.stateByContent ||
-            transition.toContent !in element.stateByContent
-    ) {
+    var copies = 0
+    if (transition.fromContent in element.stateByContent) copies++
+    if (transition.toContent in element.stateByContent) copies++
+    if (isReplacingOverlay && transition.currentScene in element.stateByContent) copies++
+    if (copies <= 1) {
         return true
     }
 
@@ -1269,9 +1275,10 @@
 
     // If we are replacing an overlay and the element is both in a single overlay and in the current
     // scene, interpolate the state of the element using the current scene as the other scene.
+    var currentSceneState: Element.State? = null
     if (!isSharedElement && transition is TransitionState.Transition.ReplaceOverlay) {
-        val currentSceneState = element.stateByContent[transition.currentScene]
-        if (currentSceneState != null) {
+        currentSceneState = element.stateByContent[transition.currentScene]
+        if (currentSceneState != null && isSharedElementEnabled(element.key, transition)) {
             return interpolateSharedElement(
                 transition = transition,
                 contentValue = contentValue,
@@ -1290,6 +1297,8 @@
             when {
                 isSharedElement && currentContent == fromContent -> fromState
                 isSharedElement -> toState
+                currentSceneState != null && currentContent == transition.currentScene ->
+                    currentSceneState
                 else -> fromState ?: toState
             }
         )
@@ -1409,7 +1418,13 @@
     val rangeProgress = transformation.range?.progress(progress) ?: progress
 
     // Interpolate between the value at rest and the value before entering/after leaving.
-    val isEntering = content == toContent
+    val isEntering =
+        when {
+            content == toContent -> true
+            content == fromContent -> false
+            content == transition.currentScene -> toState == null
+            else -> content == toContent
+        }
     return if (isEntering) {
         lerp(targetValue, idleValue, rangeProgress)
     } else {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt
index 9f2ac24..30eb302 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt
@@ -206,6 +206,17 @@
             (from == null || this.fromContent == from) &&
             (to == null || this.toContent == to)
     }
+
+    /** Whether we are transitioning from [content] to [other], or from [other] to [content]. */
+    fun isTransitioningBetween(content: ContentKey, other: ContentKey): Boolean {
+        return isTransitioning(from = content, to = other) ||
+            isTransitioning(from = other, to = content)
+    }
+
+    /** Whether we are transitioning from or to [content]. */
+    fun isTransitioningFromOrTo(content: ContentKey): Boolean {
+        return isTransitioning(from = content) || isTransitioning(to = content)
+    }
 }
 
 /**
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
index a001fa9..ffed15b 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
@@ -654,6 +654,85 @@
     }
 
     @Test
+    fun replaceAnimation_elementInCurrentSceneAndOneOverlay_sharedElementDisabled() {
+        rule.testReplaceOverlayTransition(
+            currentSceneContent = {
+                Box(Modifier.size(width = 180.dp, height = 120.dp)) {
+                    Foo(width = 60.dp, height = 40.dp)
+                }
+            },
+            fromContent = {},
+            fromAlignment = Alignment.TopStart,
+            toContent = { Foo(width = 100.dp, height = 80.dp) },
+            transition = {
+                // 4 frames of animation
+                spec = tween(4 * 16, easing = LinearEasing)
+
+                // Scale Foo to/from size 0 in each content instead of sharing it.
+                sharedElement(TestElements.Foo, enabled = false)
+                scaleSize(TestElements.Foo, width = 0f, height = 0f)
+            },
+        ) {
+            before {
+                rule
+                    .onNode(isElement(TestElements.Foo, content = SceneA))
+                    .assertSizeIsEqualTo(60.dp, 40.dp)
+                    .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+                rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist()
+                rule.onNode(isElement(TestElements.Foo, content = OverlayB)).assertDoesNotExist()
+            }
+
+            at(16) {
+                rule
+                    .onNode(isElement(TestElements.Foo, content = SceneA))
+                    .assertSizeIsEqualTo(45.dp, 30.dp)
+                    .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+                rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist()
+                rule
+                    .onNode(isElement(TestElements.Foo, content = OverlayB))
+                    .assertSizeIsEqualTo(25.dp, 20.dp)
+                    .assertPositionInRootIsEqualTo(((180 - 25) / 2f).dp, ((120 - 20) / 2f).dp)
+            }
+
+            at(32) {
+                rule
+                    .onNode(isElement(TestElements.Foo, content = SceneA))
+                    .assertSizeIsEqualTo(30.dp, 20.dp)
+                    .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+                rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist()
+                rule
+                    .onNode(isElement(TestElements.Foo, content = OverlayB))
+                    .assertSizeIsEqualTo(50.dp, 40.dp)
+                    .assertPositionInRootIsEqualTo(((180 - 50) / 2f).dp, ((120 - 40) / 2f).dp)
+            }
+
+            at(48) {
+                rule
+                    .onNode(isElement(TestElements.Foo, content = SceneA))
+                    .assertSizeIsEqualTo(15.dp, 10.dp)
+                    .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+                rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist()
+                rule
+                    .onNode(isElement(TestElements.Foo, content = OverlayB))
+                    .assertSizeIsEqualTo(75.dp, 60.dp)
+                    .assertPositionInRootIsEqualTo(((180 - 75) / 2f).dp, ((120 - 60) / 2f).dp)
+            }
+
+            after {
+                rule
+                    .onNode(isElement(TestElements.Foo, content = SceneA))
+                    .assertExists()
+                    .assertIsNotDisplayed()
+                rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist()
+                rule
+                    .onNode(isElement(TestElements.Foo, content = OverlayB))
+                    .assertSizeIsEqualTo(100.dp, 80.dp)
+                    .assertPositionInRootIsEqualTo(40.dp, 20.dp)
+            }
+        }
+    }
+
+    @Test
     fun overscrollingOverlay_movableElementNotInOverlay() {
         val state =
             rule.runOnUiThread {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManagerTest.kt
index 1386092..b7b98d4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManagerTest.kt
@@ -18,6 +18,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.plugins.ActivityStarter
 import javax.inject.Provider
 import org.junit.Before
@@ -41,10 +42,12 @@
 
     @Mock private lateinit var activityStarter: ActivityStarter
     @Mock private lateinit var dialogProvider: Provider<ExtraDimDialogDelegate>
+    @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator
 
     @Before
     fun setUp() {
-        extraDimDialogManager = ExtraDimDialogManager(dialogProvider, activityStarter)
+        extraDimDialogManager =
+            ExtraDimDialogManager(dialogProvider, activityStarter, dialogTransitionAnimator)
     }
 
     @Test
@@ -56,7 +59,7 @@
                 /* cancelAction= */ eq(null),
                 /* dismissShade= */ eq(false),
                 /* afterKeyguardGone= */ eq(true),
-                /* deferred= */ eq(false)
+                /* deferred= */ eq(false),
             )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt
index 4253c29..a330cf0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt
@@ -18,6 +18,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.filters.SmallTest
@@ -27,6 +28,7 @@
 import com.android.compose.animation.scene.SwipeDirection
 import com.android.compose.animation.scene.TransitionKey
 import com.android.compose.animation.scene.UserActionResult
+import com.android.compose.animation.scene.UserActionResult.ShowOverlay
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
@@ -42,8 +44,10 @@
 import com.android.systemui.power.data.repository.fakePowerRepository
 import com.android.systemui.power.shared.model.WakefulnessState
 import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.TransitionKeys
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
 import com.android.systemui.shade.data.repository.shadeRepository
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.testKosmos
@@ -111,12 +115,12 @@
 
         private fun expectedDownDestination(
             downFromEdge: Boolean,
-            isSingleShade: Boolean,
+            isNarrowScreen: Boolean,
             isShadeTouchable: Boolean,
         ): SceneKey? {
             return when {
                 !isShadeTouchable -> null
-                downFromEdge && isSingleShade -> Scenes.QuickSettings
+                downFromEdge && isNarrowScreen -> Scenes.QuickSettings
                 else -> Scenes.Shade
             }
         }
@@ -162,7 +166,7 @@
     @JvmField @Parameter(0) var canSwipeToEnter: Boolean = false
     @JvmField @Parameter(1) var downWithTwoPointers: Boolean = false
     @JvmField @Parameter(2) var downFromEdge: Boolean = false
-    @JvmField @Parameter(3) var isSingleShade: Boolean = true
+    @JvmField @Parameter(3) var isNarrowScreen: Boolean = true
     @JvmField @Parameter(4) var isCommunalAvailable: Boolean = false
     @JvmField @Parameter(5) var isShadeTouchable: Boolean = false
 
@@ -170,7 +174,8 @@
 
     @Test
     @EnableFlags(Flags.FLAG_COMMUNAL_HUB)
-    fun userActions() =
+    @DisableFlags(Flags.FLAG_DUAL_SHADE)
+    fun userActions_fullscreenShade() =
         testScope.runTest {
             underTest.activateIn(this)
             kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
@@ -182,7 +187,7 @@
                 }
             )
             sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
-            kosmos.shadeRepository.setShadeLayoutWide(!isSingleShade)
+            kosmos.shadeRepository.setShadeLayoutWide(!isNarrowScreen)
             kosmos.setCommunalAvailable(isCommunalAvailable)
             kosmos.fakePowerRepository.updateWakefulness(
                 rawState =
@@ -190,7 +195,7 @@
                         WakefulnessState.AWAKE
                     } else {
                         WakefulnessState.ASLEEP
-                    },
+                    }
             )
 
             val userActions by collectLastValue(underTest.actions)
@@ -212,7 +217,7 @@
                 .isEqualTo(
                     expectedDownDestination(
                         downFromEdge = downFromEdge,
-                        isSingleShade = isSingleShade,
+                        isNarrowScreen = isNarrowScreen,
                         isShadeTouchable = isShadeTouchable,
                     )
                 )
@@ -220,7 +225,7 @@
             assertThat(downDestination?.transitionKey)
                 .isEqualTo(
                     expectedDownTransitionKey(
-                        isSingleShade = isSingleShade,
+                        isSingleShade = isNarrowScreen,
                         isShadeTouchable = isShadeTouchable,
                     )
                 )
@@ -258,6 +263,101 @@
                 )
         }
 
+    @Test
+    @EnableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_DUAL_SHADE)
+    fun userActions_dualShade() =
+        testScope.runTest {
+            underTest.activateIn(this)
+            kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+                if (canSwipeToEnter) {
+                    AuthenticationMethodModel.None
+                } else {
+                    AuthenticationMethodModel.Pin
+                }
+            )
+            sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
+            kosmos.shadeRepository.setShadeLayoutWide(!isNarrowScreen)
+            kosmos.setCommunalAvailable(isCommunalAvailable)
+            kosmos.fakePowerRepository.updateWakefulness(
+                rawState =
+                    if (isShadeTouchable) {
+                        WakefulnessState.AWAKE
+                    } else {
+                        WakefulnessState.ASLEEP
+                    }
+            )
+
+            val userActions by collectLastValue(underTest.actions)
+
+            val downDestination =
+                userActions?.get(
+                    Swipe(
+                        SwipeDirection.Down,
+                        fromSource = Edge.Top.takeIf { downFromEdge },
+                        pointerCount = if (downWithTwoPointers) 2 else 1,
+                    )
+                )
+
+            if (downFromEdge || downWithTwoPointers || !isShadeTouchable) {
+                // Top edge is not applicable in dual shade, as well as two-finger swipe.
+                assertThat(downDestination).isNull()
+            } else {
+                assertThat(downDestination).isEqualTo(ShowOverlay(Overlays.NotificationsShade))
+                assertThat(downDestination?.transitionKey).isNull()
+            }
+
+            val downFromTopRightDestination =
+                userActions?.get(
+                    Swipe(
+                        SwipeDirection.Down,
+                        fromSource = SceneContainerEdge.TopRight,
+                        pointerCount = if (downWithTwoPointers) 2 else 1,
+                    )
+                )
+            when {
+                !isShadeTouchable -> assertThat(downFromTopRightDestination).isNull()
+                downWithTwoPointers -> assertThat(downFromTopRightDestination).isNull()
+                else -> {
+                    assertThat(downFromTopRightDestination)
+                        .isEqualTo(ShowOverlay(Overlays.QuickSettingsShade))
+                    assertThat(downFromTopRightDestination?.transitionKey).isNull()
+                }
+            }
+
+            val upScene by
+                collectLastValue(
+                    (userActions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene?.let {
+                        scene ->
+                        kosmos.sceneInteractor.resolveSceneFamily(scene)
+                    } ?: flowOf(null)
+                )
+
+            assertThat(upScene)
+                .isEqualTo(
+                    expectedUpDestination(
+                        canSwipeToEnter = canSwipeToEnter,
+                        isShadeTouchable = isShadeTouchable,
+                    )
+                )
+
+            val leftScene by
+                collectLastValue(
+                    (userActions?.get(Swipe.Left) as? UserActionResult.ChangeScene)?.toScene?.let {
+                        scene ->
+                        kosmos.sceneInteractor.resolveSceneFamily(scene)
+                    } ?: flowOf(null)
+                )
+
+            assertThat(leftScene)
+                .isEqualTo(
+                    expectedLeftDestination(
+                        isCommunalAvailable = isCommunalAvailable,
+                        isShadeTouchable = isShadeTouchable,
+                    )
+                )
+        }
+
     private fun createLockscreenSceneViewModel(): LockscreenUserActionsViewModel {
         return LockscreenUserActionsViewModel(
             deviceEntryInteractor = kosmos.deviceEntryInteractor,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractorTest.kt
index 3133312..b54fd86 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractorTest.kt
@@ -19,12 +19,15 @@
 import android.platform.test.annotations.EnabledOnRavenwood
 import android.platform.test.annotations.RequiresFlagsDisabled
 import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
 import android.provider.Settings
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.R
 import com.android.server.display.feature.flags.Flags
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.accessibility.extradim.ExtraDimDialogManager
 import com.android.systemui.accessibility.reduceBrightColorsController
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
@@ -33,8 +36,14 @@
 import com.android.systemui.qs.tiles.impl.reducebrightness.domain.model.ReduceBrightColorsTileModel
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.verify
 
 @SmallTest
 @EnabledOnRavenwood
@@ -45,21 +54,34 @@
     private val inputHandler = FakeQSTileIntentUserInputHandler()
     private val controller = kosmos.reduceBrightColorsController
 
-    private val underTest =
-        ReduceBrightColorsTileUserActionInteractor(
-            context.resources,
-            inputHandler,
-            controller,
-        )
+    @Mock private lateinit var mExtraDimDialogManager: ExtraDimDialogManager
 
-    private val underTestEvenDimmerEnabled =
-        ReduceBrightColorsTileUserActionInteractor(
-            context.orCreateTestableResources
-                .apply { addOverride(R.bool.config_evenDimmerEnabled, true) }
-                .resources,
-            inputHandler,
-            controller,
-        )
+    @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+    private lateinit var underTest: ReduceBrightColorsTileUserActionInteractor
+    private lateinit var underTestEvenDimmerEnabled: ReduceBrightColorsTileUserActionInteractor
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        underTest =
+            ReduceBrightColorsTileUserActionInteractor(
+                context.resources,
+                inputHandler,
+                controller,
+                mExtraDimDialogManager,
+            )
+
+        underTestEvenDimmerEnabled =
+            ReduceBrightColorsTileUserActionInteractor(
+                context.orCreateTestableResources
+                    .apply { addOverride(R.bool.config_evenDimmerEnabled, true) }
+                    .resources,
+                inputHandler,
+                controller,
+                mExtraDimDialogManager,
+            )
+    }
 
     @Test
     @RequiresFlagsDisabled(Flags.FLAG_EVEN_DIMMER)
@@ -142,9 +164,7 @@
             QSTileInputTestKtx.longClick(ReduceBrightColorsTileModel(enabled))
         )
 
-        QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
-            assertThat(it.intent.action).isEqualTo(Settings.ACTION_DISPLAY_SETTINGS)
-        }
+        verify(mExtraDimDialogManager).dismissKeyguardIfNeededAndShowDialog(anyOrNull())
     }
 
     @Test
@@ -155,9 +175,6 @@
         underTestEvenDimmerEnabled.handleInput(
             QSTileInputTestKtx.longClick(ReduceBrightColorsTileModel(enabled))
         )
-
-        QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
-            assertThat(it.intent.action).isEqualTo(Settings.ACTION_DISPLAY_SETTINGS)
-        }
+        verify(mExtraDimDialogManager).dismissKeyguardIfNeededAndShowDialog(anyOrNull())
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModelTest.kt
index 03106ec..e6a24e3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModelTest.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.scene.ui.viewmodel
 
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.testing.TestableLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -29,6 +31,7 @@
 import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
 import com.android.systemui.shade.data.repository.shadeRepository
 import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -52,14 +55,12 @@
 
     @Before
     fun setUp() {
-        underTest =
-            GoneUserActionsViewModel(
-                shadeInteractor = kosmos.shadeInteractor,
-            )
+        underTest = GoneUserActionsViewModel(shadeInteractor = kosmos.shadeInteractor)
         underTest.activateIn(testScope)
     }
 
     @Test
+    @DisableFlags(DualShade.FLAG_NAME)
     fun downTransitionKey_splitShadeEnabled_isGoneToSplitShade() =
         testScope.runTest {
             val userActions by collectLastValue(underTest.actions)
@@ -71,6 +72,7 @@
         }
 
     @Test
+    @DisableFlags(DualShade.FLAG_NAME)
     fun downTransitionKey_splitShadeDisabled_isNull() =
         testScope.runTest {
             val userActions by collectLastValue(underTest.actions)
@@ -79,4 +81,15 @@
 
             assertThat(userActions?.get(Swipe(SwipeDirection.Down))?.transitionKey).isNull()
         }
+
+    @Test
+    @EnableFlags(DualShade.FLAG_NAME)
+    fun downTransitionKey_dualShadeEnabled_isNull() =
+        testScope.runTest {
+            val userActions by collectLastValue(underTest.actions)
+            shadeRepository.setShadeLayoutWide(true)
+            runCurrent()
+
+            assertThat(userActions?.get(Swipe(SwipeDirection.Down))?.transitionKey).isNull()
+        }
 }
diff --git a/packages/SystemUI/res/layout/shelf_action_chip.xml b/packages/SystemUI/res/layout/shelf_action_chip.xml
index c7606e4..1c65e36 100644
--- a/packages/SystemUI/res/layout/shelf_action_chip.xml
+++ b/packages/SystemUI/res/layout/shelf_action_chip.xml
@@ -28,6 +28,7 @@
     <ImageView
         android:id="@+id/overlay_action_chip_icon"
         android:tint="?androidprv:attr/materialColorOnSecondary"
+        android:tintMode="src_in"
         android:layout_width="@dimen/overlay_action_chip_icon_size"
         android:layout_height="@dimen/overlay_action_chip_icon_size"/>
     <TextView
diff --git a/packages/SystemUI/res/layout/status_bar.xml b/packages/SystemUI/res/layout/status_bar.xml
index 541aebe..32bcca1 100644
--- a/packages/SystemUI/res/layout/status_bar.xml
+++ b/packages/SystemUI/res/layout/status_bar.xml
@@ -102,7 +102,9 @@
                     <include layout="@layout/ongoing_activity_chip"
                         android:id="@+id/ongoing_activity_chip_primary"/>
 
-                    <!-- TODO(b/364653005): Add a second activity chip. -->
+                    <include layout="@layout/ongoing_activity_chip"
+                        android:id="@+id/ongoing_activity_chip_secondary"
+                        android:visibility="gone"/>
 
                     <com.android.systemui.statusbar.AlphaOptimizedFrameLayout
                         android:id="@+id/notification_icon_area"
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManager.kt b/packages/SystemUI/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManager.kt
index e1297d3..60d80ef 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManager.kt
@@ -15,7 +15,9 @@
  */
 package com.android.systemui.accessibility.extradim
 
-import androidx.annotation.VisibleForTesting
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.statusbar.phone.SystemUIDialog
@@ -28,25 +30,35 @@
 @Inject
 constructor(
     private val extraDimDialogDelegateProvider: Provider<ExtraDimDialogDelegate>,
-    private val mActivityStarter: ActivityStarter
+    private val mActivityStarter: ActivityStarter,
+    private val dialogTransitionAnimator: DialogTransitionAnimator,
 ) {
     private var dialog: SystemUIDialog? = null
 
-    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
-    fun dismissKeyguardIfNeededAndShowDialog() {
+    @JvmOverloads
+    fun dismissKeyguardIfNeededAndShowDialog(expandable: Expandable? = null) {
         mActivityStarter.executeRunnableDismissingKeyguard(
-            { showRemoveExtraDimShortcutsDialog() },
+            { showRemoveExtraDimShortcutsDialog(expandable) },
             /* cancelAction= */ null,
             /* dismissShade= */ false,
             /* afterKeyguardGone= */ true,
-            /* deferred= */ false
+            /* deferred= */ false,
         )
     }
 
     /** Show the dialog for removing all Extra Dim shortcuts. */
-    private fun showRemoveExtraDimShortcutsDialog() {
+    private fun showRemoveExtraDimShortcutsDialog(expandable: Expandable?) {
         dialog?.dismiss()
-        dialog = extraDimDialogDelegateProvider.get().createDialog()
-        dialog!!.show()
+        val dialog2 = extraDimDialogDelegateProvider.get().createDialog()
+        dialog = dialog2
+
+        val controller =
+            expandable?.dialogTransitionController(
+                DialogCuj(com.android.internal.jank.Cuj.CUJ_SHADE_DIALOG_OPEN)
+            )
+
+        controller?.let {
+            dialogTransitionAnimator.show(dialog2, it, animateBackgroundBoundsChange = true)
+        } ?: dialog2.show()
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt
index dd47678..ecae079 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt
@@ -21,17 +21,19 @@
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.Swipe
 import com.android.compose.animation.scene.SwipeDirection
+import com.android.compose.animation.scene.TransitionKey
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
 import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.shared.model.ShadeMode
-import com.android.systemui.util.kotlin.filterValuesNotNull
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -52,71 +54,67 @@
         shadeInteractor.isShadeTouchable
             .flatMapLatest { isShadeTouchable ->
                 if (!isShadeTouchable) {
-                    flowOf(emptyMap())
-                } else {
-                    combine(
-                        deviceEntryInteractor.isUnlocked,
-                        communalInteractor.isCommunalAvailable,
-                        shadeInteractor.shadeMode,
-                    ) { isDeviceUnlocked, isCommunalAvailable, shadeMode ->
-                        val notifShadeSceneKey =
-                            UserActionResult(
-                                toScene = SceneFamilies.NotifShade,
-                                transitionKey =
-                                    ToSplitShade.takeIf { shadeMode is ShadeMode.Split },
+                    return@flatMapLatest flowOf(emptyMap())
+                }
+
+                combine(
+                    deviceEntryInteractor.isUnlocked,
+                    communalInteractor.isCommunalAvailable,
+                    shadeInteractor.shadeMode,
+                ) { isDeviceUnlocked, isCommunalAvailable, shadeMode ->
+                    buildList {
+                            if (isCommunalAvailable) {
+                                add(Swipe.Left to Scenes.Communal)
+                            }
+
+                            add(Swipe.Up to if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer)
+
+                            addAll(
+                                when (shadeMode) {
+                                    ShadeMode.Single -> fullscreenShadeActions()
+                                    ShadeMode.Split ->
+                                        fullscreenShadeActions(transitionKey = ToSplitShade)
+                                    ShadeMode.Dual -> dualShadeActions()
+                                }
                             )
-
-                        mapOf(
-                                Swipe.Left to
-                                    UserActionResult(Scenes.Communal).takeIf {
-                                        isCommunalAvailable
-                                    },
-                                Swipe.Up to if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer,
-
-                                // Swiping down from the top edge goes to QS (or shade if in split
-                                // shade mode).
-                                swipeDownFromTop(pointerCount = 1) to
-                                    if (shadeMode is ShadeMode.Single) {
-                                        UserActionResult(Scenes.QuickSettings)
-                                    } else {
-                                        notifShadeSceneKey
-                                    },
-
-                                // TODO(b/338577208): Remove once we add Dual Shade invocation zones
-                                swipeDownFromTop(pointerCount = 2) to
-                                    UserActionResult(
-                                        toScene = SceneFamilies.QuickSettings,
-                                        transitionKey =
-                                            ToSplitShade.takeIf { shadeMode is ShadeMode.Split }
-                                    ),
-
-                                // Swiping down, not from the edge, always navigates to the notif
-                                // shade scene.
-                                swipeDown(pointerCount = 1) to notifShadeSceneKey,
-                                swipeDown(pointerCount = 2) to notifShadeSceneKey,
-                            )
-                            .filterValuesNotNull()
-                    }
+                        }
+                        .associate { it }
                 }
             }
             .collect { setActions(it) }
     }
 
-    private fun swipeDownFromTop(pointerCount: Int): Swipe {
-        return Swipe(
-            SwipeDirection.Down,
-            fromSource = Edge.Top,
-            pointerCount = pointerCount,
+    private fun fullscreenShadeActions(
+        transitionKey: TransitionKey? = null
+    ): Array<Pair<UserAction, UserActionResult>> {
+        val notifShadeSceneKey = UserActionResult(SceneFamilies.NotifShade, transitionKey)
+        val qsShadeSceneKey = UserActionResult(SceneFamilies.QuickSettings, transitionKey)
+        return arrayOf(
+            // Swiping down, not from the edge, always goes to shade.
+            Swipe.Down to notifShadeSceneKey,
+            swipeDown(pointerCount = 2) to notifShadeSceneKey,
+            // Swiping down from the top edge goes to QS.
+            swipeDownFromTop(pointerCount = 1) to qsShadeSceneKey,
+            swipeDownFromTop(pointerCount = 2) to qsShadeSceneKey,
         )
     }
 
-    private fun swipeDown(pointerCount: Int): Swipe {
-        return Swipe(
-            SwipeDirection.Down,
-            pointerCount = pointerCount,
+    private fun dualShadeActions(): Array<Pair<UserAction, UserActionResult>> {
+        return arrayOf(
+            Swipe.Down to Overlays.NotificationsShade,
+            Swipe(direction = SwipeDirection.Down, fromSource = SceneContainerEdge.TopRight) to
+                Overlays.QuickSettingsShade,
         )
     }
 
+    private fun swipeDownFromTop(pointerCount: Int): Swipe {
+        return Swipe(SwipeDirection.Down, fromSource = Edge.Top, pointerCount = pointerCount)
+    }
+
+    private fun swipeDown(pointerCount: Int): Swipe {
+        return Swipe(SwipeDirection.Down, pointerCount = pointerCount)
+    }
+
     @AssistedFactory
     interface Factory {
         fun create(): LockscreenUserActionsViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java
index cf1dca3..893ef7e0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java
@@ -29,12 +29,6 @@
     /** Sets the activation state of Reduce Bright Colors */
     void setReduceBrightColorsActivated(boolean activated);
 
-    /** Sets whether Reduce Bright Colors is enabled */
-    void setReduceBrightColorsFeatureAvailable(boolean enabled);
-
-    /** Gets whether Reduce Bright Colors is enabled */
-    boolean isReduceBrightColorsFeatureAvailable();
-
     /** Gets whether Reduce Bright Colors is being transitioned to Even Dimmer */
     boolean isInUpgradeMode(Resources resources);
     /**
@@ -48,12 +42,5 @@
          */
         default void onActivated(boolean activated) {
         }
-        /**
-         * Listener invoked when the feature enabled state changes.
-         *
-         * @param enabled {@code true} if Reduce Bright Colors feature is enabled.
-         */
-        default void onFeatureEnabledChanged(boolean enabled) {
-        }
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java
index 4d6cf78..ca6e40d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java
@@ -48,7 +48,6 @@
     private final ContentObserver mContentObserver;
     private final SecureSettings mSecureSettings;
     private final ArrayList<ReduceBrightColorsController.Listener> mListeners = new ArrayList<>();
-    private boolean mAvailable = true;
 
     @Inject
     public ReduceBrightColorsControllerImpl(UserTracker userTracker,
@@ -77,7 +76,6 @@
         mCurrentUserTrackerCallback = new UserTracker.Callback() {
             @Override
             public void onUserChanged(int newUser, Context userContext) {
-                mAvailable = true;
                 synchronized (mListeners) {
                     if (mListeners.size() > 0) {
                         if (com.android.systemui.Flags.registerContentObserversAsync()) {
@@ -141,17 +139,6 @@
         mManager.setReduceBrightColorsActivated(activated);
     }
 
-    @Override
-    public void setReduceBrightColorsFeatureAvailable(boolean enabled) {
-        mAvailable = enabled;
-        dispatchOnEnabledChanged(enabled);
-        mAvailable = true;
-    }
-
-    @Override
-    public boolean isReduceBrightColorsFeatureAvailable() {
-        return mAvailable;
-    }
 
     @Override
     public boolean isInUpgradeMode(Resources resources) {
@@ -166,11 +153,4 @@
             l.onActivated(activated);
         }
     }
-
-    private void dispatchOnEnabledChanged(boolean enabled) {
-        ArrayList<Listener> copy = new ArrayList<>(mListeners);
-        for (Listener l : copy) {
-            l.onFeatureEnabledChanged(enabled);
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddable.kt
index 4d823ab..bde6820 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddable.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddable.kt
@@ -42,7 +42,7 @@
 ) :
     CallbackControllerAutoAddable<
         ReduceBrightColorsController.Listener,
-        ReduceBrightColorsController
+        ReduceBrightColorsController,
     >(controller) {
 
     override val spec: TileSpec
@@ -55,12 +55,6 @@
                     sendAdd()
                 }
             }
-
-            override fun onFeatureEnabledChanged(enabled: Boolean) {
-                if (!enabled) {
-                    available = false
-                }
-            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java
index af5b311..d624d98 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java
@@ -29,6 +29,7 @@
 
 import com.android.internal.R;
 import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.accessibility.extradim.ExtraDimDialogManager;
 import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -55,6 +56,7 @@
     private final ReduceBrightColorsController mReduceBrightColorsController;
     private boolean mIsListening;
     private final boolean mInUpgradeMode;
+    private final ExtraDimDialogManager mExtraDimDialogManager;
 
     @Inject
     public ReduceBrightColorsTile(
@@ -68,12 +70,14 @@
             MetricsLogger metricsLogger,
             StatusBarStateController statusBarStateController,
             ActivityStarter activityStarter,
-            QSLogger qsLogger
+            QSLogger qsLogger,
+            ExtraDimDialogManager extraDimDialogManager
     ) {
         super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger,
                 statusBarStateController, activityStarter, qsLogger);
         mReduceBrightColorsController = reduceBrightColorsController;
         mReduceBrightColorsController.observe(getLifecycle(), this);
+        mExtraDimDialogManager = extraDimDialogManager;
 
         mInUpgradeMode = reduceBrightColorsController.isInUpgradeMode(mContext.getResources());
         mIsAvailable = isAvailable || mInUpgradeMode;
@@ -102,19 +106,24 @@
 
     private boolean goToEvenDimmer() {
         if (mInUpgradeMode) {
-            mHost.removeTile(getTileSpec());
-            mIsAvailable = false;
             return true;
         }
         return false;
     }
 
     @Override
-    protected void handleClick(@Nullable Expandable expandable) {
-
+    protected void handleLongClick(@Nullable Expandable expandable) {
         if (goToEvenDimmer()) {
-            mActivityStarter.postStartActivityDismissingKeyguard(
-                    new Intent(Settings.ACTION_DISPLAY_SETTINGS), 0);
+            mExtraDimDialogManager.dismissKeyguardIfNeededAndShowDialog(expandable);
+        } else {
+            super.handleLongClick(expandable);
+        }
+    }
+
+    @Override
+    protected void handleClick(@Nullable Expandable expandable) {
+        if (goToEvenDimmer()) {
+            mExtraDimDialogManager.dismissKeyguardIfNeededAndShowDialog(expandable);
         } else {
             mReduceBrightColorsController.setReduceBrightColorsActivated(!mState.value);
         }
@@ -146,9 +155,4 @@
     public void onActivated(boolean activated) {
         refreshState();
     }
-
-    @Override
-    public void onFeatureEnabledChanged(boolean enabled) {
-        refreshState();
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractor.kt
index 00b1e41..536c5f1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractor.kt
@@ -23,13 +23,13 @@
 import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
 import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
 import com.android.systemui.qs.tiles.impl.reducebrightness.domain.model.ReduceBrightColorsTileModel
-import com.android.systemui.util.kotlin.isAvailable
 import com.android.systemui.util.kotlin.isEnabled
 import javax.inject.Inject
 import javax.inject.Named
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 
@@ -44,7 +44,7 @@
 
     override fun tileData(
         user: UserHandle,
-        triggers: Flow<DataUpdateTrigger>
+        triggers: Flow<DataUpdateTrigger>,
     ): Flow<ReduceBrightColorsTileModel> {
         return reduceBrightColorsController
             .isEnabled()
@@ -53,6 +53,5 @@
             .flowOn(bgCoroutineContext)
     }
 
-    override fun availability(user: UserHandle): Flow<Boolean> =
-        reduceBrightColorsController.isAvailable()
+    override fun availability(user: UserHandle): Flow<Boolean> = flowOf(isAvailable)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt
index de49e70..15c9901 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt
@@ -19,6 +19,7 @@
 import android.content.Intent
 import android.content.res.Resources
 import android.provider.Settings
+import com.android.systemui.accessibility.extradim.ExtraDimDialogManager
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.qs.ReduceBrightColorsController
 import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
@@ -35,6 +36,7 @@
     @Main private val resources: Resources,
     private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
     private val reduceBrightColorsController: ReduceBrightColorsController,
+    private val extraDimDialogManager: ExtraDimDialogManager,
 ) : QSTileUserActionInteractor<ReduceBrightColorsTileModel> {
 
     val isInUpgradeMode: Boolean = reduceBrightColorsController.isInUpgradeMode(resources)
@@ -44,12 +46,9 @@
             when (action) {
                 is QSTileUserAction.Click -> {
                     if (isInUpgradeMode) {
-                        reduceBrightColorsController.setReduceBrightColorsFeatureAvailable(false)
-                        qsTileIntentUserActionHandler.handle(
-                            action.expandable,
-                            Intent(Settings.ACTION_DISPLAY_SETTINGS)
+                        extraDimDialogManager.dismissKeyguardIfNeededAndShowDialog(
+                            action.expandable
                         )
-                        // TODO(b/349458355): show dialog
                         return@with
                     }
                     reduceBrightColorsController.setReduceBrightColorsActivated(
@@ -58,17 +57,14 @@
                 }
                 is QSTileUserAction.LongClick -> {
                     if (isInUpgradeMode) {
-                        reduceBrightColorsController.setReduceBrightColorsFeatureAvailable(false)
-                        qsTileIntentUserActionHandler.handle(
-                            action.expandable,
-                            Intent(Settings.ACTION_DISPLAY_SETTINGS)
+                        extraDimDialogManager.dismissKeyguardIfNeededAndShowDialog(
+                            action.expandable
                         )
-                        // TODO(b/349458355): show dialog
                         return@with
                     }
                     qsTileIntentUserActionHandler.handle(
                         action.expandable,
-                        Intent(Settings.ACTION_REDUCE_BRIGHT_COLORS_SETTINGS)
+                        Intent(Settings.ACTION_REDUCE_BRIGHT_COLORS_SETTINGS),
                     )
                 }
                 is QSTileUserAction.ToggleClick -> {}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt
index ea4122a..7bf2b63 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt
@@ -19,52 +19,49 @@
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.Swipe
 import com.android.compose.animation.scene.SwipeDirection
+import com.android.compose.animation.scene.TransitionKey
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
-import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.shared.model.ShadeMode
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
-import kotlinx.coroutines.flow.map
 
 class GoneUserActionsViewModel
 @AssistedInject
-constructor(
-    private val shadeInteractor: ShadeInteractor,
-) : UserActionsViewModel() {
+constructor(private val shadeInteractor: ShadeInteractor) : UserActionsViewModel() {
 
     override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
-        shadeInteractor.shadeMode
-            .map { shadeMode ->
-                buildMap<UserAction, UserActionResult> {
-                    if (
-                        shadeMode is ShadeMode.Single ||
-                            // TODO(b/338577208): Remove this once we add Dual Shade invocation
-                            // zones.
-                            shadeMode is ShadeMode.Dual
-                    ) {
-                        put(
-                            Swipe(
-                                pointerCount = 2,
-                                fromSource = Edge.Top,
-                                direction = SwipeDirection.Down,
-                            ),
-                            UserActionResult(SceneFamilies.QuickSettings)
-                        )
-                    }
-
-                    put(
-                        Swipe.Down,
-                        UserActionResult(
-                            SceneFamilies.NotifShade,
-                            ToSplitShade.takeIf { shadeMode is ShadeMode.Split }
-                        )
-                    )
+        shadeInteractor.shadeMode.collect { shadeMode ->
+            setActions(
+                when (shadeMode) {
+                    ShadeMode.Single -> fullscreenShadeActions()
+                    ShadeMode.Split -> fullscreenShadeActions(transitionKey = ToSplitShade)
+                    ShadeMode.Dual -> dualShadeActions()
                 }
-            }
-            .collect { setActions(it) }
+            )
+        }
+    }
+
+    private fun fullscreenShadeActions(
+        transitionKey: TransitionKey? = null
+    ): Map<UserAction, UserActionResult> {
+        return mapOf(
+            Swipe.Down to UserActionResult(Scenes.Shade, transitionKey),
+            Swipe(direction = SwipeDirection.Down, pointerCount = 2, fromSource = Edge.Top) to
+                UserActionResult(Scenes.Shade, transitionKey),
+        )
+    }
+
+    private fun dualShadeActions(): Map<UserAction, UserActionResult> {
+        return mapOf(
+            Swipe.Down to Overlays.NotificationsShade,
+            Swipe(direction = SwipeDirection.Down, fromSource = SceneContainerEdge.TopRight) to
+                Overlays.QuickSettingsShade,
+        )
     }
 
     @AssistedFactory
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
index 083cee7..75165cb 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.settings.brightness;
 
-import static com.android.systemui.Flags.hapticBrightnessSlider;
-
 import android.content.Context;
 import android.content.Intent;
 import android.view.LayoutInflater;
@@ -317,9 +315,7 @@
             SeekbarHapticPlugin plugin = new SeekbarHapticPlugin(
                     mVibratorHelper,
                     mSystemClock);
-            if (hapticBrightnessSlider()) {
-                HapticSliderViewBinder.bind(viewRoot, plugin);
-            }
+            HapticSliderViewBinder.bind(viewRoot, plugin);
             return new BrightnessSliderController(
                     root, mFalsingManager, mUiEventLogger, plugin, mActivityStarter);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/shared/StatusBarRonChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/shared/StatusBarRonChips.kt
new file mode 100644
index 0000000..4c0c461
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/shared/StatusBarRonChips.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.shared
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the status bar ron chips flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object StatusBarRonChips {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_STATUS_BAR_RON_CHIPS
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.statusBarRonChips()
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is not enabled to ensure that the refactor author catches issues in testing.
+     * Caution!! Using this check incorrectly will cause crashes in nextfood builds!
+     */
+    @JvmStatic
+    inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/MultipleOngoingActivityChipsModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/MultipleOngoingActivityChipsModel.kt
new file mode 100644
index 0000000..d2555b0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/MultipleOngoingActivityChipsModel.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.statusbar.chips.ui.model
+
+/** Models multiple active ongoing activity chips at once. */
+data class MultipleOngoingActivityChipsModel(
+    /** The primary chip to show. This will *always* be shown. */
+    val primary: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
+    /**
+     * The secondary chip to show. If there's not enough room in the status bar, this chip will
+     * *not* be shown.
+     */
+    val secondary: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
+) {
+    init {
+        if (
+            primary is OngoingActivityChipModel.Hidden &&
+                secondary is OngoingActivityChipModel.Shown
+        ) {
+            throw IllegalArgumentException("`secondary` cannot be Shown if `primary` is Hidden")
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
index 199eb06..24c1b87 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.chips.ui.viewmodel
 
+import com.android.systemui.Flags
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.log.LogBuffer
@@ -27,6 +28,7 @@
 import com.android.systemui.statusbar.chips.ron.shared.StatusBarRonChips
 import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel
 import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel
+import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.util.kotlin.pairwise
 import javax.inject.Inject
@@ -87,68 +89,58 @@
         ) : InternalChipModel
     }
 
-    private val internalChip: Flow<InternalChipModel> =
+    private data class ChipBundle(
+        val screenRecord: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
+        val shareToApp: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
+        val castToOtherDevice: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
+        val call: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
+        val demoRon: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
+    )
+
+    /** Bundles all the incoming chips into one object to easily pass to various flows. */
+    private val incomingChipBundle =
         combine(
-            screenRecordChipViewModel.chip,
-            shareToAppChipViewModel.chip,
-            castToOtherDeviceChipViewModel.chip,
-            callChipViewModel.chip,
-            demoRonChipViewModel.chip,
-        ) { screenRecord, shareToApp, castToOtherDevice, call, demoRon ->
-            logger.log(
-                TAG,
-                LogLevel.INFO,
-                {
-                    str1 = screenRecord.logName
-                    str2 = shareToApp.logName
-                    str3 = castToOtherDevice.logName
-                },
-                { "Chips: ScreenRecord=$str1 > ShareToApp=$str2 > CastToOther=$str3..." },
-            )
-            logger.log(
-                TAG,
-                LogLevel.INFO,
-                {
-                    str1 = call.logName
-                    str2 = demoRon.logName
-                },
-                { "... > Call=$str1 > DemoRon=$str2" }
-            )
-            // This `when` statement shows the priority order of the chips.
-            when {
-                // Screen recording also activates the media projection APIs, so whenever the
-                // screen recording chip is active, the media projection chip would also be
-                // active. We want the screen-recording-specific chip shown in this case, so we
-                // give the screen recording chip priority. See b/296461748.
-                screenRecord is OngoingActivityChipModel.Shown ->
-                    InternalChipModel.Shown(ChipType.ScreenRecord, screenRecord)
-                shareToApp is OngoingActivityChipModel.Shown ->
-                    InternalChipModel.Shown(ChipType.ShareToApp, shareToApp)
-                castToOtherDevice is OngoingActivityChipModel.Shown ->
-                    InternalChipModel.Shown(ChipType.CastToOtherDevice, castToOtherDevice)
-                call is OngoingActivityChipModel.Shown ->
-                    InternalChipModel.Shown(ChipType.Call, call)
-                demoRon is OngoingActivityChipModel.Shown -> {
-                    StatusBarRonChips.assertInNewMode()
-                    InternalChipModel.Shown(ChipType.DemoRon, demoRon)
-                }
-                else -> {
-                    // We should only get here if all chip types are hidden
-                    check(screenRecord is OngoingActivityChipModel.Hidden)
-                    check(shareToApp is OngoingActivityChipModel.Hidden)
-                    check(castToOtherDevice is OngoingActivityChipModel.Hidden)
-                    check(call is OngoingActivityChipModel.Hidden)
-                    check(demoRon is OngoingActivityChipModel.Hidden)
-                    InternalChipModel.Hidden(
-                        screenRecord = screenRecord,
-                        shareToApp = shareToApp,
-                        castToOtherDevice = castToOtherDevice,
-                        call = call,
-                        demoRon = demoRon,
-                    )
-                }
+                screenRecordChipViewModel.chip,
+                shareToAppChipViewModel.chip,
+                castToOtherDeviceChipViewModel.chip,
+                callChipViewModel.chip,
+                demoRonChipViewModel.chip,
+            ) { screenRecord, shareToApp, castToOtherDevice, call, demoRon ->
+                logger.log(
+                    TAG,
+                    LogLevel.INFO,
+                    {
+                        str1 = screenRecord.logName
+                        str2 = shareToApp.logName
+                        str3 = castToOtherDevice.logName
+                    },
+                    { "Chips: ScreenRecord=$str1 > ShareToApp=$str2 > CastToOther=$str3..." },
+                )
+                logger.log(
+                    TAG,
+                    LogLevel.INFO,
+                    {
+                        str1 = call.logName
+                        str2 = demoRon.logName
+                    },
+                    { "... > Call=$str1 > DemoRon=$str2" }
+                )
+                ChipBundle(
+                    screenRecord = screenRecord,
+                    shareToApp = shareToApp,
+                    castToOtherDevice = castToOtherDevice,
+                    call = call,
+                    demoRon = demoRon,
+                )
             }
-        }
+            // Some of the chips could have timers in them and we don't want the start time
+            // for those timers to get reset for any reason. So, as soon as any subscriber has
+            // requested the chip information, we maintain it forever by using
+            // [SharingStarted.Lazily]. See b/347726238.
+            .stateIn(scope, SharingStarted.Lazily, ChipBundle())
+
+    private val internalChip: Flow<InternalChipModel> =
+        incomingChipBundle.map { bundle -> pickMostImportantChip(bundle).mostImportantChip }
 
     /**
      * A flow modeling the primary chip that should be shown in the status bar after accounting for
@@ -160,38 +152,189 @@
     val primaryChip: StateFlow<OngoingActivityChipModel> =
         internalChip
             .pairwise(initialValue = DEFAULT_INTERNAL_HIDDEN_MODEL)
-            .map { (old, new) ->
-                if (old is InternalChipModel.Shown && new is InternalChipModel.Hidden) {
-                    // If we're transitioning from showing the chip to hiding the chip, different
-                    // chips require different animation behaviors. For example, the screen share
-                    // chips shouldn't animate if the user stopped the screen share from the dialog
-                    // (see b/353249803#comment4), but the call chip should always animate.
-                    //
-                    // This `when` block makes sure that when we're transitioning from Shown to
-                    // Hidden, we check what chip type was previously showing and we use that chip
-                    // type's hide animation behavior.
-                    when (old.type) {
-                        ChipType.ScreenRecord -> new.screenRecord
-                        ChipType.ShareToApp -> new.shareToApp
-                        ChipType.CastToOtherDevice -> new.castToOtherDevice
-                        ChipType.Call -> new.call
-                        ChipType.DemoRon -> new.demoRon
-                    }
-                } else if (new is InternalChipModel.Shown) {
-                    // If we have a chip to show, always show it.
-                    new.model
-                } else {
-                    // In the Hidden -> Hidden transition, it shouldn't matter which hidden model we
-                    // choose because no animation should happen regardless.
-                    OngoingActivityChipModel.Hidden()
-                }
-            }
-            // Some of the chips could have timers in them and we don't want the start time
-            // for those timers to get reset for any reason. So, as soon as any subscriber has
-            // requested the chip information, we maintain it forever by using
-            // [SharingStarted.Lazily]. See b/347726238.
+            .map { (old, new) -> createOutputModel(old, new) }
             .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden())
 
+    /**
+     * Equivalent to [MultipleOngoingActivityChipsModel] but using the internal models to do some
+     * state tracking before we get the final output.
+     */
+    private data class InternalMultipleOngoingActivityChipsModel(
+        val primary: InternalChipModel,
+        val secondary: InternalChipModel,
+    )
+
+    private val internalChips: Flow<InternalMultipleOngoingActivityChipsModel> =
+        incomingChipBundle.map { bundle ->
+            // First: Find the most important chip.
+            val primaryChipResult = pickMostImportantChip(bundle)
+            val primaryChip = primaryChipResult.mostImportantChip
+            if (primaryChip is InternalChipModel.Hidden) {
+                // If the primary chip is hidden, the secondary chip will also be hidden, so just
+                // pass the same Hidden model for both.
+                InternalMultipleOngoingActivityChipsModel(primaryChip, primaryChip)
+            } else {
+                // Then: Find the next most important chip.
+                val secondaryChip =
+                    pickMostImportantChip(primaryChipResult.remainingChips).mostImportantChip
+                InternalMultipleOngoingActivityChipsModel(primaryChip, secondaryChip)
+            }
+        }
+
+    /**
+     * A flow modeling the primary chip that should be shown in the status bar after accounting for
+     * possibly multiple ongoing activities and animation requirements.
+     *
+     * [com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment] is responsible for
+     * actually displaying the chip.
+     */
+    val chips: StateFlow<MultipleOngoingActivityChipsModel> =
+        if (!Flags.statusBarRonChips()) {
+            // Multiple chips are only allowed with RONs. If the flag isn't on, use just the
+            // primary chip.
+            primaryChip
+                .map {
+                    MultipleOngoingActivityChipsModel(
+                        primary = it,
+                        secondary = OngoingActivityChipModel.Hidden(),
+                    )
+                }
+                .stateIn(
+                    scope,
+                    SharingStarted.Lazily,
+                    MultipleOngoingActivityChipsModel(),
+                )
+        } else {
+            internalChips
+                .pairwise(initialValue = DEFAULT_MULTIPLE_INTERNAL_HIDDEN_MODEL)
+                .map { (old, new) ->
+                    val correctPrimary = createOutputModel(old.primary, new.primary)
+                    val correctSecondary = createOutputModel(old.secondary, new.secondary)
+                    MultipleOngoingActivityChipsModel(correctPrimary, correctSecondary)
+                }
+                .stateIn(
+                    scope,
+                    SharingStarted.Lazily,
+                    MultipleOngoingActivityChipsModel(),
+                )
+        }
+
+    /** A data class representing the return result of [pickMostImportantChip]. */
+    private data class MostImportantChipResult(
+        val mostImportantChip: InternalChipModel,
+        val remainingChips: ChipBundle,
+    )
+
+    /**
+     * Finds the most important chip from the given [bundle].
+     *
+     * This function returns that most important chip, and it also returns any remaining chips that
+     * still want to be shown after filtering out the most important chip.
+     */
+    private fun pickMostImportantChip(bundle: ChipBundle): MostImportantChipResult {
+        // This `when` statement shows the priority order of the chips.
+        return when {
+            bundle.screenRecord is OngoingActivityChipModel.Shown ->
+                MostImportantChipResult(
+                    mostImportantChip =
+                        InternalChipModel.Shown(ChipType.ScreenRecord, bundle.screenRecord),
+                    remainingChips =
+                        bundle.copy(
+                            screenRecord = OngoingActivityChipModel.Hidden(),
+                            // Screen recording also activates the media projection APIs, which
+                            // means that whenever the screen recording chip is active, the
+                            // share-to-app chip would also be active. (Screen recording is a
+                            // special case of share-to-app, where the app receiving the share is
+                            // specifically System UI.)
+                            // We want only the screen-recording-specific chip to be shown in this
+                            // case. If we did have screen recording as the primary chip, we need to
+                            // suppress the share-to-app chip to make sure they don't both show.
+                            // See b/296461748.
+                            shareToApp = OngoingActivityChipModel.Hidden(),
+                        )
+                )
+            bundle.shareToApp is OngoingActivityChipModel.Shown ->
+                MostImportantChipResult(
+                    mostImportantChip =
+                        InternalChipModel.Shown(ChipType.ShareToApp, bundle.shareToApp),
+                    remainingChips = bundle.copy(shareToApp = OngoingActivityChipModel.Hidden()),
+                )
+            bundle.castToOtherDevice is OngoingActivityChipModel.Shown ->
+                MostImportantChipResult(
+                    mostImportantChip =
+                        InternalChipModel.Shown(
+                            ChipType.CastToOtherDevice,
+                            bundle.castToOtherDevice,
+                        ),
+                    remainingChips =
+                        bundle.copy(castToOtherDevice = OngoingActivityChipModel.Hidden()),
+                )
+            bundle.call is OngoingActivityChipModel.Shown ->
+                MostImportantChipResult(
+                    mostImportantChip = InternalChipModel.Shown(ChipType.Call, bundle.call),
+                    remainingChips = bundle.copy(call = OngoingActivityChipModel.Hidden()),
+                )
+            bundle.demoRon is OngoingActivityChipModel.Shown -> {
+                StatusBarRonChips.assertInNewMode()
+                MostImportantChipResult(
+                    mostImportantChip = InternalChipModel.Shown(ChipType.DemoRon, bundle.demoRon),
+                    remainingChips = bundle.copy(demoRon = OngoingActivityChipModel.Hidden()),
+                )
+            }
+            else -> {
+                // We should only get here if all chip types are hidden
+                check(bundle.screenRecord is OngoingActivityChipModel.Hidden)
+                check(bundle.shareToApp is OngoingActivityChipModel.Hidden)
+                check(bundle.castToOtherDevice is OngoingActivityChipModel.Hidden)
+                check(bundle.call is OngoingActivityChipModel.Hidden)
+                check(bundle.demoRon is OngoingActivityChipModel.Hidden)
+                MostImportantChipResult(
+                    mostImportantChip =
+                        InternalChipModel.Hidden(
+                            screenRecord = bundle.screenRecord,
+                            shareToApp = bundle.shareToApp,
+                            castToOtherDevice = bundle.castToOtherDevice,
+                            call = bundle.call,
+                            demoRon = bundle.demoRon,
+                        ),
+                    // All the chips are already hidden, so no need to filter anything out of the
+                    // bundle.
+                    remainingChips = bundle,
+                )
+            }
+        }
+    }
+
+    private fun createOutputModel(
+        old: InternalChipModel,
+        new: InternalChipModel,
+    ): OngoingActivityChipModel {
+        return if (old is InternalChipModel.Shown && new is InternalChipModel.Hidden) {
+            // If we're transitioning from showing the chip to hiding the chip, different
+            // chips require different animation behaviors. For example, the screen share
+            // chips shouldn't animate if the user stopped the screen share from the dialog
+            // (see b/353249803#comment4), but the call chip should always animate.
+            //
+            // This `when` block makes sure that when we're transitioning from Shown to
+            // Hidden, we check what chip type was previously showing and we use that chip
+            // type's hide animation behavior.
+            return when (old.type) {
+                ChipType.ScreenRecord -> new.screenRecord
+                ChipType.ShareToApp -> new.shareToApp
+                ChipType.CastToOtherDevice -> new.castToOtherDevice
+                ChipType.Call -> new.call
+                ChipType.DemoRon -> new.demoRon
+            }
+        } else if (new is InternalChipModel.Shown) {
+            // If we have a chip to show, always show it.
+            new.model
+        } else {
+            // In the Hidden -> Hidden transition, it shouldn't matter which hidden model we
+            // choose because no animation should happen regardless.
+            OngoingActivityChipModel.Hidden()
+        }
+    }
+
     companion object {
         private const val TAG = "ChipsViewModel"
 
@@ -203,5 +346,11 @@
                 call = OngoingActivityChipModel.Hidden(),
                 demoRon = OngoingActivityChipModel.Hidden(),
             )
+
+        private val DEFAULT_MULTIPLE_INTERNAL_HIDDEN_MODEL =
+            InternalMultipleOngoingActivityChipsModel(
+                primary = DEFAULT_INTERNAL_HIDDEN_MODEL,
+                secondary = DEFAULT_INTERNAL_HIDDEN_MODEL,
+            )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index 4a0fdee..659cee3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -55,6 +55,7 @@
 import com.android.systemui.statusbar.OperatorNameView;
 import com.android.systemui.statusbar.OperatorNameViewController;
 import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.chips.shared.StatusBarRonChips;
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger.DisableState;
 import com.android.systemui.statusbar.events.SystemStatusAnimationCallback;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
@@ -122,6 +123,7 @@
     private LinearLayout mEndSideContent;
     private View mClockView;
     private View mPrimaryOngoingActivityChip;
+    private View mSecondaryOngoingActivityChip;
     private View mNotificationIconAreaInner;
     // Visibilities come in from external system callers via disable flags, but we also sometimes
     // modify the visibilities internally. We need to store both so that we don't accidentally
@@ -212,9 +214,16 @@
     private boolean mHomeStatusBarAllowedByScene = true;
 
     /**
-     * True if there's an active ongoing activity that should be showing a chip and false otherwise.
+     * True if there's a primary active ongoing activity that should be showing a chip and false
+     * otherwise.
      */
-    private boolean mHasOngoingActivity;
+    private boolean mHasPrimaryOngoingActivity;
+
+    /**
+     * True if there's a secondary active ongoing activity that should be showing a chip and false
+     * otherwise.
+     */
+    private boolean mHasSecondaryOngoingActivity;
 
     /**
      * Listener that updates {@link #mWaitingForWindowStateChangeAfterCameraLaunch} when it receives
@@ -355,6 +364,8 @@
         mEndSideAlphaController = new MultiSourceMinAlphaController(mEndSideContent);
         mClockView = mStatusBar.findViewById(R.id.clock);
         mPrimaryOngoingActivityChip = mStatusBar.findViewById(R.id.ongoing_activity_chip_primary);
+        mSecondaryOngoingActivityChip =
+                mStatusBar.findViewById(R.id.ongoing_activity_chip_secondary);
         showEndSideContent(false);
         showClock(false);
         initOperatorName();
@@ -508,8 +519,11 @@
 
                 @Override
                 public void onOngoingActivityStatusChanged(
-                        boolean hasOngoingActivity, boolean shouldAnimate) {
-                    mHasOngoingActivity = hasOngoingActivity;
+                        boolean hasPrimaryOngoingActivity,
+                        boolean hasSecondaryOngoingActivity,
+                        boolean shouldAnimate) {
+                    mHasPrimaryOngoingActivity = hasPrimaryOngoingActivity;
+                    mHasSecondaryOngoingActivity = hasSecondaryOngoingActivity;
                     updateStatusBarVisibilities(shouldAnimate);
                 }
 
@@ -554,7 +568,7 @@
         boolean notifsChanged =
                 newModel.getShowNotificationIcons() != previousModel.getShowNotificationIcons();
         boolean ongoingActivityChanged =
-                newModel.getShowOngoingActivityChip() != previousModel.getShowOngoingActivityChip();
+                newModel.isOngoingActivityStatusDifferentFrom(previousModel);
         if (notifsChanged || ongoingActivityChanged) {
             updateNotificationIconAreaAndOngoingActivityChip(animate);
         }
@@ -597,21 +611,26 @@
 
         boolean showClock = externalModel.getShowClock() && !headsUpVisible;
 
-        boolean showOngoingActivityChip;
+        boolean showPrimaryOngoingActivityChip;
         if (Flags.statusBarScreenSharingChips()) {
             // If this flag is on, the ongoing activity status comes from
-            // CollapsedStatusBarViewBinder, which updates the mHasOngoingActivity variable.
-            showOngoingActivityChip = mHasOngoingActivity;
+            // CollapsedStatusBarViewBinder, which updates the mHasPrimaryOngoingActivity variable.
+            showPrimaryOngoingActivityChip = mHasPrimaryOngoingActivity;
         } else {
             // If this flag is off, the only ongoing activity is the ongoing call, and we pull it
             // from the controller directly.
-            showOngoingActivityChip = mOngoingCallController.hasOngoingCall();
+            showPrimaryOngoingActivityChip = mOngoingCallController.hasOngoingCall();
         }
+        boolean showSecondaryOngoingActivityChip =
+                Flags.statusBarScreenSharingChips()
+                        && StatusBarRonChips.isEnabled()
+                        && mHasSecondaryOngoingActivity;
 
         return new StatusBarVisibilityModel(
                 showClock,
                 externalModel.getShowNotificationIcons(),
-                showOngoingActivityChip && !headsUpVisible,
+                showPrimaryOngoingActivityChip && !headsUpVisible,
+                showSecondaryOngoingActivityChip && !headsUpVisible,
                 externalModel.getShowSystemInfo());
     }
 
@@ -622,7 +641,7 @@
     private void updateNotificationIconAreaAndOngoingActivityChip(boolean animate) {
         StatusBarVisibilityModel visibilityModel = mLastModifiedVisibility;
         boolean disableNotifications = !visibilityModel.getShowNotificationIcons();
-        boolean hasOngoingActivity = visibilityModel.getShowOngoingActivityChip();
+        boolean hasOngoingActivity = visibilityModel.getShowPrimaryOngoingActivityChip();
 
         // Hide notifications if the disable flag is set or we have an ongoing activity.
         if (disableNotifications || hasOngoingActivity) {
@@ -634,12 +653,24 @@
         // Show the ongoing activity chip only if there is an ongoing activity *and* notification
         // icons are allowed. (The ongoing activity chip occupies the same area as the notification,
         // icons so if the icons are disabled then the activity chip should be, too.)
-        boolean showOngoingActivityChip = hasOngoingActivity && !disableNotifications;
-        if (showOngoingActivityChip) {
+        boolean showPrimaryOngoingActivityChip =
+                visibilityModel.getShowPrimaryOngoingActivityChip() && !disableNotifications;
+        if (showPrimaryOngoingActivityChip) {
             showPrimaryOngoingActivityChip(animate);
         } else {
             hidePrimaryOngoingActivityChip(animate);
         }
+
+        boolean showSecondaryOngoingActivityChip =
+                // Secondary chips are only supported when RONs are enabled.
+                StatusBarRonChips.isEnabled()
+                        && visibilityModel.getShowSecondaryOngoingActivityChip()
+                        && !disableNotifications;
+        if (showSecondaryOngoingActivityChip) {
+            showSecondaryOngoingActivityChip(animate);
+        } else {
+            hideSecondaryOngoingActivityChip(animate);
+        }
     }
 
     private boolean shouldHideStatusBar() {
@@ -745,6 +776,15 @@
         animateShow(mPrimaryOngoingActivityChip, animate);
     }
 
+    private void hideSecondaryOngoingActivityChip(boolean animate) {
+        animateHiddenState(mSecondaryOngoingActivityChip, View.GONE, animate);
+    }
+
+    private void showSecondaryOngoingActivityChip(boolean animate) {
+        StatusBarRonChips.assertInNewMode();
+        animateShow(mSecondaryOngoingActivityChip, animate);
+    }
+
     /**
      * If panel is expanded/expanding it usually means QS shade is opening, so
      * don't set the clock GONE otherwise it'll mess up the animation.
@@ -850,6 +890,7 @@
 
     private void initOngoingCallChip() {
         mOngoingCallController.addCallback(mOngoingCallListener);
+        // TODO(b/364653005): Do we also need to set the secondary activity chip?
         mOngoingCallController.setChipView(mPrimaryOngoingActivityChip);
     }
 
@@ -908,7 +949,8 @@
     @Override
     public void dump(PrintWriter printWriter, String[] args) {
         IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, /* singleIndent= */"  ");
-        pw.println("mHasOngoingActivity=" + mHasOngoingActivity);
+        pw.println("mHasPrimaryOngoingActivity=" + mHasPrimaryOngoingActivity);
+        pw.println("mHasSecondaryOngoingActivity=" + mHasSecondaryOngoingActivity);
         pw.println("mAnimationsEnabled=" + mAnimationsEnabled);
         StatusBarFragmentComponent component = mStatusBarFragmentComponent;
         if (component == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt
index 0a19023..deef886 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt
@@ -16,16 +16,18 @@
 
 package com.android.systemui.statusbar.phone.fragment
 
-import com.android.systemui.log.dagger.CollapsedSbFragmentLog
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.CollapsedSbFragmentLog
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
 import javax.inject.Inject
 
 /** Used by [CollapsedStatusBarFragment] to log messages to a [LogBuffer]. */
-class CollapsedStatusBarFragmentLogger @Inject constructor(
-        @CollapsedSbFragmentLog private val buffer: LogBuffer,
-        private val disableFlagsLogger: DisableFlagsLogger,
+class CollapsedStatusBarFragmentLogger
+@Inject
+constructor(
+    @CollapsedSbFragmentLog private val buffer: LogBuffer,
+    private val disableFlagsLogger: DisableFlagsLogger,
 ) {
 
     /**
@@ -38,17 +40,17 @@
         new: DisableFlagsLogger.DisableState,
     ) {
         buffer.log(
-                TAG,
-                LogLevel.INFO,
-                {
-                    int1 = new.disable1
-                    int2 = new.disable2
-                },
-                {
-                    disableFlagsLogger.getDisableFlagsString(
-                        DisableFlagsLogger.DisableState(int1, int2),
-                    )
-                }
+            TAG,
+            LogLevel.INFO,
+            {
+                int1 = new.disable1
+                int2 = new.disable2
+            },
+            {
+                disableFlagsLogger.getDisableFlagsString(
+                    DisableFlagsLogger.DisableState(int1, int2),
+                )
+            }
         )
     }
 
@@ -59,13 +61,16 @@
             {
                 bool1 = model.showClock
                 bool2 = model.showNotificationIcons
-                bool3 = model.showOngoingActivityChip
+                bool3 = model.showPrimaryOngoingActivityChip
+                int1 = if (model.showSecondaryOngoingActivityChip) 1 else 0
                 bool4 = model.showSystemInfo
             },
-            { "New visibilities calculated internally. " +
+            {
+                "New visibilities calculated internally. " +
                     "showClock=$bool1 " +
                     "showNotificationIcons=$bool2 " +
-                    "showOngoingActivityChip=$bool3 " +
+                    "showPrimaryOngoingActivityChip=$bool3 " +
+                    "showSecondaryOngoingActivityChip=${if (int1 == 1) "true" else "false"}" +
                     "showSystemInfo=$bool4"
             }
         )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModel.kt
index 9255e63..e9e9a4e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModel.kt
@@ -26,9 +26,15 @@
 data class StatusBarVisibilityModel(
     val showClock: Boolean,
     val showNotificationIcons: Boolean,
-    val showOngoingActivityChip: Boolean,
+    val showPrimaryOngoingActivityChip: Boolean,
+    val showSecondaryOngoingActivityChip: Boolean,
     val showSystemInfo: Boolean,
 ) {
+    fun isOngoingActivityStatusDifferentFrom(other: StatusBarVisibilityModel): Boolean {
+        return this.showPrimaryOngoingActivityChip != other.showPrimaryOngoingActivityChip ||
+            this.showSecondaryOngoingActivityChip != other.showSecondaryOngoingActivityChip
+    }
+
     companion object {
         /** Creates the default model. */
         @JvmStatic
@@ -42,7 +48,8 @@
             return StatusBarVisibilityModel(
                 showClock = false,
                 showNotificationIcons = false,
-                showOngoingActivityChip = false,
+                showPrimaryOngoingActivityChip = false,
+                showSecondaryOngoingActivityChip = false,
                 showSystemInfo = false,
             )
         }
@@ -59,7 +66,8 @@
                 showNotificationIcons = (disabled1 and DISABLE_NOTIFICATION_ICONS) == 0,
                 // TODO(b/279899176): [CollapsedStatusBarFragment] always overwrites this with the
                 //  value of [OngoingCallController]. Do we need to process the flag here?
-                showOngoingActivityChip = (disabled1 and DISABLE_ONGOING_CALL_CHIP) == 0,
+                showPrimaryOngoingActivityChip = (disabled1 and DISABLE_ONGOING_CALL_CHIP) == 0,
+                showSecondaryOngoingActivityChip = (disabled1 and DISABLE_ONGOING_CALL_CHIP) == 0,
                 showSystemInfo =
                     (disabled1 and DISABLE_SYSTEM_INFO) == 0 &&
                         (disabled2 and DISABLE2_SYSTEM_ICONS) == 0
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
index 87d0e64..49eabba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
@@ -80,7 +80,7 @@
                     }
                 }
 
-                if (Flags.statusBarScreenSharingChips()) {
+                if (Flags.statusBarScreenSharingChips() && !Flags.statusBarRonChips()) {
                     val primaryChipView: View =
                         view.requireViewById(R.id.ongoing_activity_chip_primary)
                     launch {
@@ -89,12 +89,14 @@
                             when (primaryChipModel) {
                                 is OngoingActivityChipModel.Shown ->
                                     listener.onOngoingActivityStatusChanged(
-                                        hasOngoingActivity = true,
+                                        hasPrimaryOngoingActivity = true,
+                                        hasSecondaryOngoingActivity = false,
                                         shouldAnimate = true,
                                     )
                                 is OngoingActivityChipModel.Hidden ->
                                     listener.onOngoingActivityStatusChanged(
-                                        hasOngoingActivity = false,
+                                        hasPrimaryOngoingActivity = false,
+                                        hasSecondaryOngoingActivity = false,
                                         shouldAnimate = primaryChipModel.shouldAnimate,
                                     )
                             }
@@ -102,6 +104,29 @@
                     }
                 }
 
+                if (Flags.statusBarScreenSharingChips() && Flags.statusBarRonChips()) {
+                    val primaryChipView: View =
+                        view.requireViewById(R.id.ongoing_activity_chip_primary)
+                    val secondaryChipView: View =
+                        view.requireViewById(R.id.ongoing_activity_chip_secondary)
+                    launch {
+                        viewModel.ongoingActivityChips.collect { chips ->
+                            OngoingActivityChipBinder.bind(chips.primary, primaryChipView)
+                            // TODO(b/364653005): Don't show the secondary chip if there isn't
+                            // enough space for it.
+                            OngoingActivityChipBinder.bind(chips.secondary, secondaryChipView)
+                            listener.onOngoingActivityStatusChanged(
+                                hasPrimaryOngoingActivity =
+                                    chips.primary is OngoingActivityChipModel.Shown,
+                                hasSecondaryOngoingActivity =
+                                    chips.secondary is OngoingActivityChipModel.Shown,
+                                // TODO(b/364653005): Figure out the animation story here.
+                                shouldAnimate = true,
+                            )
+                        }
+                    }
+                }
+
                 if (SceneContainerFlag.isEnabled) {
                     launch {
                         viewModel.isHomeStatusBarAllowedByScene.collect {
@@ -161,7 +186,11 @@
      * @param shouldAnimate true if the chip should animate in/out, and false if the chip should
      *   immediately appear/disappear.
      */
-    fun onOngoingActivityStatusChanged(hasOngoingActivity: Boolean, shouldAnimate: Boolean)
+    fun onOngoingActivityStatusChanged(
+        hasPrimaryOngoingActivity: Boolean,
+        hasSecondaryOngoingActivity: Boolean,
+        shouldAnimate: Boolean,
+    )
 
     /**
      * Called when the scene state has changed such that the home status bar is newly allowed or no
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModel.kt
index 5474231..9cce2b8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModel.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModel
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
@@ -71,6 +72,12 @@
     val primaryOngoingActivityChip: StateFlow<OngoingActivityChipModel>
 
     /**
+     * The multiple ongoing activity chips that should be shown on the left-hand side of the status
+     * bar.
+     */
+    val ongoingActivityChips: StateFlow<MultipleOngoingActivityChipsModel>
+
+    /**
      * True if the current scene can show the home status bar (aka this status bar), and false if
      * the current scene should never show the home status bar.
      */
@@ -113,6 +120,8 @@
 
     override val primaryOngoingActivityChip = ongoingActivityChipsViewModel.primaryChip
 
+    override val ongoingActivityChips = ongoingActivityChipsViewModel.chips
+
     override val isHomeStatusBarAllowedByScene: StateFlow<Boolean> =
         combine(
                 sceneInteractor.currentScene,
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt
index e6e2a07..ee00e8b 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt
@@ -35,17 +35,3 @@
         }
         .onStart { emit(isReduceBrightColorsActivated) }
 }
-
-fun ReduceBrightColorsController.isAvailable(): Flow<Boolean> {
-    return conflatedCallbackFlow {
-            val callback =
-                object : ReduceBrightColorsController.Listener {
-                    override fun onFeatureEnabledChanged(enabled: Boolean) {
-                        trySend(enabled)
-                    }
-                }
-            addCallback(callback)
-            awaitClose { removeCallback(callback) }
-        }
-        .onStart { emit(isReduceBrightColorsFeatureAvailable) }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java
index d6bde27..1aff45b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java
@@ -18,6 +18,8 @@
 
 import static junit.framework.Assert.assertEquals;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -25,6 +27,7 @@
 
 import android.os.Handler;
 import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.service.quicksettings.Tile;
 import android.testing.TestableLooper;
 
@@ -35,6 +38,7 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.server.display.feature.flags.Flags;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.accessibility.extradim.ExtraDimDialogManager;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.qs.QSTile;
@@ -74,6 +78,8 @@
     private ReduceBrightColorsController mReduceBrightColorsController;
     @Mock
     private QsEventLogger mUiEventLogger;
+    @Mock
+    private ExtraDimDialogManager mExtraDimDialogManager;
 
     private TestableLooper mTestableLooper;
     private ReduceBrightColorsTile mTile;
@@ -97,7 +103,8 @@
                 mMetricsLogger,
                 mStatusBarStateController,
                 mActivityStarter,
-                mQSLogger
+                mQSLogger,
+                mExtraDimDialogManager
         );
 
         mTile.initialize();
@@ -148,6 +155,78 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER)
+    public void testDialogueShownOnClick() {
+        when(mReduceBrightColorsController.isReduceBrightColorsActivated()).thenReturn(true);
+        when(mReduceBrightColorsController.isInUpgradeMode(mContext.getResources()))
+                .thenReturn(true);
+        mTile = new ReduceBrightColorsTile(
+                true,
+                mReduceBrightColorsController,
+                mHost,
+                mUiEventLogger,
+                mTestableLooper.getLooper(),
+                new Handler(mTestableLooper.getLooper()),
+                new FalsingManagerFake(),
+                mMetricsLogger,
+                mStatusBarStateController,
+                mActivityStarter,
+                mQSLogger,
+                mExtraDimDialogManager
+        );
+
+        mTile.initialize();
+        mTestableLooper.processAllMessages();
+
+        // Validity check
+        assertEquals(Tile.STATE_ACTIVE, mTile.getState().state);
+        mTile.handleClick(null /* view */);
+
+        verify(mExtraDimDialogManager, times(1))
+                .dismissKeyguardIfNeededAndShowDialog(any());
+        verify(mReduceBrightColorsController, times(0))
+                .setReduceBrightColorsActivated(anyBoolean());
+        mTile.destroy();
+        mTestableLooper.processAllMessages();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER)
+    public void testDialogueShownOnLongClick() {
+        when(mReduceBrightColorsController.isReduceBrightColorsActivated()).thenReturn(true);
+        when(mReduceBrightColorsController.isInUpgradeMode(mContext.getResources()))
+                .thenReturn(true);
+        mTile = new ReduceBrightColorsTile(
+                true,
+                mReduceBrightColorsController,
+                mHost,
+                mUiEventLogger,
+                mTestableLooper.getLooper(),
+                new Handler(mTestableLooper.getLooper()),
+                new FalsingManagerFake(),
+                mMetricsLogger,
+                mStatusBarStateController,
+                mActivityStarter,
+                mQSLogger,
+                mExtraDimDialogManager
+        );
+
+        mTile.initialize();
+        mTestableLooper.processAllMessages();
+
+        // Validity check
+        assertEquals(Tile.STATE_ACTIVE, mTile.getState().state);
+        mTile.handleLongClick(null /* view */);
+
+        verify(mExtraDimDialogManager, times(1))
+                .dismissKeyguardIfNeededAndShowDialog(any());
+        verify(mReduceBrightColorsController, times(0))
+                .setReduceBrightColorsActivated(anyBoolean());
+        mTile.destroy();
+        mTestableLooper.processAllMessages();
+    }
+
+    @Test
     public void testIcon_whenTileEnabled_isOnState() {
         when(mReduceBrightColorsController.isReduceBrightColorsActivated()).thenReturn(true);
         mTile.refreshState();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
index f528ebb..26ce7b9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
@@ -19,8 +19,9 @@
 import android.content.DialogInterface
 import android.content.packageManager
 import android.content.pm.PackageManager
+import android.graphics.Bitmap
 import android.graphics.drawable.BitmapDrawable
-import android.platform.test.annotations.EnableFlags
+import android.platform.test.annotations.DisableFlags
 import android.view.View
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -39,11 +40,9 @@
 import com.android.systemui.screenrecord.data.repository.screenRecordRepository
 import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE
 import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
-import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.DemoRonChipViewModelTest.Companion.addDemoRonChip
 import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.demoRonChipViewModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
-import com.android.systemui.statusbar.commandline.commandRegistry
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
@@ -51,8 +50,6 @@
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel
 import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
-import java.io.PrintWriter
-import java.io.StringWriter
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -69,21 +66,22 @@
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
+/**
+ * Tests for [OngoingActivityChipsViewModel] when the [FLAG_STATUS_BAR_RON_CHIPS] flag is disabled.
+ */
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @OptIn(ExperimentalCoroutinesApi::class)
+@DisableFlags(FLAG_STATUS_BAR_RON_CHIPS)
 class OngoingActivityChipsViewModelTest : SysuiTestCase() {
     private val kosmos = Kosmos().also { it.testCase = this }
     private val testScope = kosmos.testScope
     private val systemClock = kosmos.fakeSystemClock
-    private val commandRegistry = kosmos.commandRegistry
 
     private val screenRecordState = kosmos.screenRecordRepository.screenRecordState
     private val mediaProjectionState = kosmos.fakeMediaProjectionRepository.mediaProjectionState
     private val callRepo = kosmos.ongoingCallRepository
 
-    private val pw = PrintWriter(StringWriter())
-
     private val mockSystemUIDialog = mock<SystemUIDialog>()
     private val chipBackgroundView = mock<ChipBackgroundContainer>()
     private val chipView =
@@ -102,8 +100,12 @@
     fun setUp() {
         setUpPackageManagerForMediaProjection(kosmos)
         kosmos.demoRonChipViewModel.start()
-        whenever(kosmos.packageManager.getApplicationIcon(any<String>()))
-            .thenReturn(BitmapDrawable())
+        val icon =
+            BitmapDrawable(
+                context.resources,
+                Bitmap.createBitmap(/* width= */ 100, /* height= */ 100, Bitmap.Config.ARGB_8888)
+            )
+        whenever(kosmos.packageManager.getApplicationIcon(any<String>())).thenReturn(icon)
     }
 
     @Test
@@ -183,24 +185,16 @@
         }
 
     @Test
-    @EnableFlags(FLAG_STATUS_BAR_RON_CHIPS)
     fun primaryChip_higherPriorityChipAdded_lowerPriorityChipReplaced() =
         testScope.runTest {
             // Start with just the lowest priority chip shown
-            addDemoRonChip(commandRegistry, pw)
+            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
             // And everything else hidden
-            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
             mediaProjectionState.value = MediaProjectionState.NotProjecting
             screenRecordState.value = ScreenRecordModel.DoingNothing
 
             val latest by collectLastValue(underTest.primaryChip)
 
-            assertIsDemoRonChip(latest)
-
-            // WHEN the higher priority call chip is added
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
-
-            // THEN the higher priority call chip is used
             assertIsCallChip(latest)
 
             // WHEN the higher priority media projection chip is added
@@ -222,7 +216,6 @@
         }
 
     @Test
-    @EnableFlags(FLAG_STATUS_BAR_RON_CHIPS)
     fun primaryChip_highestPriorityChipRemoved_showsNextPriorityChip() =
         testScope.runTest {
             // WHEN all chips are active
@@ -230,7 +223,6 @@
             mediaProjectionState.value =
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
             callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
-            addDemoRonChip(commandRegistry, pw)
 
             val latest by collectLastValue(underTest.primaryChip)
 
@@ -248,12 +240,6 @@
 
             // THEN the lower priority call is used
             assertIsCallChip(latest)
-
-            // WHEN the higher priority call is removed
-            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
-
-            // THEN the lower priority demo RON is used
-            assertIsDemoRonChip(latest)
         }
 
     /** Regression test for b/347726238. */
@@ -390,11 +376,5 @@
                     .impl as Icon.Resource
             assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone)
         }
-
-        fun assertIsDemoRonChip(latest: OngoingActivityChipModel?) {
-            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            assertThat((latest as OngoingActivityChipModel.Shown).icon)
-                .isInstanceOf(OngoingActivityChipModel.ChipIcon.FullColorAppIcon::class.java)
-        }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt
new file mode 100644
index 0000000..631120b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt
@@ -0,0 +1,534 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.ui.viewmodel
+
+import android.content.DialogInterface
+import android.content.packageManager
+import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
+import android.platform.test.annotations.EnableFlags
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_STATUS_BAR_RON_CHIPS
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.data.model.MediaProjectionState
+import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
+import com.android.systemui.res.R
+import com.android.systemui.screenrecord.data.model.ScreenRecordModel
+import com.android.systemui.screenrecord.data.repository.screenRecordRepository
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
+import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.DemoRonChipViewModelTest.Companion.addDemoRonChip
+import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.demoRonChipViewModel
+import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
+import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsCallChip
+import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsScreenRecordChip
+import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsShareToAppChip
+import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog
+import com.android.systemui.statusbar.commandline.commandRegistry
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel
+import com.android.systemui.testKosmos
+import com.android.systemui.util.time.fakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import java.io.PrintWriter
+import java.io.StringWriter
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/**
+ * Tests for [OngoingActivityChipsViewModel] when the [FLAG_STATUS_BAR_RON_CHIPS] flag is enabled.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+@EnableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+class OngoingActivityChipsWithRonsViewModelTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val systemClock = kosmos.fakeSystemClock
+    private val commandRegistry = kosmos.commandRegistry
+
+    private val screenRecordState = kosmos.screenRecordRepository.screenRecordState
+    private val mediaProjectionState = kosmos.fakeMediaProjectionRepository.mediaProjectionState
+    private val callRepo = kosmos.ongoingCallRepository
+
+    private val pw = PrintWriter(StringWriter())
+
+    private val mockSystemUIDialog = mock<SystemUIDialog>()
+    private val chipBackgroundView = mock<ChipBackgroundContainer>()
+    private val chipView =
+        mock<View>().apply {
+            whenever(
+                    this.requireViewById<ChipBackgroundContainer>(
+                        R.id.ongoing_activity_chip_background
+                    )
+                )
+                .thenReturn(chipBackgroundView)
+        }
+
+    private val underTest = kosmos.ongoingActivityChipsViewModel
+
+    @Before
+    fun setUp() {
+        setUpPackageManagerForMediaProjection(kosmos)
+        kosmos.demoRonChipViewModel.start()
+        val icon =
+            BitmapDrawable(
+                context.resources,
+                Bitmap.createBitmap(/* width= */ 100, /* height= */ 100, Bitmap.Config.ARGB_8888)
+            )
+        whenever(kosmos.packageManager.getApplicationIcon(any<String>())).thenReturn(icon)
+    }
+
+    // Even though the `primaryChip` flow isn't used when the RONs flag is on, still test that the
+    // flow has the right behavior to verify that we don't break any existing functionality.
+
+    @Test
+    fun primaryChip_allHidden_hidden() =
+        testScope.runTest {
+            screenRecordState.value = ScreenRecordModel.DoingNothing
+            mediaProjectionState.value = MediaProjectionState.NotProjecting
+            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+
+            val latest by collectLastValue(underTest.primaryChip)
+
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+        }
+
+    @Test
+    fun chips_allHidden_bothPrimaryAndSecondaryHidden() =
+        testScope.runTest {
+            screenRecordState.value = ScreenRecordModel.DoingNothing
+            mediaProjectionState.value = MediaProjectionState.NotProjecting
+            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+
+            val latest by collectLastValue(underTest.chips)
+
+            assertThat(latest!!.primary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+            assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+        }
+
+    @Test
+    fun primaryChip_screenRecordShow_restHidden_screenRecordShown() =
+        testScope.runTest {
+            screenRecordState.value = ScreenRecordModel.Recording
+            mediaProjectionState.value = MediaProjectionState.NotProjecting
+            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+
+            val latest by collectLastValue(underTest.primaryChip)
+
+            assertIsScreenRecordChip(latest)
+        }
+
+    @Test
+    fun chips_screenRecordShow_restHidden_primaryIsScreenRecordSecondaryIsHidden() =
+        testScope.runTest {
+            screenRecordState.value = ScreenRecordModel.Recording
+            mediaProjectionState.value = MediaProjectionState.NotProjecting
+            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+
+            val latest by collectLastValue(underTest.chips)
+
+            assertIsScreenRecordChip(latest!!.primary)
+            assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+        }
+
+    @Test
+    fun primaryChip_screenRecordShowAndCallShow_screenRecordShown() =
+        testScope.runTest {
+            screenRecordState.value = ScreenRecordModel.Recording
+            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+
+            val latest by collectLastValue(underTest.primaryChip)
+
+            assertIsScreenRecordChip(latest)
+        }
+
+    @Test
+    fun chips_screenRecordShowAndCallShow_primaryIsScreenRecordSecondaryIsCall() =
+        testScope.runTest {
+            screenRecordState.value = ScreenRecordModel.Recording
+            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+
+            val latest by collectLastValue(underTest.chips)
+
+            assertIsScreenRecordChip(latest!!.primary)
+            assertIsCallChip(latest!!.secondary)
+        }
+
+    @Test
+    fun primaryChip_screenRecordShowAndShareToAppShow_screenRecordShown() =
+        testScope.runTest {
+            screenRecordState.value = ScreenRecordModel.Recording
+            mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
+            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+
+            val latest by collectLastValue(underTest.primaryChip)
+
+            assertIsScreenRecordChip(latest)
+        }
+
+    @Test
+    fun chips_screenRecordShowAndShareToAppShow_primaryIsScreenRecordSecondaryIsHidden() =
+        testScope.runTest {
+            screenRecordState.value = ScreenRecordModel.Recording
+            mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
+            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+
+            val latest by collectLastValue(underTest.chips)
+
+            assertIsScreenRecordChip(latest!!.primary)
+            // Even though share-to-app is active, we suppress it because this share-to-app is
+            // represented by screen record being active. See b/296461748.
+            assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+        }
+
+    @Test
+    fun primaryChip_shareToAppShowAndCallShow_shareToAppShown() =
+        testScope.runTest {
+            screenRecordState.value = ScreenRecordModel.DoingNothing
+            mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
+            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+
+            val latest by collectLastValue(underTest.primaryChip)
+
+            assertIsShareToAppChip(latest)
+        }
+
+    @Test
+    fun chips_shareToAppShowAndCallShow_primaryIsShareToAppSecondaryIsCall() =
+        testScope.runTest {
+            screenRecordState.value = ScreenRecordModel.DoingNothing
+            mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
+            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+
+            val latest by collectLastValue(underTest.chips)
+
+            assertIsShareToAppChip(latest!!.primary)
+            assertIsCallChip(latest!!.secondary)
+        }
+
+    @Test
+    fun chips_threeActiveChips_topTwoShown() =
+        testScope.runTest {
+            screenRecordState.value = ScreenRecordModel.Recording
+            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+            addDemoRonChip(commandRegistry, pw)
+
+            val latest by collectLastValue(underTest.chips)
+
+            assertIsScreenRecordChip(latest!!.primary)
+            assertIsCallChip(latest!!.secondary)
+            // Demo RON chip is dropped
+        }
+
+    @Test
+    fun primaryChip_onlyCallShown_callShown() =
+        testScope.runTest {
+            screenRecordState.value = ScreenRecordModel.DoingNothing
+            // MediaProjection covers both share-to-app and cast-to-other-device
+            mediaProjectionState.value = MediaProjectionState.NotProjecting
+
+            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+
+            val latest by collectLastValue(underTest.primaryChip)
+
+            assertIsCallChip(latest)
+        }
+
+    @Test
+    fun chips_onlyCallShown_primaryIsCallSecondaryIsHidden() =
+        testScope.runTest {
+            screenRecordState.value = ScreenRecordModel.DoingNothing
+            // MediaProjection covers both share-to-app and cast-to-other-device
+            mediaProjectionState.value = MediaProjectionState.NotProjecting
+
+            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+
+            val latest by collectLastValue(underTest.chips)
+
+            assertIsCallChip(latest!!.primary)
+            assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+        }
+
+    @Test
+    fun primaryChip_higherPriorityChipAdded_lowerPriorityChipReplaced() =
+        testScope.runTest {
+            // Start with just the lowest priority chip shown
+            addDemoRonChip(commandRegistry, pw)
+            // And everything else hidden
+            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+            mediaProjectionState.value = MediaProjectionState.NotProjecting
+            screenRecordState.value = ScreenRecordModel.DoingNothing
+
+            val latest by collectLastValue(underTest.primaryChip)
+
+            assertIsDemoRonChip(latest)
+
+            // WHEN the higher priority call chip is added
+            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+
+            // THEN the higher priority call chip is used
+            assertIsCallChip(latest)
+
+            // WHEN the higher priority media projection chip is added
+            mediaProjectionState.value =
+                MediaProjectionState.Projecting.SingleTask(
+                    NORMAL_PACKAGE,
+                    hostDeviceName = null,
+                    createTask(taskId = 1),
+                )
+
+            // THEN the higher priority media projection chip is used
+            assertIsShareToAppChip(latest)
+
+            // WHEN the higher priority screen record chip is added
+            screenRecordState.value = ScreenRecordModel.Recording
+
+            // THEN the higher priority screen record chip is used
+            assertIsScreenRecordChip(latest)
+        }
+
+    @Test
+    fun primaryChip_highestPriorityChipRemoved_showsNextPriorityChip() =
+        testScope.runTest {
+            // WHEN all chips are active
+            screenRecordState.value = ScreenRecordModel.Recording
+            mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
+            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+            addDemoRonChip(commandRegistry, pw)
+
+            val latest by collectLastValue(underTest.primaryChip)
+
+            // THEN the highest priority screen record is used
+            assertIsScreenRecordChip(latest)
+
+            // WHEN the higher priority screen record is removed
+            screenRecordState.value = ScreenRecordModel.DoingNothing
+
+            // THEN the lower priority media projection is used
+            assertIsShareToAppChip(latest)
+
+            // WHEN the higher priority media projection is removed
+            mediaProjectionState.value = MediaProjectionState.NotProjecting
+
+            // THEN the lower priority call is used
+            assertIsCallChip(latest)
+
+            // WHEN the higher priority call is removed
+            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+
+            // THEN the lower priority demo RON is used
+            assertIsDemoRonChip(latest)
+        }
+
+    @Test
+    fun chips_movesChipsAroundAccordingToPriority() =
+        testScope.runTest {
+            // Start with just the lowest priority chip shown
+            addDemoRonChip(commandRegistry, pw)
+            // And everything else hidden
+            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+            mediaProjectionState.value = MediaProjectionState.NotProjecting
+            screenRecordState.value = ScreenRecordModel.DoingNothing
+
+            val latest by collectLastValue(underTest.chips)
+
+            assertIsDemoRonChip(latest!!.primary)
+            assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+
+            // WHEN the higher priority call chip is added
+            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+
+            // THEN the higher priority call chip is used as primary and demo ron is demoted to
+            // secondary
+            assertIsCallChip(latest!!.primary)
+            assertIsDemoRonChip(latest!!.secondary)
+
+            // WHEN the higher priority media projection chip is added
+            mediaProjectionState.value =
+                MediaProjectionState.Projecting.SingleTask(
+                    NORMAL_PACKAGE,
+                    hostDeviceName = null,
+                    createTask(taskId = 1),
+                )
+
+            // THEN the higher priority media projection chip is used as primary and call is demoted
+            // to secondary (and demo RON is dropped altogether)
+            assertIsShareToAppChip(latest!!.primary)
+            assertIsCallChip(latest!!.secondary)
+
+            // WHEN the higher priority screen record chip is added
+            screenRecordState.value = ScreenRecordModel.Recording
+
+            // THEN the higher priority screen record chip is used
+            assertIsScreenRecordChip(latest!!.primary)
+
+            // WHEN screen record and call is dropped
+            screenRecordState.value = ScreenRecordModel.DoingNothing
+            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+
+            // THEN media projection and demo RON remain
+            assertIsShareToAppChip(latest!!.primary)
+            assertIsDemoRonChip(latest!!.secondary)
+
+            // WHEN media projection is dropped
+            mediaProjectionState.value = MediaProjectionState.NotProjecting
+
+            // THEN demo RON is promoted to primary
+            assertIsDemoRonChip(latest!!.primary)
+            assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+        }
+
+    /** Regression test for b/347726238. */
+    @Test
+    fun primaryChip_timerDoesNotResetAfterSubscribersRestart() =
+        testScope.runTest {
+            var latest: OngoingActivityChipModel? = null
+
+            val job1 = underTest.primaryChip.onEach { latest = it }.launchIn(this)
+
+            // Start a chip with a timer
+            systemClock.setElapsedRealtime(1234)
+            screenRecordState.value = ScreenRecordModel.Recording
+
+            runCurrent()
+
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(1234)
+
+            // Stop subscribing to the chip flow
+            job1.cancel()
+
+            // Let time pass
+            systemClock.setElapsedRealtime(5678)
+
+            // WHEN we re-subscribe to the chip flow
+            val job2 = underTest.primaryChip.onEach { latest = it }.launchIn(this)
+
+            runCurrent()
+
+            // THEN the old start time is still used
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(1234)
+
+            job2.cancel()
+        }
+
+    /** Regression test for b/347726238. */
+    @Test
+    fun chips_timerDoesNotResetAfterSubscribersRestart() =
+        testScope.runTest {
+            var latest: MultipleOngoingActivityChipsModel? = null
+
+            val job1 = underTest.chips.onEach { latest = it }.launchIn(this)
+
+            // Start a chip with a timer
+            systemClock.setElapsedRealtime(1234)
+            screenRecordState.value = ScreenRecordModel.Recording
+
+            runCurrent()
+
+            val primaryChip = latest!!.primary as OngoingActivityChipModel.Shown.Timer
+            assertThat(primaryChip.startTimeMs).isEqualTo(1234)
+
+            // Stop subscribing to the chip flow
+            job1.cancel()
+
+            // Let time pass
+            systemClock.setElapsedRealtime(5678)
+
+            // WHEN we re-subscribe to the chip flow
+            val job2 = underTest.chips.onEach { latest = it }.launchIn(this)
+
+            runCurrent()
+
+            // THEN the old start time is still used
+            val newPrimaryChip = latest!!.primary as OngoingActivityChipModel.Shown.Timer
+            assertThat(newPrimaryChip.startTimeMs).isEqualTo(1234)
+
+            job2.cancel()
+        }
+
+    @Test
+    fun primaryChip_screenRecordStoppedViaDialog_chipHiddenWithoutAnimation() =
+        testScope.runTest {
+            screenRecordState.value = ScreenRecordModel.Recording
+            mediaProjectionState.value = MediaProjectionState.NotProjecting
+            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+
+            val latest by collectLastValue(underTest.primaryChip)
+
+            assertIsScreenRecordChip(latest)
+
+            // WHEN screen record gets stopped via dialog
+            val dialogStopAction =
+                getStopActionFromDialog(latest, chipView, mockSystemUIDialog, kosmos)
+            dialogStopAction.onClick(mock<DialogInterface>(), 0)
+
+            // THEN the chip is immediately hidden with no animation
+            assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false))
+        }
+
+    @Test
+    fun primaryChip_projectionStoppedViaDialog_chipHiddenWithoutAnimation() =
+        testScope.runTest {
+            mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
+            screenRecordState.value = ScreenRecordModel.DoingNothing
+            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+
+            val latest by collectLastValue(underTest.primaryChip)
+
+            assertIsShareToAppChip(latest)
+
+            // WHEN media projection gets stopped via dialog
+            val dialogStopAction =
+                getStopActionFromDialog(latest, chipView, mockSystemUIDialog, kosmos)
+            dialogStopAction.onClick(mock<DialogInterface>(), 0)
+
+            // THEN the chip is immediately hidden with no animation
+            assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false))
+        }
+
+    private fun assertIsDemoRonChip(latest: OngoingActivityChipModel?) {
+        assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+        assertThat((latest as OngoingActivityChipModel.Shown).icon)
+            .isInstanceOf(OngoingActivityChipModel.ChipIcon.FullColorAppIcon::class.java)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt
index 219b16f..d7fb129 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt
@@ -34,12 +34,13 @@
 @RunWith(AndroidJUnit4::class)
 class CollapsedStatusBarFragmentLoggerTest : SysuiTestCase() {
 
-    private val buffer = LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java))
-            .create("buffer", 10)
-    private val disableFlagsLogger = DisableFlagsLogger(
+    private val buffer =
+        LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java)).create("buffer", 10)
+    private val disableFlagsLogger =
+        DisableFlagsLogger(
             listOf(DisableFlagsLogger.DisableFlag(0b001, 'A', 'a')),
             listOf(DisableFlagsLogger.DisableFlag(0b001, 'B', 'b'))
-    )
+        )
     private val logger = CollapsedStatusBarFragmentLogger(buffer, disableFlagsLogger)
 
     @Test
@@ -66,7 +67,8 @@
             StatusBarVisibilityModel(
                 showClock = false,
                 showNotificationIcons = true,
-                showOngoingActivityChip = false,
+                showPrimaryOngoingActivityChip = false,
+                showSecondaryOngoingActivityChip = false,
                 showSystemInfo = true,
             )
         )
@@ -77,7 +79,8 @@
 
         assertThat(actualString).contains("showClock=false")
         assertThat(actualString).contains("showNotificationIcons=true")
-        assertThat(actualString).contains("showOngoingActivityChip=false")
+        assertThat(actualString).contains("showPrimaryOngoingActivityChip=false")
+        assertThat(actualString).contains("showSecondaryOngoingActivityChip=false")
         assertThat(actualString).contains("showSystemInfo=true")
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 6e337ef..135fab8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -16,6 +16,7 @@
 
 import static android.view.Display.DEFAULT_DISPLAY;
 
+import static com.android.systemui.Flags.FLAG_STATUS_BAR_RON_CHIPS;
 import static com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPEN;
@@ -526,7 +527,9 @@
 
         // WHEN there's *no* ongoing activity via new callback
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ false, /* shouldAnimate= */ false);
+                /* hasPrimaryOngoingActivity= */ false,
+                /* hasSecondaryOngoingActivity= */ false,
+                /* shouldAnimate= */ false);
 
         // THEN the old callback value is used, so the view is shown
         assertEquals(View.VISIBLE, getPrimaryOngoingActivityChipView().getVisibility());
@@ -535,12 +538,15 @@
         when(mOngoingCallController.hasOngoingCall()).thenReturn(false);
         fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
 
-        // WHEN there *is* an ongoing activity via new callback
+        // WHEN there *are* ongoing activities via new callback
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
+                /* hasPrimaryOngoingActivity= */ true,
+                /* hasSecondaryOngoingActivity= */ true,
+                /* shouldAnimate= */ false);
 
-        // THEN the old callback value is used, so the view is hidden
+        // THEN the old callback value is used, so the views are hidden
         assertEquals(View.GONE, getPrimaryOngoingActivityChipView().getVisibility());
+        assertEquals(View.GONE, getSecondaryOngoingActivityChipView().getVisibility());
     }
 
     @Test
@@ -553,18 +559,22 @@
         // listener, but I'm unable to get the fragment to get attached so that the binder starts
         // listening to flows.
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ false, /* shouldAnimate= */ false);
+                /* hasPrimaryOngoingActivity= */ false,
+                /* hasSecondaryOngoingActivity= */ false,
+                /* shouldAnimate= */ false);
 
         assertEquals(View.GONE, getPrimaryOngoingActivityChipView().getVisibility());
     }
 
     @Test
     @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    public void hasOngoingActivity_chipDisplayedAndNotificationIconsHidden() {
+    public void hasPrimaryOngoingActivity_primaryChipDisplayedAndNotificationIconsHidden() {
         resumeAndGetFragment();
 
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
+                /* hasPrimaryOngoingActivity= */ true,
+                /* hasSecondaryOngoingActivity= */ false,
+                /* shouldAnimate= */ false);
 
         assertEquals(View.VISIBLE, getPrimaryOngoingActivityChipView().getVisibility());
         assertEquals(View.INVISIBLE, getNotificationAreaView().getVisibility());
@@ -572,11 +582,42 @@
 
     @Test
     @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    public void hasOngoingActivityButNotificationIconsDisabled_chipHidden() {
+    @DisableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+    public void hasSecondaryOngoingActivity_butRonsFlagOff_secondaryChipHidden() {
+        resumeAndGetFragment();
+
+        mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
+                /* hasPrimaryOngoingActivity= */ true,
+                /* hasSecondaryOngoingActivity= */ true,
+                /* shouldAnimate= */ false);
+
+        assertEquals(View.GONE, getSecondaryOngoingActivityChipView().getVisibility());
+    }
+
+    @Test
+    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+    public void hasSecondaryOngoingActivity_flagOn_secondaryChipShownAndNotificationIconsHidden() {
+        resumeAndGetFragment();
+
+        mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
+                /* hasPrimaryOngoingActivity= */ true,
+                /* hasSecondaryOngoingActivity= */ true,
+                /* shouldAnimate= */ false);
+
+        assertEquals(View.VISIBLE, getSecondaryOngoingActivityChipView().getVisibility());
+        assertEquals(View.INVISIBLE, getNotificationAreaView().getVisibility());
+    }
+
+    @Test
+    @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+    @DisableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+    public void hasOngoingActivityButNotificationIconsDisabled_chipHidden_ronsFlagOff() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
+                /* hasPrimaryOngoingActivity= */ true,
+                /* hasSecondaryOngoingActivity= */ false,
+                /* shouldAnimate= */ false);
 
         fragment.disable(DEFAULT_DISPLAY,
                 StatusBarManager.DISABLE_NOTIFICATION_ICONS, 0, false);
@@ -585,12 +626,32 @@
     }
 
     @Test
-    @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    public void hasOngoingActivityButAlsoHun_chipHidden() {
+    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+    public void hasOngoingActivitiesButNotificationIconsDisabled_chipsHidden_ronsFlagOn() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
+                /* hasPrimaryOngoingActivity= */ true,
+                /* hasSecondaryOngoingActivity= */ true,
+                /* shouldAnimate= */ false);
+
+        fragment.disable(DEFAULT_DISPLAY,
+                StatusBarManager.DISABLE_NOTIFICATION_ICONS, 0, false);
+
+        assertEquals(View.GONE, getPrimaryOngoingActivityChipView().getVisibility());
+        assertEquals(View.GONE, getSecondaryOngoingActivityChipView().getVisibility());
+    }
+
+    @Test
+    @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+    @DisableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+    public void hasOngoingActivityButAlsoHun_chipHidden_ronsFlagOff() {
+        CollapsedStatusBarFragment fragment = resumeAndGetFragment();
+
+        mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
+                /* hasPrimaryOngoingActivity= */ true,
+                /* hasSecondaryOngoingActivity= */ false,
+                /* shouldAnimate= */ false);
         when(mHeadsUpAppearanceController.shouldBeVisible()).thenReturn(true);
 
         fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
@@ -599,33 +660,120 @@
     }
 
     @Test
+    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+    public void hasOngoingActivitiesButAlsoHun_chipsHidden_ronsFlagOn() {
+        CollapsedStatusBarFragment fragment = resumeAndGetFragment();
+
+        mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
+                /* hasPrimaryOngoingActivity= */ true,
+                /* hasSecondaryOngoingActivity= */ true,
+                /* shouldAnimate= */ false);
+        when(mHeadsUpAppearanceController.shouldBeVisible()).thenReturn(true);
+
+        fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
+
+        assertEquals(View.GONE, getPrimaryOngoingActivityChipView().getVisibility());
+        assertEquals(View.GONE, getSecondaryOngoingActivityChipView().getVisibility());
+    }
+
+    @Test
     @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    public void ongoingActivityEnded_chipHidden() {
+    @DisableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+    public void primaryOngoingActivityEnded_chipHidden_ronsFlagOff() {
         resumeAndGetFragment();
 
         // Ongoing activity started
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
+                /* hasPrimaryOngoingActivity= */ true,
+                /* hasSecondaryOngoingActivity= */ false,
+                /* shouldAnimate= */ false);
 
         assertEquals(View.VISIBLE, getPrimaryOngoingActivityChipView().getVisibility());
 
         // Ongoing activity ended
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ false, /* shouldAnimate= */ false);
+                /* hasPrimaryOngoingActivity= */ false,
+                /* hasSecondaryOngoingActivity= */ false,
+                /* shouldAnimate= */ false);
 
         assertEquals(View.GONE, getPrimaryOngoingActivityChipView().getVisibility());
     }
 
     @Test
+    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+    public void primaryOngoingActivityEnded_chipHidden_ronsFlagOn() {
+        resumeAndGetFragment();
+
+        // Ongoing activity started
+        mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
+                /* hasPrimaryOngoingActivity= */ true,
+                /* hasSecondaryOngoingActivity= */ false,
+                /* shouldAnimate= */ false);
+
+        assertEquals(View.VISIBLE, getPrimaryOngoingActivityChipView().getVisibility());
+
+        // Ongoing activity ended
+        mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
+                /* hasPrimaryOngoingActivity= */ false,
+                /* hasSecondaryOngoingActivity= */ false,
+                /* shouldAnimate= */ false);
+
+        assertEquals(View.GONE, getPrimaryOngoingActivityChipView().getVisibility());
+    }
+
+    @Test
+    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+    public void secondaryOngoingActivityEnded_chipHidden() {
+        resumeAndGetFragment();
+
+        // Secondary ongoing activity started
+        mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
+                /* hasPrimaryOngoingActivity= */ true,
+                /* hasSecondaryOngoingActivity= */ true,
+                /* shouldAnimate= */ false);
+
+        assertEquals(View.VISIBLE, getSecondaryOngoingActivityChipView().getVisibility());
+
+        // Ongoing activity ended
+        mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
+                /* hasPrimaryOngoingActivity= */ true,
+                /* hasSecondaryOngoingActivity= */ false,
+                /* shouldAnimate= */ false);
+
+        assertEquals(View.GONE, getSecondaryOngoingActivityChipView().getVisibility());
+    }
+
+    @Test
     @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    public void hasOngoingActivity_hidesNotifsWithoutAnimation() {
+    @DisableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+    public void hasOngoingActivity_hidesNotifsWithoutAnimation_ronsFlagOff() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
         // Enable animations for testing so that we can verify we still aren't animating
         fragment.enableAnimationsForTesting();
 
-        // Ongoing call started
+        // Ongoing activity started
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
+                /* hasPrimaryOngoingActivity= */ true,
+                /* hasSecondaryOngoingActivity= */ false,
+                /* shouldAnimate= */ false);
+
+        // Notification area is hidden without delay
+        assertEquals(0f, getNotificationAreaView().getAlpha(), 0.01);
+        assertEquals(View.INVISIBLE, getNotificationAreaView().getVisibility());
+    }
+
+    @Test
+    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+    public void hasOngoingActivity_hidesNotifsWithoutAnimation_ronsFlagOn() {
+        CollapsedStatusBarFragment fragment = resumeAndGetFragment();
+        // Enable animations for testing so that we can verify we still aren't animating
+        fragment.enableAnimationsForTesting();
+
+        // Ongoing activity started
+        mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
+                /* hasPrimaryOngoingActivity= */ true,
+                /* hasSecondaryOngoingActivity= */ false,
+                /* shouldAnimate= */ false);
 
         // Notification area is hidden without delay
         assertEquals(0f, getNotificationAreaView().getAlpha(), 0.01);
@@ -634,7 +782,8 @@
 
     @Test
     @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    public void screenSharingChipsEnabled_ignoresOngoingCallController() {
+    @DisableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+    public void screenSharingChipsEnabled_ignoresOngoingCallController_ronsFlagOff() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
         // WHEN there *is* an ongoing call via old callback
@@ -643,7 +792,9 @@
 
         // WHEN there's *no* ongoing activity via new callback
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ false, /* shouldAnimate= */ false);
+                /* hasPrimaryOngoingActivity= */ false,
+                /* hasSecondaryOngoingActivity= */ false,
+                /* shouldAnimate= */ false);
 
         // THEN the new callback value is used, so the view is hidden
         assertEquals(View.GONE, getPrimaryOngoingActivityChipView().getVisibility());
@@ -652,15 +803,50 @@
         when(mOngoingCallController.hasOngoingCall()).thenReturn(false);
         fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
 
-        // WHEN there *is* an ongoing activity via new callback
+        // WHEN there *are* ongoing activities via new callback
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
+                /* hasPrimaryOngoingActivity= */ true,
+                /* hasSecondaryOngoingActivity= */ false,
+                /* shouldAnimate= */ false);
 
-        // THEN the new callback value is used, so the view is shown
+        // THEN the new callback value is used, so the views are shown
         assertEquals(View.VISIBLE, getPrimaryOngoingActivityChipView().getVisibility());
     }
 
     @Test
+    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+    public void screenSharingChipsEnabled_ignoresOngoingCallController_ronsFlagOn() {
+        CollapsedStatusBarFragment fragment = resumeAndGetFragment();
+
+        // WHEN there *is* an ongoing call via old callback
+        when(mOngoingCallController.hasOngoingCall()).thenReturn(true);
+        fragment.disable(DEFAULT_DISPLAY, 0, 0, true);
+
+        // WHEN there's *no* ongoing activity via new callback
+        mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
+                /* hasPrimaryOngoingActivity= */ false,
+                /* hasSecondaryOngoingActivity= */ false,
+                /* shouldAnimate= */ false);
+
+        // THEN the new callback value is used, so the view is hidden
+        assertEquals(View.GONE, getPrimaryOngoingActivityChipView().getVisibility());
+
+        // WHEN there's *no* ongoing call via old callback
+        when(mOngoingCallController.hasOngoingCall()).thenReturn(false);
+        fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
+
+        // WHEN there *are* ongoing activities via new callback
+        mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
+                /* hasPrimaryOngoingActivity= */ true,
+                /* hasSecondaryOngoingActivity= */ true,
+                /* shouldAnimate= */ false);
+
+        // THEN the new callback value is used, so the views are shown
+        assertEquals(View.VISIBLE, getPrimaryOngoingActivityChipView().getVisibility());
+        assertEquals(View.VISIBLE, getSecondaryOngoingActivityChipView().getVisibility());
+    }
+
+    @Test
     @EnableSceneContainer
     public void isHomeStatusBarAllowedByScene_false_everythingHidden() {
         resumeAndGetFragment();
@@ -1010,4 +1196,8 @@
     private View getPrimaryOngoingActivityChipView() {
         return mFragment.getView().findViewById(R.id.ongoing_activity_chip_primary);
     }
+
+    private View getSecondaryOngoingActivityChipView() {
+        return mFragment.getView().findViewById(R.id.ongoing_activity_chip_secondary);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModelTest.kt
index 9f6f51a..d47a903 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModelTest.kt
@@ -39,7 +39,8 @@
             StatusBarVisibilityModel(
                 showClock = true,
                 showNotificationIcons = true,
-                showOngoingActivityChip = true,
+                showPrimaryOngoingActivityChip = true,
+                showSecondaryOngoingActivityChip = true,
                 showSystemInfo = true,
             )
 
@@ -75,17 +76,19 @@
     }
 
     @Test
-    fun createModelFromFlags_ongoingCallChipNotDisabled_showOngoingActivityChipTrue() {
+    fun createModelFromFlags_ongoingCallChipNotDisabled_showOngoingActivityChipsTrue() {
         val result = createModelFromFlags(disabled1 = 0, disabled2 = 0)
 
-        assertThat(result.showOngoingActivityChip).isTrue()
+        assertThat(result.showPrimaryOngoingActivityChip).isTrue()
+        assertThat(result.showSecondaryOngoingActivityChip).isTrue()
     }
 
     @Test
-    fun createModelFromFlags_ongoingCallChipDisabled_showOngoingActivityChipFalse() {
+    fun createModelFromFlags_ongoingCallChipDisabled_showOngoingActivityChipsFalse() {
         val result = createModelFromFlags(disabled1 = DISABLE_ONGOING_CALL_CHIP, disabled2 = 0)
 
-        assertThat(result.showOngoingActivityChip).isFalse()
+        assertThat(result.showPrimaryOngoingActivityChip).isFalse()
+        assertThat(result.showSecondaryOngoingActivityChip).isFalse()
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt
index e71f521..4834d36 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel
 
+import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
@@ -31,6 +32,8 @@
     override val primaryOngoingActivityChip: MutableStateFlow<OngoingActivityChipModel> =
         MutableStateFlow(OngoingActivityChipModel.Hidden())
 
+    override val ongoingActivityChips = MutableStateFlow(MultipleOngoingActivityChipsModel())
+
     override val isHomeStatusBarAllowedByScene = MutableStateFlow(false)
 
     override fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> = areNotificationLightsOut
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/FakeReduceBrightColorsController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/FakeReduceBrightColorsController.kt
index e02042d..ab745ef 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/FakeReduceBrightColorsController.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/FakeReduceBrightColorsController.kt
@@ -47,14 +47,6 @@
         }
     }
 
-    override fun setReduceBrightColorsFeatureAvailable(enabled: Boolean) {
-        // do nothing
-    }
-
-    override fun isReduceBrightColorsFeatureAvailable(): Boolean {
-        return true
-    }
-
     override fun isInUpgradeMode(resources: Resources?): Boolean {
         if (resources != null) {
             return Flags.evenDimmer() &&
diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java
index 39aa27a..b1c25c4 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java
@@ -18,11 +18,8 @@
 
 import android.annotation.NonNull;
 import android.app.appsearch.AppSearchBatchResult;
-import android.app.appsearch.AppSearchManager;
-import android.app.appsearch.AppSearchManager.SearchContext;
 import android.app.appsearch.AppSearchResult;
 import android.app.appsearch.AppSearchSession;
-import android.app.appsearch.BatchResultCallback;
 import android.app.appsearch.GenericDocument;
 import android.app.appsearch.GetByDocumentIdRequest;
 import android.app.appsearch.GetSchemaResponse;
@@ -33,7 +30,6 @@
 import android.app.appsearch.SearchSpec;
 import android.app.appsearch.SetSchemaRequest;
 import android.app.appsearch.SetSchemaResponse;
-import android.util.Slog;
 
 import com.android.internal.infra.AndroidFuture;
 
@@ -44,28 +40,11 @@
 import java.util.concurrent.Executor;
 
 /** A future API wrapper of {@link AppSearchSession} APIs. */
-public class FutureAppSearchSession implements Closeable {
-    private static final String TAG = FutureAppSearchSession.class.getSimpleName();
-    private final Executor mExecutor;
-    private final AndroidFuture<AppSearchResult<AppSearchSession>> mSettableSessionFuture;
-
-    public FutureAppSearchSession(
-            @NonNull AppSearchManager appSearchManager,
-            @NonNull Executor executor,
-            @NonNull SearchContext appSearchContext) {
-        Objects.requireNonNull(appSearchManager);
-        Objects.requireNonNull(executor);
-        Objects.requireNonNull(appSearchContext);
-
-        mExecutor = executor;
-        mSettableSessionFuture = new AndroidFuture<>();
-        appSearchManager.createSearchSession(
-                appSearchContext, mExecutor, mSettableSessionFuture::complete);
-    }
+public interface FutureAppSearchSession extends Closeable {
 
     /** Converts a failed app search result codes into an exception. */
     @NonNull
-    public static Exception failedResultToException(@NonNull AppSearchResult<?> appSearchResult) {
+    static Exception failedResultToException(@NonNull AppSearchResult<?> appSearchResult) {
         return switch (appSearchResult.getResultCode()) {
             case AppSearchResult.RESULT_INVALID_ARGUMENT ->
                     new IllegalArgumentException(appSearchResult.getErrorMessage());
@@ -77,114 +56,39 @@
         };
     }
 
-    private AndroidFuture<AppSearchSession> getSessionAsync() {
-        return mSettableSessionFuture.thenApply(
-                result -> {
-                    if (result.isSuccess()) {
-                        return result.getResultValue();
-                    } else {
-                        throw new RuntimeException(failedResultToException(result));
-                    }
-                });
-    }
+    /**
+     * Sets the schema that represents the organizational structure of data within the AppSearch
+     * database.
+     */
+    AndroidFuture<SetSchemaResponse> setSchema(@NonNull SetSchemaRequest setSchemaRequest);
 
-    /** Gets the schema for a given app search session. */
-    public AndroidFuture<GetSchemaResponse> getSchema() {
-        return getSessionAsync()
-                .thenCompose(
-                        session -> {
-                            AndroidFuture<AppSearchResult<GetSchemaResponse>>
-                                    settableSchemaResponse = new AndroidFuture<>();
-                            session.getSchema(mExecutor, settableSchemaResponse::complete);
-                            return settableSchemaResponse.thenApply(
-                                    result -> {
-                                        if (result.isSuccess()) {
-                                            return result.getResultValue();
-                                        } else {
-                                            throw new RuntimeException(
-                                                    failedResultToException(result));
-                                        }
-                                    });
-                        });
-    }
+    /** Retrieves the schema most recently successfully provided to {@code setSchema}. */
+    AndroidFuture<GetSchemaResponse> getSchema();
 
-    /** Sets the schema for a given app search session. */
-    public AndroidFuture<SetSchemaResponse> setSchema(@NonNull SetSchemaRequest setSchemaRequest) {
-        return getSessionAsync()
-                .thenCompose(
-                        session -> {
-                            AndroidFuture<AppSearchResult<SetSchemaResponse>>
-                                    settableSchemaResponse = new AndroidFuture<>();
-                            session.setSchema(
-                                    setSchemaRequest,
-                                    mExecutor,
-                                    mExecutor,
-                                    settableSchemaResponse::complete);
-                            return settableSchemaResponse.thenApply(
-                                    result -> {
-                                        if (result.isSuccess()) {
-                                            return result.getResultValue();
-                                        } else {
-                                            throw new RuntimeException(
-                                                    failedResultToException(result));
-                                        }
-                                    });
-                        });
-    }
+    /** Indexes documents into the {@link AppSearchSession} database. */
+    AndroidFuture<AppSearchBatchResult<String, Void>> put(
+            @NonNull PutDocumentsRequest putDocumentsRequest);
 
-    /** Indexes documents into the AppSearchSession database. */
-    public AndroidFuture<AppSearchBatchResult<String, Void>> put(
-            @NonNull PutDocumentsRequest putDocumentsRequest) {
-        return getSessionAsync()
-                .thenCompose(
-                        session -> {
-                            AndroidFuture<AppSearchBatchResult<String, Void>> batchResultFuture =
-                                    new AndroidFuture<>();
-
-                            session.put(
-                                    putDocumentsRequest, mExecutor, batchResultFuture::complete);
-                            return batchResultFuture;
-                        });
-    }
-
-    /** Removes documents from the AppSearchSession database. */
-    public AndroidFuture<AppSearchBatchResult<String, Void>> remove(
-            @NonNull RemoveByDocumentIdRequest removeRequest) {
-        return getSessionAsync()
-                .thenCompose(
-                        session -> {
-                            AndroidFuture<AppSearchBatchResult<String, Void>>
-                                    settableBatchResultFuture = new AndroidFuture<>();
-                            session.remove(
-                                    removeRequest,
-                                    mExecutor,
-                                    new BatchResultCallbackAdapter<>(settableBatchResultFuture));
-                            return settableBatchResultFuture;
-                        });
-    }
+    /** Removes {@link GenericDocument} from the index by Query. */
+    AndroidFuture<AppSearchBatchResult<String, Void>> remove(
+            @NonNull RemoveByDocumentIdRequest removeRequest);
 
     /**
-     * Retrieves documents from the open AppSearchSession that match a given query string and type
-     * of search provided.
+     * Gets {@link GenericDocument} objects by document IDs in a namespace from the {@link
+     * AppSearchSession} database.
      */
-    public AndroidFuture<FutureSearchResults> search(
-            @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
-        return getSessionAsync()
-                .thenApply(session -> session.search(queryExpression, searchSpec))
-                .thenApply(result -> new FutureSearchResults(result, mExecutor));
-    }
+    AndroidFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentId(
+            GetByDocumentIdRequest getRequest);
 
-    @Override
-    public void close() throws IOException {
-        try {
-            getSessionAsync().get().close();
-        } catch (Exception ex) {
-            Slog.e(TAG, "Failed to close app search session", ex);
-        }
-    }
+    /**
+     * Retrieves documents from the open {@link AppSearchSession} that match a given query string
+     * and type of search provided.
+     */
+    AndroidFuture<FutureSearchResults> search(
+            @NonNull String queryExpression, @NonNull SearchSpec searchSpec);
 
     /** A future API wrapper of {@link android.app.appsearch.SearchResults}. */
-    public static class FutureSearchResults {
+    class FutureSearchResults {
         private final SearchResults mSearchResults;
         private final Executor mExecutor;
 
@@ -209,56 +113,4 @@
                     });
         }
     }
-
-    /** A future API to retrieve a document by its id from the local AppSearch session. */
-    public AndroidFuture<GenericDocument> getByDocumentId(
-            @NonNull String documentId, @NonNull String namespace) {
-        Objects.requireNonNull(documentId);
-        Objects.requireNonNull(namespace);
-
-        GetByDocumentIdRequest request =
-                new GetByDocumentIdRequest.Builder(namespace).addIds(documentId).build();
-        return getSessionAsync()
-                .thenCompose(
-                        session -> {
-                            AndroidFuture<AppSearchBatchResult<String, GenericDocument>>
-                                    batchResultFuture = new AndroidFuture<>();
-                            session.getByDocumentId(
-                                    request,
-                                    mExecutor,
-                                    new BatchResultCallbackAdapter<>(batchResultFuture));
-
-                            return batchResultFuture.thenApply(
-                                    batchResult ->
-                                            getGenericDocumentFromBatchResult(
-                                                    batchResult, documentId));
-                        });
-    }
-
-    private static GenericDocument getGenericDocumentFromBatchResult(
-            AppSearchBatchResult<String, GenericDocument> result, String documentId) {
-        if (result.isSuccess()) {
-            return result.getSuccesses().get(documentId);
-        }
-        throw new IllegalArgumentException("No document in the result for id: " + documentId);
-    }
-
-    private static final class BatchResultCallbackAdapter<K, V>
-            implements BatchResultCallback<K, V> {
-        private final AndroidFuture<AppSearchBatchResult<K, V>> mFuture;
-
-        BatchResultCallbackAdapter(AndroidFuture<AppSearchBatchResult<K, V>> future) {
-            mFuture = future;
-        }
-
-        @Override
-        public void onResult(@NonNull AppSearchBatchResult<K, V> result) {
-            mFuture.complete(result);
-        }
-
-        @Override
-        public void onSystemError(Throwable t) {
-            mFuture.completeExceptionally(t);
-        }
-    }
 }
diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSessionImpl.java b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSessionImpl.java
new file mode 100644
index 0000000..e78f390
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSessionImpl.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appfunctions;
+
+import static com.android.server.appfunctions.FutureAppSearchSession.failedResultToException;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchManager.SearchContext;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.AppSearchSession;
+import android.app.appsearch.BatchResultCallback;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.GetByDocumentIdRequest;
+import android.app.appsearch.GetSchemaResponse;
+import android.app.appsearch.PutDocumentsRequest;
+import android.app.appsearch.RemoveByDocumentIdRequest;
+import android.app.appsearch.SearchSpec;
+import android.app.appsearch.SetSchemaRequest;
+import android.app.appsearch.SetSchemaResponse;
+
+import com.android.internal.infra.AndroidFuture;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/** Implementation of {@link FutureAppSearchSession} */
+public class FutureAppSearchSessionImpl implements FutureAppSearchSession {
+
+    private static final String TAG = FutureAppSearchSession.class.getSimpleName();
+    private final Executor mExecutor;
+    private final AndroidFuture<AppSearchResult<AppSearchSession>> mSettableSessionFuture;
+
+    public FutureAppSearchSessionImpl(
+            @NonNull AppSearchManager appSearchManager,
+            @NonNull Executor executor,
+            @NonNull SearchContext appSearchContext) {
+        Objects.requireNonNull(appSearchManager);
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(appSearchContext);
+
+        mExecutor = executor;
+        mSettableSessionFuture = new AndroidFuture<>();
+        appSearchManager.createSearchSession(
+                appSearchContext, mExecutor, mSettableSessionFuture::complete);
+    }
+
+    private AndroidFuture<AppSearchSession> getSessionAsync() {
+        return mSettableSessionFuture.thenApply(
+                result -> {
+                    if (result.isSuccess()) {
+                        return result.getResultValue();
+                    } else {
+                        throw new RuntimeException(failedResultToException(result));
+                    }
+                });
+    }
+
+    @Override
+    public AndroidFuture<SetSchemaResponse> setSchema(@NonNull SetSchemaRequest setSchemaRequest) {
+        Objects.requireNonNull(setSchemaRequest);
+
+        return getSessionAsync()
+                .thenCompose(
+                        session -> {
+                            AndroidFuture<AppSearchResult<SetSchemaResponse>>
+                                    settableSchemaResponse = new AndroidFuture<>();
+                            session.setSchema(
+                                    setSchemaRequest,
+                                    mExecutor,
+                                    mExecutor,
+                                    settableSchemaResponse::complete);
+                            return settableSchemaResponse.thenApply(
+                                    result -> {
+                                        if (result.isSuccess()) {
+                                            return result.getResultValue();
+                                        } else {
+                                            throw new RuntimeException(
+                                                    failedResultToException(result));
+                                        }
+                                    });
+                        });
+    }
+
+    @Override
+    public AndroidFuture<GetSchemaResponse> getSchema() {
+        return getSessionAsync()
+                .thenCompose(
+                        session -> {
+                            AndroidFuture<AppSearchResult<GetSchemaResponse>>
+                                    settableSchemaResponse = new AndroidFuture<>();
+                            session.getSchema(mExecutor, settableSchemaResponse::complete);
+                            return settableSchemaResponse.thenApply(
+                                    result -> {
+                                        if (result.isSuccess()) {
+                                            return result.getResultValue();
+                                        } else {
+                                            throw new RuntimeException(
+                                                    failedResultToException(result));
+                                        }
+                                    });
+                        });
+    }
+
+    @Override
+    public AndroidFuture<AppSearchBatchResult<String, Void>> put(
+            @NonNull PutDocumentsRequest putDocumentsRequest) {
+        Objects.requireNonNull(putDocumentsRequest);
+
+        return getSessionAsync()
+                .thenCompose(
+                        session -> {
+                            AndroidFuture<AppSearchBatchResult<String, Void>> batchResultFuture =
+                                    new AndroidFuture<>();
+
+                            session.put(
+                                    putDocumentsRequest, mExecutor, batchResultFuture::complete);
+                            return batchResultFuture;
+                        });
+    }
+
+    @Override
+    public AndroidFuture<AppSearchBatchResult<String, Void>> remove(
+            @NonNull RemoveByDocumentIdRequest removeRequest) {
+        Objects.requireNonNull(removeRequest);
+
+        return getSessionAsync()
+                .thenCompose(
+                        session -> {
+                            AndroidFuture<AppSearchBatchResult<String, Void>>
+                                    settableBatchResultFuture = new AndroidFuture<>();
+                            session.remove(
+                                    removeRequest,
+                                    mExecutor,
+                                    new BatchResultCallbackAdapter<>(settableBatchResultFuture));
+                            return settableBatchResultFuture;
+                        });
+    }
+
+    @Override
+    public AndroidFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentId(
+            @NonNull GetByDocumentIdRequest getRequest) {
+        Objects.requireNonNull(getRequest);
+
+        return getSessionAsync()
+                .thenCompose(
+                        session -> {
+                            AndroidFuture<AppSearchBatchResult<String, GenericDocument>>
+                                    batchResultFuture = new AndroidFuture<>();
+                            session.getByDocumentId(
+                                    getRequest,
+                                    mExecutor,
+                                    new BatchResultCallbackAdapter<>(batchResultFuture));
+                            return batchResultFuture;
+                        });
+    }
+
+    @Override
+    public AndroidFuture<FutureSearchResults> search(
+            @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
+        return getSessionAsync()
+                .thenApply(session -> session.search(queryExpression, searchSpec))
+                .thenApply(result -> new FutureSearchResults(result, mExecutor));
+    }
+
+    @Override
+    public void close() throws IOException {}
+
+    private static final class BatchResultCallbackAdapter<K, V>
+            implements BatchResultCallback<K, V> {
+        private final AndroidFuture<AppSearchBatchResult<K, V>> mFuture;
+
+        BatchResultCallbackAdapter(AndroidFuture<AppSearchBatchResult<K, V>> future) {
+            mFuture = future;
+        }
+
+        @Override
+        public void onResult(@NonNull AppSearchBatchResult<K, V> result) {
+            mFuture.complete(result);
+        }
+
+        @Override
+        public void onSystemError(Throwable t) {
+            mFuture.completeExceptionally(t);
+        }
+    }
+}
diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt
index 3bc4411..edcbb9e 100644
--- a/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt
@@ -21,6 +21,8 @@
 import android.app.appfunctions.AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema
 import android.app.appsearch.AppSearchBatchResult
 import android.app.appsearch.AppSearchManager
+import android.app.appsearch.GenericDocument
+import android.app.appsearch.GetByDocumentIdRequest
 import android.app.appsearch.PutDocumentsRequest
 import android.app.appsearch.RemoveByDocumentIdRequest
 import android.app.appsearch.SearchSpec
@@ -44,16 +46,16 @@
     @After
     fun clearData() {
         val searchContext = AppSearchManager.SearchContext.Builder(TEST_DB).build()
-        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
+        FutureAppSearchSessionImpl(appSearchManager, testExecutor, searchContext).use {
             val setSchemaRequest = SetSchemaRequest.Builder().setForceOverride(true).build()
-            it.setSchema(setSchemaRequest)
+            it.setSchema(setSchemaRequest).get()
         }
     }
 
     @Test
     fun setSchema() {
         val searchContext = AppSearchManager.SearchContext.Builder(TEST_DB).build()
-        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { session ->
+        FutureAppSearchSessionImpl(appSearchManager, testExecutor, searchContext).use { session ->
             val setSchemaRequest =
                 SetSchemaRequest.Builder()
                     .addSchemas(
@@ -71,7 +73,7 @@
     @Test
     fun put() {
         val searchContext = AppSearchManager.SearchContext.Builder(TEST_DB).build()
-        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { session ->
+        FutureAppSearchSessionImpl(appSearchManager, testExecutor, searchContext).use { session ->
             val setSchemaRequest =
                 SetSchemaRequest.Builder()
                     .addSchemas(
@@ -97,7 +99,7 @@
     @Test
     fun remove() {
         val searchContext = AppSearchManager.SearchContext.Builder(TEST_DB).build()
-        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { session ->
+        FutureAppSearchSessionImpl(appSearchManager, testExecutor, searchContext).use { session ->
             val setSchemaRequest =
                 SetSchemaRequest.Builder()
                     .addSchemas(
@@ -131,7 +133,7 @@
     @Test
     fun search() {
         val searchContext = AppSearchManager.SearchContext.Builder(TEST_DB).build()
-        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { session ->
+        FutureAppSearchSessionImpl(appSearchManager, testExecutor, searchContext).use { session ->
             val setSchemaRequest =
                 SetSchemaRequest.Builder()
                     .addSchemas(
@@ -163,7 +165,7 @@
     @Test
     fun getByDocumentId() {
         val searchContext = AppSearchManager.SearchContext.Builder(TEST_DB).build()
-        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { session ->
+        FutureAppSearchSessionImpl(appSearchManager, testExecutor, searchContext).use { session ->
             val setSchemaRequest =
                 SetSchemaRequest.Builder()
                     .addSchemas(
@@ -171,24 +173,24 @@
                         createAppFunctionRuntimeSchema(TEST_PACKAGE_NAME),
                     )
                     .build()
-            val schema = session.setSchema(setSchemaRequest)
+            session.setSchema(setSchemaRequest).get()
             val appFunctionRuntimeMetadata =
                 AppFunctionRuntimeMetadata.Builder(TEST_PACKAGE_NAME, TEST_FUNCTION_ID, "").build()
             val putDocumentsRequest: PutDocumentsRequest =
                 PutDocumentsRequest.Builder()
                     .addGenericDocuments(appFunctionRuntimeMetadata)
                     .build()
-            val putResult = session.put(putDocumentsRequest)
+            session.put(putDocumentsRequest)
+            val getRequest =
+                GetByDocumentIdRequest.Builder(APP_FUNCTION_RUNTIME_NAMESPACE)
+                    .addIds(appFunctionRuntimeMetadata.id)
+                    .build()
 
-            val genricDocument =
-                session
-                    .getByDocumentId(
-                        /* documentId= */ "${TEST_PACKAGE_NAME}/${TEST_FUNCTION_ID}",
-                        APP_FUNCTION_RUNTIME_NAMESPACE,
-                    )
-                    .get()
+            val genericDocument: GenericDocument? =
+                session.getByDocumentId(getRequest).get().successes[appFunctionRuntimeMetadata.id]
 
-            val foundAppFunctionRuntimeMetadata = AppFunctionRuntimeMetadata(genricDocument)
+            assertThat(genericDocument).isNotNull()
+            val foundAppFunctionRuntimeMetadata = AppFunctionRuntimeMetadata(genericDocument!!)
             assertThat(foundAppFunctionRuntimeMetadata.functionId).isEqualTo(TEST_FUNCTION_ID)
         }
     }
diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/FutureGlobalSearchSessionTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureGlobalSearchSessionTest.kt
index 1fa55c7..38cba65 100644
--- a/services/tests/appfunctions/src/com/android/server/appfunctions/FutureGlobalSearchSessionTest.kt
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureGlobalSearchSessionTest.kt
@@ -46,9 +46,9 @@
     @After
     fun clearData() {
         val searchContext = SearchContext.Builder(TEST_DB).build()
-        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
+        FutureAppSearchSessionImpl(appSearchManager, testExecutor, searchContext).use {
             val setSchemaRequest = SetSchemaRequest.Builder().setForceOverride(true).build()
-            it.setSchema(setSchemaRequest)
+            it.setSchema(setSchemaRequest).get()
         }
     }
 
@@ -83,7 +83,7 @@
         assertThat(registerPackageObserver).isNull()
         // Trigger document change
         val searchContext = SearchContext.Builder(TEST_DB).build()
-        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { session ->
+        FutureAppSearchSessionImpl(appSearchManager, testExecutor, searchContext).use { session ->
             val setSchemaRequest =
                 SetSchemaRequest.Builder()
                     .addSchemas(
diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
index 5769e07..3ebf689 100644
--- a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
@@ -41,9 +41,9 @@
     @After
     fun clearData() {
         val searchContext = SearchContext.Builder(TEST_DB).build()
-        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
+        FutureAppSearchSessionImpl(appSearchManager, testExecutor, searchContext).use {
             val setSchemaRequest = SetSchemaRequest.Builder().setForceOverride(true).build()
-            it.setSchema(setSchemaRequest)
+            it.setSchema(setSchemaRequest).get()
         }
     }
 
@@ -61,7 +61,7 @@
                 .build()
         val putDocumentsRequest: PutDocumentsRequest =
             PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build()
-        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
+        FutureAppSearchSessionImpl(appSearchManager, testExecutor, searchContext).use {
             val setSchemaResponse = it.setSchema(setSchemaRequest).get()
             assertThat(setSchemaResponse).isNotNull()
             val appSearchBatchResult = it.put(putDocumentsRequest).get()
@@ -71,7 +71,7 @@
         val metadataSyncAdapter =
             MetadataSyncAdapter(
                 testExecutor,
-                FutureAppSearchSession(appSearchManager, testExecutor, searchContext),
+                FutureAppSearchSessionImpl(appSearchManager, testExecutor, searchContext),
             )
         val packageToFunctionIdMap =
             metadataSyncAdapter.getPackageToFunctionIdMap(
@@ -111,7 +111,7 @@
                     functionRuntimeMetadata3,
                 )
                 .build()
-        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
+        FutureAppSearchSessionImpl(appSearchManager, testExecutor, searchContext).use {
             val setSchemaResponse = it.setSchema(setSchemaRequest).get()
             assertThat(setSchemaResponse).isNotNull()
             val appSearchBatchResult = it.put(putDocumentsRequest).get()
@@ -121,7 +121,7 @@
         val metadataSyncAdapter =
             MetadataSyncAdapter(
                 testExecutor,
-                FutureAppSearchSession(appSearchManager, testExecutor, searchContext),
+                FutureAppSearchSessionImpl(appSearchManager, testExecutor, searchContext),
             )
         val packageToFunctionIdMap =
             metadataSyncAdapter.getPackageToFunctionIdMap(