Merge "Adds a flag to force the Nav Bar handle to remain visible"
diff --git a/Android.bp b/Android.bp
index a7ac094..f6e0104 100644
--- a/Android.bp
+++ b/Android.bp
@@ -409,6 +409,7 @@
 filegroup {
     name: "libincident_aidl",
     srcs: [
+        "core/java/android/os/IIncidentDumpCallback.aidl",
         "core/java/android/os/IIncidentManager.aidl",
         "core/java/android/os/IIncidentReportStatusListener.aidl",
     ],
@@ -491,8 +492,7 @@
         "framework-sdkextensions-stubs-systemapi",
         // TODO(b/146167933): Use framework-statsd-stubs instead.
         "framework-statsd",
-        // TODO(b/140299412): should be framework-wifi-stubs
-        "framework-wifi",
+        "framework-wifi-stubs",
         "ike-stubs",
         // TODO(b/147200698): should be the stub of framework-tethering
         "framework-tethering",
@@ -528,7 +528,8 @@
         "compat-changeid-annotation-processor",
     ],
     static_libs: [
-        "exoplayer2-core"
+        "exoplayer2-core",
+        "android.hardware.wifi-V1.0-java-constants",
     ]
 }
 
@@ -637,6 +638,7 @@
     name: "framework-ike-shared-srcs",
     visibility: ["//frameworks/opt/net/ike"],
     srcs: [
+        "core/java/android/annotation/StringDef.java",
         "core/java/android/net/annotations/PolicyDirection.java",
         "core/java/com/android/internal/util/IState.java",
         "core/java/com/android/internal/util/State.java",
@@ -1124,6 +1126,7 @@
         "core/java/android/util/TimeUtils.java",
         "core/java/com/android/internal/os/SomeArgs.java",
         "core/java/com/android/internal/util/AsyncChannel.java",
+        "core/java/com/android/internal/util/AsyncService.java",
         "core/java/com/android/internal/util/BitwiseInputStream.java",
         "core/java/com/android/internal/util/FastXmlSerializer.java",
         "core/java/com/android/internal/util/HexDump.java",
diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearch.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearch.java
new file mode 100644
index 0000000..e779b69
--- /dev/null
+++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearch.java
@@ -0,0 +1,762 @@
+/*
+ * Copyright (C) 2020 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.app.appsearch;
+
+import android.annotation.CurrentTimeSecondsLong;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+
+import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.PropertyProto;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Collection of all AppSearch Document Types.
+ *
+ * @hide
+ */
+// TODO(b/143789408) Spilt this class to make all subclasses to their own file.
+public final class AppSearch {
+
+    private AppSearch() {}
+    /**
+     * Represents a document unit.
+     *
+     * <p>Documents are constructed via {@link Document.Builder}.
+     *
+     * @hide
+     */
+    // TODO(b/143789408) set TTL for document in mProtoBuilder
+    // TODO(b/144518768) add visibility field if the stakeholders are comfortable with a no-op
+    //  opt-in for this release.
+    public static class Document {
+        private static final String TAG = "AppSearch.Document";
+
+        /**
+         * The maximum number of elements in a repeatable field. Will reject the request if exceed
+         * this limit.
+         */
+        private static final int MAX_REPEATED_PROPERTY_LENGTH = 100;
+
+        /**
+         * The maximum {@link String#length} of a {@link String} field. Will reject the request if
+         * {@link String}s longer than this.
+         */
+        private static final int MAX_STRING_LENGTH = 20_000;
+
+        /**
+         * Contains {@link Document} basic information (uri, schemaType etc) and properties ordered
+         * by keys.
+         */
+        @NonNull
+        private final DocumentProto mProto;
+
+        /** Contains all properties in {@link #mProto} to support get properties via keys. */
+        @NonNull
+        private final Bundle mPropertyBundle;
+
+        /**
+         * Create a new {@link Document}.
+         * @param proto Contains {@link Document} basic information (uri, schemaType etc) and
+         *               properties ordered by keys.
+         * @param propertyBundle Contains all properties in {@link #mProto} to support get
+         *                        properties via keys.
+         */
+        private Document(@NonNull DocumentProto proto, @NonNull Bundle propertyBundle) {
+            this.mProto = proto;
+            this.mPropertyBundle = propertyBundle;
+        }
+
+        /**
+         * Create a new {@link Document} from an existing instance.
+         *
+         * <p>This method should be only used by constructor of a subclass.
+         */
+        // TODO(b/143789408) add constructor take DocumentProto to create a document.
+        protected Document(@NonNull Document document) {
+            this(document.mProto, document.mPropertyBundle);
+        }
+
+        /**
+         * Creates a new {@link Document.Builder}.
+         *
+         * @param uri The uri of {@link Document}.
+         * @param schemaType The schema type of the {@link Document}. The passed-in
+         *     {@code schemaType} must be defined using {@link AppSearchManager#setSchema} prior to
+         *     inserting a document of this {@code schemaType} into the AppSearch index using
+         *     {@link AppSearchManager#put}. Otherwise, the document will be rejected by
+         *     {@link AppSearchManager#put}.
+         * @hide
+         */
+        @NonNull
+        public static Builder newBuilder(@NonNull String uri, @NonNull String schemaType) {
+            return new Builder(uri, schemaType);
+        }
+
+        /**
+         * Get the {@link DocumentProto} of the {@link Document}.
+         *
+         * <p>The {@link DocumentProto} contains {@link Document}'s basic information and all
+         *    properties ordered by keys.
+         * @hide
+         */
+        @NonNull
+        @VisibleForTesting
+        public DocumentProto getProto() {
+            return mProto;
+        }
+
+        /**
+         * Get the uri of the {@link Document}.
+         *
+         * @hide
+         */
+        @NonNull
+        public String getUri() {
+            return mProto.getUri();
+        }
+
+        /**
+         * Get the schema type of the {@link Document}.
+         * @hide
+         */
+        @NonNull
+        public String getSchemaType() {
+            return mProto.getSchema();
+        }
+
+        /**
+         * Get the creation timestamp in seconds of the {@link Document}.
+         *
+         * @hide
+         */
+        // TODO(b/143789408) Change seconds to millis with Icing library.
+        @CurrentTimeSecondsLong
+        public long getCreationTimestampSecs() {
+            return mProto.getCreationTimestampSecs();
+        }
+
+        /**
+         * Returns the score of the {@link Document}.
+         *
+         * <p>The score is a query-independent measure of the document's quality, relative to other
+         * {@link Document}s of the same type.
+         *
+         * <p>The default value is 0.
+         *
+         * @hide
+         */
+        public int getScore() {
+            return mProto.getScore();
+        }
+
+        /**
+         * Retrieve a {@link String} value by key.
+         *
+         * @param key The key to look for.
+         * @return The first {@link String} associated with the given key or {@code null} if there
+         *     is no such key or the value is of a different type.
+         * @hide
+         */
+        @Nullable
+        public String getPropertyString(@NonNull String key) {
+            String[] propertyArray = getPropertyStringArray(key);
+            if (ArrayUtils.isEmpty(propertyArray)) {
+                return null;
+            }
+            warnIfSinglePropertyTooLong("String", key, propertyArray.length);
+            return propertyArray[0];
+        }
+
+        /**
+         * Retrieve a {@link Long} value by key.
+         *
+         * @param key The key to look for.
+         * @return The first {@link Long} associated with the given key or {@code null} if there
+         *     is no such key or the value is of a different type.
+         * @hide
+         */
+        @Nullable
+        public Long getPropertyLong(@NonNull String key) {
+            long[] propertyArray = getPropertyLongArray(key);
+            if (ArrayUtils.isEmpty(propertyArray)) {
+                return null;
+            }
+            warnIfSinglePropertyTooLong("Long", key, propertyArray.length);
+            return propertyArray[0];
+        }
+
+        /**
+         * Retrieve a {@link Double} value by key.
+         *
+         * @param key The key to look for.
+         * @return The first {@link Double} associated with the given key or {@code null} if there
+         *     is no such key or the value is of a different type.
+         * @hide
+         */
+        @Nullable
+        public Double getPropertyDouble(@NonNull String key) {
+            double[] propertyArray = getPropertyDoubleArray(key);
+            // TODO(tytytyww): Add support double array to ArraysUtils.isEmpty().
+            if (propertyArray == null || propertyArray.length == 0) {
+                return null;
+            }
+            warnIfSinglePropertyTooLong("Double", key, propertyArray.length);
+            return propertyArray[0];
+        }
+
+        /**
+         * Retrieve a {@link Boolean} value by key.
+         *
+         * @param key The key to look for.
+         * @return The first {@link Boolean} associated with the given key or {@code null} if there
+         *     is no such key or the value is of a different type.
+         * @hide
+         */
+        @Nullable
+        public Boolean getPropertyBoolean(@NonNull String key) {
+            boolean[] propertyArray = getPropertyBooleanArray(key);
+            if (ArrayUtils.isEmpty(propertyArray)) {
+                return null;
+            }
+            warnIfSinglePropertyTooLong("Boolean", key, propertyArray.length);
+            return propertyArray[0];
+        }
+
+        /** Prints a warning to logcat if the given propertyLength is greater than 1. */
+        private static void warnIfSinglePropertyTooLong(
+                @NonNull String propertyType, @NonNull String key, int propertyLength) {
+            if (propertyLength > 1) {
+                Log.w(TAG, "The value for \"" + key + "\" contains " + propertyLength
+                        + " elements. Only the first one will be returned from "
+                        + "getProperty" + propertyType + "(). Try getProperty" + propertyType
+                        + "Array().");
+            }
+        }
+
+        /**
+         * Retrieve a repeated {@code String} property by key.
+         *
+         * @param key The key to look for.
+         * @return The {@code String[]} associated with the given key, or {@code null} if no value
+         *     is set or the value is of a different type.
+         * @hide
+         */
+        @Nullable
+        public String[] getPropertyStringArray(@NonNull String key) {
+            return getAndCastPropertyArray(key, String[].class);
+        }
+
+        /**
+         * Retrieve a repeated {@code long} property by key.
+         *
+         * @param key The key to look for.
+         * @return The {@code long[]} associated with the given key, or {@code null} if no value is
+         *     set or the value is of a different type.
+         * @hide
+         */
+        @Nullable
+        public long[] getPropertyLongArray(@NonNull String key) {
+            return getAndCastPropertyArray(key, long[].class);
+        }
+
+        /**
+         * Retrieve a repeated {@code double} property by key.
+         *
+         * @param key The key to look for.
+         * @return The {@code double[]} associated with the given key, or {@code null} if no value
+         *     is set or the value is of a different type.
+         * @hide
+         */
+        @Nullable
+        public double[] getPropertyDoubleArray(@NonNull String key) {
+            return getAndCastPropertyArray(key, double[].class);
+        }
+
+        /**
+         * Retrieve a repeated {@code boolean} property by key.
+         *
+         * @param key The key to look for.
+         * @return The {@code boolean[]} associated with the given key, or {@code null} if no value
+         *     is set or the value is of a different type.
+         * @hide
+         */
+        @Nullable
+        public boolean[] getPropertyBooleanArray(@NonNull String key) {
+            return getAndCastPropertyArray(key, boolean[].class);
+        }
+
+        /**
+         * Gets a repeated property of the given key, and casts it to the given class type, which
+         * must be an array class type.
+         */
+        @Nullable
+        private <T> T getAndCastPropertyArray(@NonNull String key, @NonNull Class<T> tClass) {
+            Object value = mPropertyBundle.get(key);
+            if (value == null) {
+                return null;
+            }
+            try {
+                return tClass.cast(value);
+            } catch (ClassCastException e) {
+                Log.w(TAG, "Error casting to requested type for key \"" + key + "\"", e);
+                return null;
+            }
+        }
+
+        @Override
+        public boolean equals(@Nullable Object other) {
+            // Check only proto's equality is sufficient here since all properties in
+            // mPropertyBundle are ordered by keys and stored in proto.
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof Document)) {
+                return false;
+            }
+            Document otherDocument = (Document) other;
+            return this.mProto.equals(otherDocument.mProto);
+        }
+
+        @Override
+        public int hashCode() {
+            // Hash only proto is sufficient here since all properties in mPropertyBundle are
+            // ordered by keys and stored in proto.
+            return mProto.hashCode();
+        }
+
+        @Override
+        public String toString() {
+            return mProto.toString();
+        }
+
+        /**
+         * The builder class for {@link Document}.
+         *
+         * @param <BuilderType> Type of subclass who extend this.
+         * @hide
+         */
+        public static class Builder<BuilderType extends Builder> {
+
+            private final Bundle mPropertyBundle = new Bundle();
+            private final DocumentProto.Builder mProtoBuilder = DocumentProto.newBuilder();
+            private final BuilderType mBuilderTypeInstance;
+
+            /**
+             * Create a new {@link Document.Builder}.
+             *
+             * @param uri The uri of {@link Document}.
+             * @param schemaType The schema type of the {@link Document}. The passed-in
+             *     {@code schemaType} must be defined using {@link AppSearchManager#setSchema} prior
+             *     to inserting a document of this {@code schemaType} into the AppSearch index using
+             *     {@link AppSearchManager#put}. Otherwise, the document will be rejected by
+             *     {@link AppSearchManager#put}.
+             * @hide
+             */
+            protected Builder(@NonNull String uri, @NonNull String schemaType) {
+                mBuilderTypeInstance = (BuilderType) this;
+                mProtoBuilder.setUri(uri).setSchema(schemaType);
+                 // Set current timestamp for creation timestamp by default.
+                setCreationTimestampSecs(
+                        TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
+            }
+
+            /**
+             * Set the score of the {@link Document}.
+             *
+             * <p>The score is a query-independent measure of the document's quality, relative to
+             * other {@link Document}s of the same type.
+             *
+             * @throws IllegalArgumentException If the provided value is negative.
+             * @hide
+             */
+            @NonNull
+            public BuilderType setScore(@IntRange(from = 0, to = Integer.MAX_VALUE) int score) {
+                if (score < 0) {
+                    throw new IllegalArgumentException("Document score cannot be negative");
+                }
+                mProtoBuilder.setScore(score);
+                return mBuilderTypeInstance;
+            }
+
+            /**
+             * Set the creation timestamp in seconds of the {@link Document}.
+             *
+             * @hide
+             */
+            @NonNull
+            public BuilderType setCreationTimestampSecs(
+                    @CurrentTimeSecondsLong long creationTimestampSecs) {
+                mProtoBuilder.setCreationTimestampSecs(creationTimestampSecs);
+                return mBuilderTypeInstance;
+            }
+
+            /**
+             * Sets one or multiple {@code String} values for a property, replacing its previous
+             * values.
+             *
+             * @param key The key associated with the {@code values}.
+             * @param values The {@code String} values of the property.
+             * @hide
+             */
+            @NonNull
+            public BuilderType setProperty(@NonNull String key, @NonNull String... values) {
+                putInBundle(mPropertyBundle, key, values);
+                return mBuilderTypeInstance;
+            }
+
+            /**
+             * Sets one or multiple {@code boolean} values for a property, replacing its previous
+             * values.
+             *
+             * @param key The key associated with the {@code values}.
+             * @param values The {@code boolean} values of the schema.org property.
+             * @hide
+             */
+            @NonNull
+            public BuilderType setProperty(@NonNull String key, @NonNull boolean... values) {
+                putInBundle(mPropertyBundle, key, values);
+                return mBuilderTypeInstance;
+            }
+
+            /**
+             * Sets one or multiple {@code long} values for a property, replacing its previous
+             * values.
+             *
+             * @param key The key associated with the {@code values}.
+             * @param values The {@code long} values of the schema.org property.
+             * @hide
+             */
+            @NonNull
+            public BuilderType setProperty(@NonNull String key, @NonNull long... values) {
+                putInBundle(mPropertyBundle, key, values);
+                return mBuilderTypeInstance;
+            }
+
+            /**
+             * Sets one or multiple {@code double} values for a property, replacing its previous
+             * values.
+             *
+             * @param key The key associated with the {@code values}.
+             * @param values The {@code double} values of the schema.org property.
+             * @hide
+             */
+            @NonNull
+            public BuilderType setProperty(@NonNull String key, @NonNull double... values) {
+                putInBundle(mPropertyBundle, key, values);
+                return mBuilderTypeInstance;
+            }
+
+            private static void putInBundle(
+                    @NonNull Bundle bundle, @NonNull String key, @NonNull String... values)
+                    throws IllegalArgumentException {
+                Objects.requireNonNull(key);
+                Objects.requireNonNull(values);
+                validateRepeatedPropertyLength(key, values.length);
+                for (int i = 0; i < values.length; i++) {
+                    if (values[i] == null) {
+                        throw new IllegalArgumentException("The String at " + i + " is null.");
+                    } else if (values[i].length() > MAX_STRING_LENGTH) {
+                        throw new IllegalArgumentException("The String at " + i + " length is: "
+                                + values[i].length()  + ", which exceeds length limit: "
+                                + MAX_STRING_LENGTH + ".");
+                    }
+                }
+                bundle.putStringArray(key, values);
+            }
+
+            private static void putInBundle(
+                    @NonNull Bundle bundle, @NonNull String key, @NonNull boolean... values) {
+                Objects.requireNonNull(key);
+                Objects.requireNonNull(values);
+                validateRepeatedPropertyLength(key, values.length);
+                bundle.putBooleanArray(key, values);
+            }
+
+            private static void putInBundle(
+                    @NonNull Bundle bundle, @NonNull String key, @NonNull double... values) {
+                Objects.requireNonNull(key);
+                Objects.requireNonNull(values);
+                validateRepeatedPropertyLength(key, values.length);
+                bundle.putDoubleArray(key, values);
+            }
+
+            private static void putInBundle(
+                    @NonNull Bundle bundle, @NonNull String key, @NonNull long... values) {
+                Objects.requireNonNull(key);
+                Objects.requireNonNull(values);
+                validateRepeatedPropertyLength(key, values.length);
+                bundle.putLongArray(key, values);
+            }
+
+            private static void validateRepeatedPropertyLength(@NonNull String key, int length) {
+                if (length == 0) {
+                    throw new IllegalArgumentException("The input array is empty.");
+                } else if (length > MAX_REPEATED_PROPERTY_LENGTH) {
+                    throw new IllegalArgumentException(
+                            "Repeated property \"" + key + "\" has length " + length
+                                    + ", which exceeds the limit of "
+                                    + MAX_REPEATED_PROPERTY_LENGTH);
+                }
+            }
+
+            /**
+             * Builds the {@link Document} object.
+             * @hide
+             */
+            public Document build() {
+                // Build proto by sorting the keys in propertyBundle to exclude the influence of
+                // order. Therefore documents will generate same proto as long as the contents are
+                // same. Note that the order of repeated fields is still preserved.
+                ArrayList<String> keys = new ArrayList<>(mPropertyBundle.keySet());
+                Collections.sort(keys);
+                for (String key : keys) {
+                    Object values = mPropertyBundle.get(key);
+                    PropertyProto.Builder propertyProto = PropertyProto.newBuilder().setName(key);
+                    if (values instanceof boolean[]) {
+                        for (boolean value : (boolean[]) values) {
+                            propertyProto.addBooleanValues(value);
+                        }
+                    } else if (values instanceof long[]) {
+                        for (long value : (long[]) values) {
+                            propertyProto.addInt64Values(value);
+                        }
+                    } else if (values instanceof double[]) {
+                        for (double value : (double[]) values) {
+                            propertyProto.addDoubleValues(value);
+                        }
+                    } else if (values instanceof String[]) {
+                        for (String value : (String[]) values) {
+                            propertyProto.addStringValues(value);
+                        }
+                    } else {
+                        throw new IllegalStateException(
+                                "Property \"" + key + "\" has unsupported value type \""
+                                        + values.getClass().getSimpleName() + "\"");
+                    }
+                    mProtoBuilder.addProperties(propertyProto);
+                }
+                return new Document(mProtoBuilder.build(), mPropertyBundle);
+            }
+        }
+    }
+
+    /**
+     * Encapsulates a {@link Document} that represent an email.
+     *
+     * <p>This class is a higher level implement of {@link Document}.
+     *
+     * <p>This class will eventually migrate to Jetpack, where it will become public API.
+     *
+     * @hide
+     */
+    public static class Email extends Document {
+
+        /** The name of the schema type for {@link Email} documents.*/
+        public static final String SCHEMA_TYPE = "builtin:Email";
+
+        private static final String KEY_FROM = "from";
+        private static final String KEY_TO = "to";
+        private static final String KEY_CC = "cc";
+        private static final String KEY_BCC = "bcc";
+        private static final String KEY_SUBJECT = "subject";
+        private static final String KEY_BODY = "body";
+
+        /**
+         * Creates a new {@link Email} from the contents of an existing {@link Document}.
+         *
+         * @param document The {@link Document} containing the email content.
+         */
+        public Email(@NonNull Document document) {
+            super(document);
+        }
+
+        /**
+         * Creates a new {@link Email.Builder}.
+         *
+         * @param uri The uri of {@link Email}.
+         */
+        public static Builder newBuilder(@NonNull String uri) {
+            return new Builder(uri);
+        }
+
+        /**
+         * Get the from address of {@link Email}.
+         *
+         * @return Returns the subject of {@link Email} or {@code null} if it's not been set yet.
+         * @hide
+         */
+        @Nullable
+        public String getFrom() {
+            return getPropertyString(KEY_FROM);
+        }
+
+        /**
+         * Get the destination address of {@link Email}.
+         *
+         * @return Returns the destination address of {@link Email} or {@code null} if it's not been
+         *         set yet.
+         * @hide
+         */
+        @Nullable
+        public String[] getTo() {
+            return getPropertyStringArray(KEY_TO);
+        }
+
+        /**
+         * Get the CC list of {@link Email}.
+         *
+         * @return Returns the CC list of {@link Email} or {@code null} if it's not been set yet.
+         * @hide
+         */
+        @Nullable
+        public String[] getCc() {
+            return getPropertyStringArray(KEY_CC);
+        }
+
+        /**
+         * Get the BCC list of {@link Email}.
+         *
+         * @return Returns the BCC list of {@link Email} or {@code null} if it's not been set yet.
+         * @hide
+         */
+        @Nullable
+        public String[] getBcc() {
+            return getPropertyStringArray(KEY_BCC);
+        }
+
+        /**
+         * Get the subject of {@link Email}.
+         *
+         * @return Returns the value subject of {@link Email} or {@code null} if it's not been set
+         * yet.
+         * @hide
+         */
+        @Nullable
+        public String getSubject() {
+            return getPropertyString(KEY_SUBJECT);
+        }
+
+        /**
+         * Get the body of {@link Email}.
+         *
+         * @return Returns the body of {@link Email} or {@code null} if it's not been set yet.
+         * @hide
+         */
+        @Nullable
+        public String getBody() {
+            return getPropertyString(KEY_BODY);
+        }
+
+        /**
+         * The builder class for {@link Email}.
+         * @hide
+         */
+        public static class Builder extends Document.Builder<Email.Builder> {
+
+            /**
+             * Create a new {@link Email.Builder}
+             * @param uri The Uri of the Email.
+             * @hide
+             */
+            private Builder(@NonNull String uri) {
+                super(uri, SCHEMA_TYPE);
+            }
+
+            /**
+             * Set the from address of {@link Email}
+             * @hide
+             */
+            @NonNull
+            public Email.Builder setFrom(@NonNull String from) {
+                setProperty(KEY_FROM, from);
+                return this;
+            }
+
+            /**
+             * Set the destination address of {@link Email}
+             * @hide
+             */
+            @NonNull
+            public Email.Builder setTo(@NonNull String... to) {
+                setProperty(KEY_TO, to);
+                return this;
+            }
+
+            /**
+             * Set the CC list of {@link Email}
+             * @hide
+             */
+            @NonNull
+            public Email.Builder setCc(@NonNull String... cc) {
+                setProperty(KEY_CC, cc);
+                return this;
+            }
+
+            /**
+             * Set the BCC list of {@link Email}
+             * @hide
+             */
+            @NonNull
+            public Email.Builder setBcc(@NonNull String... bcc) {
+                setProperty(KEY_BCC, bcc);
+                return this;
+            }
+
+            /**
+             * Set the subject of {@link Email}
+             * @hide
+             */
+            @NonNull
+            public Email.Builder setSubject(@NonNull String subject) {
+                setProperty(KEY_SUBJECT, subject);
+                return this;
+            }
+
+            /**
+             * Set the body of {@link Email}
+             * @hide
+             */
+            @NonNull
+            public Email.Builder setBody(@NonNull String body) {
+                setProperty(KEY_BODY, body);
+                return this;
+            }
+
+            /**
+             * Builds the {@link Email} object.
+             *
+             * @hide
+             */
+            @NonNull
+            @Override
+            public Email build() {
+                return new Email(super.build());
+            }
+        }
+    }
+}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java
index 58bb605..83195dc 100644
--- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java
+++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java
@@ -18,6 +18,7 @@
 import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.annotation.SystemService;
+import android.app.appsearch.AppSearch.Document;
 import android.content.Context;
 import android.os.RemoteException;
 
@@ -25,6 +26,7 @@
 
 import com.google.android.icing.proto.SchemaProto;
 
+import java.util.List;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
@@ -95,4 +97,34 @@
         }
         future.whenCompleteAsync((noop, err) -> callback.accept(err), executor);
     }
+
+    /**
+     * Index {@link Document} to AppSearch
+     *
+     * <p>You should not call this method directly; instead, use the {@code AppSearch#put()} API
+     * provided by JetPack.
+     *
+     * <p>The schema should be set via {@link #setSchema} method.
+     *
+     * @param documents {@link Document Documents} that need to be indexed.
+     * @param executor Executor on which to invoke the callback.
+     * @param callback Callback to receive errors resulting from setting the schema. If the
+     *                 operation succeeds, the callback will be invoked with {@code null}.
+     */
+    public void put(@NonNull List<Document> documents,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull Consumer<? super Throwable> callback) {
+        AndroidFuture<Void> future = new AndroidFuture<>();
+        for (Document document : documents) {
+            // TODO(b/146386470) batching Document protos
+            try {
+                mService.put(document.getProto().toByteArray(), future);
+            } catch (RemoteException e) {
+                future.completeExceptionally(e);
+                break;
+            }
+        }
+        // TODO(b/147614371) Fix error report for multiple documents.
+        future.whenCompleteAsync((noop, err) -> callback.accept(err), executor);
+    }
 }
diff --git a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl
index 8085aa8..fc83d8c 100644
--- a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl
+++ b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl
@@ -28,4 +28,5 @@
      *     if setSchema fails.
      */
     void setSchema(in byte[] schemaProto, in AndroidFuture callback);
+    void put(in byte[] documentBytes, in AndroidFuture callback);
 }
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java
index 96316b3..042f051 100644
--- a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java
+++ b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java
@@ -43,6 +43,16 @@
             try {
                 SchemaProto schema = SchemaProto.parseFrom(schemaBytes);
                 throw new UnsupportedOperationException("setSchema not yet implemented: " + schema);
+
+            } catch (Throwable t) {
+                callback.completeExceptionally(t);
+            }
+        }
+
+        @Override
+        public void put(byte[] documentBytes, AndroidFuture callback) {
+            try {
+                throw new UnsupportedOperationException("Put document not yet implemented");
             } catch (Throwable t) {
                 callback.completeExceptionally(t);
             }
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index f6512a6..102e848 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -257,7 +257,7 @@
     private final List<JobRestriction> mJobRestrictions;
 
     private final CountQuotaTracker mQuotaTracker;
-    private static final String QUOTA_TRACKER_SCHEDULE_TAG = ".schedule()";
+    private static final String QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG = ".schedulePersisted()";
     private final PlatformCompat mPlatformCompat;
 
     /**
@@ -522,7 +522,7 @@
         private static final float DEFAULT_CONN_CONGESTION_DELAY_FRAC = 0.5f;
         private static final float DEFAULT_CONN_PREFETCH_RELAX_FRAC = 0.5f;
         private static final boolean DEFAULT_ENABLE_API_QUOTAS = true;
-        private static final int DEFAULT_API_QUOTA_SCHEDULE_COUNT = 500;
+        private static final int DEFAULT_API_QUOTA_SCHEDULE_COUNT = 250;
         private static final long DEFAULT_API_QUOTA_SCHEDULE_WINDOW_MS = MINUTE_IN_MILLIS;
         private static final boolean DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION = true;
 
@@ -740,6 +740,8 @@
 
             ENABLE_API_QUOTAS = mParser.getBoolean(KEY_ENABLE_API_QUOTAS,
                 DEFAULT_ENABLE_API_QUOTAS);
+            // Set a minimum value on the quota limit so it's not so low that it interferes with
+            // legitimate use cases.
             API_QUOTA_SCHEDULE_COUNT = Math.max(250,
                     mParser.getInt(KEY_API_QUOTA_SCHEDULE_COUNT, DEFAULT_API_QUOTA_SCHEDULE_COUNT));
             API_QUOTA_SCHEDULE_WINDOW_MS = mParser.getDurationMillis(
@@ -1054,44 +1056,48 @@
 
     public int scheduleAsPackage(JobInfo job, JobWorkItem work, int uId, String packageName,
             int userId, String tag) {
-        final String pkg = packageName == null ? job.getService().getPackageName() : packageName;
-        if (!mQuotaTracker.isWithinQuota(userId, pkg, QUOTA_TRACKER_SCHEDULE_TAG)) {
-            Slog.e(TAG, userId + "-" + pkg + " has called schedule() too many times");
-            // TODO(b/145551233): attempt to restrict app
-            if (mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION
-                    && mPlatformCompat.isChangeEnabledByPackageName(
-                            CRASH_ON_EXCEEDED_LIMIT, pkg, userId)) {
-                final boolean isDebuggable;
-                synchronized (mLock) {
-                    if (!mDebuggableApps.containsKey(packageName)) {
-                        try {
-                            final ApplicationInfo appInfo = AppGlobals.getPackageManager()
-                                    .getApplicationInfo(pkg, 0, userId);
-                            if (appInfo != null) {
-                                mDebuggableApps.put(packageName,
-                                        (appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0);
-                            } else {
-                                return JobScheduler.RESULT_FAILURE;
+        if (job.isPersisted()) {
+            // Only limit schedule calls for persisted jobs.
+            final String pkg =
+                    packageName == null ? job.getService().getPackageName() : packageName;
+            if (!mQuotaTracker.isWithinQuota(userId, pkg, QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG)) {
+                Slog.e(TAG, userId + "-" + pkg + " has called schedule() too many times");
+                // TODO(b/145551233): attempt to restrict app
+                if (mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION
+                        && mPlatformCompat.isChangeEnabledByPackageName(
+                                CRASH_ON_EXCEEDED_LIMIT, pkg, userId)) {
+                    final boolean isDebuggable;
+                    synchronized (mLock) {
+                        if (!mDebuggableApps.containsKey(packageName)) {
+                            try {
+                                final ApplicationInfo appInfo = AppGlobals.getPackageManager()
+                                        .getApplicationInfo(pkg, 0, userId);
+                                if (appInfo != null) {
+                                    mDebuggableApps.put(packageName,
+                                            (appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0);
+                                } else {
+                                    return JobScheduler.RESULT_FAILURE;
+                                }
+                            } catch (RemoteException e) {
+                                throw new RuntimeException(e);
                             }
-                        } catch (RemoteException e) {
-                            throw new RuntimeException(e);
                         }
+                        isDebuggable = mDebuggableApps.get(packageName);
                     }
-                    isDebuggable = mDebuggableApps.get(packageName);
+                    if (isDebuggable) {
+                        // Only throw the exception for debuggable apps.
+                        throw new IllegalStateException(
+                                "schedule()/enqueue() called more than "
+                                        + mQuotaTracker.getLimit(Category.SINGLE_CATEGORY)
+                                        + " times in the past "
+                                        + mQuotaTracker.getWindowSizeMs(Category.SINGLE_CATEGORY)
+                                        + "ms");
+                    }
                 }
-                if (isDebuggable) {
-                    // Only throw the exception for debuggable apps.
-                    throw new IllegalStateException(
-                            "schedule()/enqueue() called more than "
-                                    + mQuotaTracker.getLimit(Category.SINGLE_CATEGORY)
-                                    + " times in the past "
-                                    + mQuotaTracker.getWindowSizeMs(Category.SINGLE_CATEGORY)
-                                    + "ms");
-                }
+                return JobScheduler.RESULT_FAILURE;
             }
-            return JobScheduler.RESULT_FAILURE;
+            mQuotaTracker.noteEvent(userId, pkg, QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG);
         }
-        mQuotaTracker.noteEvent(userId, pkg, QUOTA_TRACKER_SCHEDULE_TAG);
 
         try {
             if (ActivityManager.getService().isAppStartModeDisabled(uId,
diff --git a/apex/statsd/service/java/com/android/server/stats/StatsCompanionService.java b/apex/statsd/service/java/com/android/server/stats/StatsCompanionService.java
index 1faa814..6444b4e 100644
--- a/apex/statsd/service/java/com/android/server/stats/StatsCompanionService.java
+++ b/apex/statsd/service/java/com/android/server/stats/StatsCompanionService.java
@@ -1404,21 +1404,6 @@
         }
     }
 
-    private void pullBuildInformation(int tagId,
-            long elapsedNanos, long wallClockNanos, List<StatsLogEventWrapper> pulledData) {
-        StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos);
-        e.writeString(Build.FINGERPRINT);
-        e.writeString(Build.BRAND);
-        e.writeString(Build.PRODUCT);
-        e.writeString(Build.DEVICE);
-        e.writeString(Build.VERSION.RELEASE);
-        e.writeString(Build.ID);
-        e.writeString(Build.VERSION.INCREMENTAL);
-        e.writeString(Build.TYPE);
-        e.writeString(Build.TAGS);
-        pulledData.add(e);
-    }
-
     private BatteryStatsHelper getBatteryStatsHelper() {
         if (mBatteryStatsHelper == null) {
             final long callingToken = Binder.clearCallingIdentity();
@@ -2113,11 +2098,6 @@
                 break;
             }
 
-            case StatsLog.BUILD_INFORMATION: {
-                pullBuildInformation(tagId, elapsedNanos, wallClockNanos, ret);
-                break;
-            }
-
             case StatsLog.PROCESS_CPU_TIME: {
                 pullProcessCpuTime(tagId, elapsedNanos, wallClockNanos, ret);
                 break;
diff --git a/api/current.txt b/api/current.txt
index 49676b4..6d05797 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -29652,7 +29652,7 @@
     method @NonNull public android.net.NetworkRequest.Builder clearCapabilities();
     method public android.net.NetworkRequest.Builder removeCapability(int);
     method public android.net.NetworkRequest.Builder removeTransportType(int);
-    method public android.net.NetworkRequest.Builder setNetworkSpecifier(String);
+    method @Deprecated public android.net.NetworkRequest.Builder setNetworkSpecifier(String);
     method public android.net.NetworkRequest.Builder setNetworkSpecifier(android.net.NetworkSpecifier);
   }
 
@@ -29751,6 +29751,19 @@
     method public void onStopped();
   }
 
+  public final class TelephonyNetworkSpecifier extends android.net.NetworkSpecifier implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getSubscriptionId();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.TelephonyNetworkSpecifier> CREATOR;
+  }
+
+  public static final class TelephonyNetworkSpecifier.Builder {
+    ctor public TelephonyNetworkSpecifier.Builder();
+    method @NonNull public android.net.TelephonyNetworkSpecifier build();
+    method @NonNull public android.net.TelephonyNetworkSpecifier.Builder setSubscriptionId(int);
+  }
+
   public class TrafficStats {
     ctor public TrafficStats();
     method public static void clearThreadStatsTag();
@@ -30508,6 +30521,11 @@
     field @Deprecated public static final String[] strings;
   }
 
+  @Deprecated public static class WifiConfiguration.SuiteBCipher {
+    field @Deprecated public static final int ECDHE_ECDSA = 0; // 0x0
+    field @Deprecated public static final int ECDHE_RSA = 1; // 0x1
+  }
+
   public class WifiEnterpriseConfig implements android.os.Parcelable {
     ctor public WifiEnterpriseConfig();
     ctor public WifiEnterpriseConfig(android.net.wifi.WifiEnterpriseConfig);
@@ -44062,6 +44080,7 @@
     method public java.util.List<android.telecom.Call> getChildren();
     method public java.util.List<android.telecom.Call> getConferenceableCalls();
     method public android.telecom.Call.Details getDetails();
+    method @Nullable public android.telecom.Call getGenericConferenceActiveChildCall();
     method public android.telecom.Call getParent();
     method public String getRemainingPostDialSequence();
     method @Nullable public android.telecom.Call.RttCall getRttCall();
@@ -44145,6 +44164,7 @@
     method public int getCallerDisplayNamePresentation();
     method public int getCallerNumberVerificationStatus();
     method public final long getConnectTimeMillis();
+    method @Nullable public String getContactDisplayName();
     method public long getCreationTimeMillis();
     method public android.telecom.DisconnectCause getDisconnectCause();
     method public android.os.Bundle getExtras();
@@ -45154,6 +45174,39 @@
     field public static final int PRIORITY_MED = 2; // 0x2
   }
 
+  public final class BarringInfo implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public android.telephony.BarringInfo.BarringServiceInfo getBarringServiceInfo(int);
+    method public boolean isServiceBarred(int);
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field public static final int BARRING_SERVICE_TYPE_CS_FALLBACK = 5; // 0x5
+    field public static final int BARRING_SERVICE_TYPE_CS_SERVICE = 0; // 0x0
+    field public static final int BARRING_SERVICE_TYPE_CS_VOICE = 2; // 0x2
+    field public static final int BARRING_SERVICE_TYPE_EMERGENCY = 8; // 0x8
+    field public static final int BARRING_SERVICE_TYPE_MMTEL_VIDEO = 7; // 0x7
+    field public static final int BARRING_SERVICE_TYPE_MMTEL_VOICE = 6; // 0x6
+    field public static final int BARRING_SERVICE_TYPE_MO_DATA = 4; // 0x4
+    field public static final int BARRING_SERVICE_TYPE_MO_SIGNALLING = 3; // 0x3
+    field public static final int BARRING_SERVICE_TYPE_PS_SERVICE = 1; // 0x1
+    field public static final int BARRING_SERVICE_TYPE_SMS = 9; // 0x9
+    field @NonNull public static final android.os.Parcelable.Creator<android.telephony.BarringInfo> CREATOR;
+  }
+
+  public static final class BarringInfo.BarringServiceInfo implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getBarringType();
+    method public int getConditionalBarringFactor();
+    method public int getConditionalBarringTimeSeconds();
+    method public boolean isBarred();
+    method public boolean isConditionallyBarred();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field public static final int BARRING_TYPE_CONDITIONAL = 1; // 0x1
+    field public static final int BARRING_TYPE_NONE = 0; // 0x0
+    field public static final int BARRING_TYPE_UNCONDITIONAL = 2; // 0x2
+    field public static final int BARRING_TYPE_UNKNOWN = -1; // 0xffffffff
+    field @NonNull public static final android.os.Parcelable.Creator<android.telephony.BarringInfo.BarringServiceInfo> CREATOR;
+  }
+
   public class CarrierConfigManager {
     method @Nullable public android.os.PersistableBundle getConfig();
     method @Nullable public android.os.PersistableBundle getConfigByComponentForSubId(@NonNull String, int);
@@ -45847,6 +45900,7 @@
     ctor public PhoneStateListener();
     ctor public PhoneStateListener(@NonNull java.util.concurrent.Executor);
     method public void onActiveDataSubscriptionIdChanged(int);
+    method public void onBarringInfoChanged(@NonNull android.telephony.BarringInfo);
     method @RequiresPermission("android.permission.READ_PRECISE_PHONE_STATE") public void onCallDisconnectCauseChanged(int, int);
     method public void onCallForwardingIndicatorChanged(boolean);
     method public void onCallStateChanged(int, String);
@@ -45864,6 +45918,7 @@
     method public void onSignalStrengthsChanged(android.telephony.SignalStrength);
     method public void onUserMobileDataStateChanged(boolean);
     field public static final int LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE = 4194304; // 0x400000
+    field @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public static final int LISTEN_BARRING_INFO = -2147483648; // 0x80000000
     field @RequiresPermission("android.permission.READ_PRECISE_PHONE_STATE") public static final int LISTEN_CALL_DISCONNECT_CAUSES = 33554432; // 0x2000000
     field public static final int LISTEN_CALL_FORWARDING_INDICATOR = 8; // 0x8
     field public static final int LISTEN_CALL_STATE = 32; // 0x20
diff --git a/api/system-current.txt b/api/system-current.txt
index 48f8bc1..442e116 100755
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -5194,6 +5194,10 @@
     field @NonNull public final String specifier;
   }
 
+  public final class TelephonyNetworkSpecifier extends android.net.NetworkSpecifier implements android.os.Parcelable {
+    method public boolean satisfiedBy(android.net.NetworkSpecifier);
+  }
+
   public class TrafficStats {
     method public static void setThreadStatsTagApp();
     method public static void setThreadStatsTagBackup();
@@ -9463,6 +9467,11 @@
     field public static final int TRANSPORT_TYPE_WWAN = 1; // 0x1
   }
 
+  public final class BarringInfo implements android.os.Parcelable {
+    ctor public BarringInfo();
+    method @NonNull public android.telephony.BarringInfo createLocationInfoSanitizedCopy();
+  }
+
   public final class CallAttributes implements android.os.Parcelable {
     ctor public CallAttributes(@NonNull android.telephony.PreciseCallState, int, @NonNull android.telephony.CallQuality);
     method public int describeContents();
@@ -10344,6 +10353,7 @@
   public class ServiceState implements android.os.Parcelable {
     method @NonNull public android.telephony.ServiceState createLocationInfoSanitizedCopy(boolean);
     method public void fillInNotifierBundle(@NonNull android.os.Bundle);
+    method public int getDataNetworkType();
     method public int getDataRegistrationState();
     method @Nullable public android.telephony.NetworkRegistrationInfo getNetworkRegistrationInfo(int, int);
     method @NonNull public java.util.List<android.telephony.NetworkRegistrationInfo> getNetworkRegistrationInfoList();
@@ -10722,6 +10732,7 @@
     field public static final int CDMA_SUBSCRIPTION_NV = 1; // 0x1
     field public static final int CDMA_SUBSCRIPTION_RUIM_SIM = 0; // 0x0
     field public static final int CDMA_SUBSCRIPTION_UNKNOWN = -1; // 0xffffffff
+    field public static final int DEFAULT_PREFERRED_NETWORK_MODE = 0; // 0x0
     field public static final String EXTRA_ANOMALY_DESCRIPTION = "android.telephony.extra.ANOMALY_DESCRIPTION";
     field public static final String EXTRA_ANOMALY_ID = "android.telephony.extra.ANOMALY_ID";
     field @Deprecated public static final String EXTRA_APN_PROTOCOL = "apnProto";
@@ -10784,6 +10795,7 @@
   public class TelephonyRegistryManager {
     method public void addOnOpportunisticSubscriptionsChangedListener(@NonNull android.telephony.SubscriptionManager.OnOpportunisticSubscriptionsChangedListener, @NonNull java.util.concurrent.Executor);
     method public void addOnSubscriptionsChangedListener(@NonNull android.telephony.SubscriptionManager.OnSubscriptionsChangedListener, @NonNull java.util.concurrent.Executor);
+    method public void notifyBarringInfoChanged(int, int, @NonNull android.telephony.BarringInfo);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void notifyCallStateChangedForAllSubscriptions(int, @Nullable String);
     method public void notifyCarrierNetworkChange(boolean);
     method public void notifyRegistrationFailed(int, int, @NonNull android.telephony.CellIdentity, @NonNull String, int, int, int);
diff --git a/api/test-current.txt b/api/test-current.txt
index 10028f2..6498e9a 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -3137,6 +3137,15 @@
     field public static final int TRANSPORT_TYPE_WWAN = 1; // 0x1
   }
 
+  public final class BarringInfo implements android.os.Parcelable {
+    ctor public BarringInfo();
+    ctor public BarringInfo(@Nullable android.telephony.CellIdentity, @NonNull android.util.SparseArray<android.telephony.BarringInfo.BarringServiceInfo>);
+  }
+
+  public static final class BarringInfo.BarringServiceInfo implements android.os.Parcelable {
+    ctor public BarringInfo.BarringServiceInfo(int, boolean, int, int);
+  }
+
   public final class CallQuality implements android.os.Parcelable {
     ctor public CallQuality(int, int, int, int, int, int, int, int, int, int, int);
     ctor public CallQuality(int, int, int, int, int, int, int, int, int, int, int, boolean, boolean, boolean);
@@ -3267,6 +3276,7 @@
 
   public class ServiceState implements android.os.Parcelable {
     method public void addNetworkRegistrationInfo(android.telephony.NetworkRegistrationInfo);
+    method public int getDataNetworkType();
     method public void setCdmaSystemAndNetworkId(int, int);
     method public void setCellBandwidths(int[]);
     method public void setChannelNumber(int);
diff --git a/cmds/incidentd/src/IncidentService.cpp b/cmds/incidentd/src/IncidentService.cpp
index cfd77c2..62312d1 100644
--- a/cmds/incidentd/src/IncidentService.cpp
+++ b/cmds/incidentd/src/IncidentService.cpp
@@ -123,14 +123,17 @@
 
 // ================================================================================
 ReportHandler::ReportHandler(const sp<WorkDirectory>& workDirectory,
-            const sp<Broadcaster>& broadcaster, const sp<Looper>& handlerLooper,
-            const sp<Throttler>& throttler)
+                             const sp<Broadcaster>& broadcaster,
+                             const sp<Looper>& handlerLooper,
+                             const sp<Throttler>& throttler,
+                             const vector<BringYourOwnSection*>& registeredSections)
         :mLock(),
          mWorkDirectory(workDirectory),
          mBroadcaster(broadcaster),
          mHandlerLooper(handlerLooper),
          mBacklogDelay(DEFAULT_DELAY_NS),
          mThrottler(throttler),
+         mRegisteredSections(registeredSections),
          mBatch(new ReportBatch()) {
 }
 
@@ -185,7 +188,7 @@
         return;
     }
 
-    sp<Reporter> reporter = new Reporter(mWorkDirectory, batch);
+    sp<Reporter> reporter = new Reporter(mWorkDirectory, batch, mRegisteredSections);
 
     // Take the report, which might take a while. More requests might queue
     // up while we're doing this, and we'll handle them in their next batch.
@@ -237,7 +240,7 @@
     mWorkDirectory = new WorkDirectory();
     mBroadcaster = new Broadcaster(mWorkDirectory);
     mHandler = new ReportHandler(mWorkDirectory, mBroadcaster, handlerLooper,
-            mThrottler);
+            mThrottler, mRegisteredSections);
     mBroadcaster->setHandler(mHandler);
 }
 
@@ -327,6 +330,11 @@
             incidentArgs.addSection(id);
         }
     }
+    for (const Section* section : mRegisteredSections) {
+        if (!section_requires_specific_mention(section->id)) {
+            incidentArgs.addSection(section->id);
+        }
+    }
 
     // The ReportRequest takes ownership of the fd, so we need to dup it.
     int fd = dup(stream.get());
@@ -339,6 +347,45 @@
     return Status::ok();
 }
 
+Status IncidentService::registerSection(const int id, const String16& name16,
+        const sp<IIncidentDumpCallback>& callback) {
+    const char* name = String8(name16).c_str();
+    ALOGI("Register section %d: %s", id, name);
+    if (callback == nullptr) {
+        return Status::fromExceptionCode(Status::EX_NULL_POINTER);
+    }
+    const uid_t callingUid = IPCThreadState::self()->getCallingUid();
+    for (int i = 0; i < mRegisteredSections.size(); i++) {
+        if (mRegisteredSections.at(i)->id == id) {
+            if (mRegisteredSections.at(i)->uid != callingUid) {
+                ALOGW("Error registering section %d: calling uid does not match", id);
+                return Status::fromExceptionCode(Status::EX_SECURITY);
+            }
+            mRegisteredSections.at(i) = new BringYourOwnSection(id, name, callingUid, callback);
+            return Status::ok();
+        }
+    }
+    mRegisteredSections.push_back(new BringYourOwnSection(id, name, callingUid, callback));
+    return Status::ok();
+}
+
+Status IncidentService::unregisterSection(const int id) {
+    ALOGI("Unregister section %d", id);
+    uid_t callingUid = IPCThreadState::self()->getCallingUid();
+    for (auto it = mRegisteredSections.begin(); it != mRegisteredSections.end(); it++) {
+        if ((*it)->id == id) {
+            if ((*it)->uid != callingUid) {
+                ALOGW("Error unregistering section %d: calling uid does not match", id);
+                return Status::fromExceptionCode(Status::EX_SECURITY);
+            }
+            mRegisteredSections.erase(it);
+            return Status::ok();
+        }
+    }
+    ALOGW("Section %d not found", id);
+    return Status::fromExceptionCode(Status::EX_ILLEGAL_STATE);
+}
+
 Status IncidentService::systemRunning() {
     if (IPCThreadState::self()->getCallingUid() != AID_SYSTEM) {
         return Status::fromExceptionCode(Status::EX_SECURITY,
diff --git a/cmds/incidentd/src/IncidentService.h b/cmds/incidentd/src/IncidentService.h
index b2c7f23..49fc566 100644
--- a/cmds/incidentd/src/IncidentService.h
+++ b/cmds/incidentd/src/IncidentService.h
@@ -40,12 +40,16 @@
 using namespace android::binder;
 using namespace android::os;
 
+class BringYourOwnSection;
+
 // ================================================================================
 class ReportHandler : public MessageHandler {
 public:
     ReportHandler(const sp<WorkDirectory>& workDirectory,
-            const sp<Broadcaster>& broadcaster, const sp<Looper>& handlerLooper,
-            const sp<Throttler>& throttler);
+                  const sp<Broadcaster>& broadcaster,
+                  const sp<Looper>& handlerLooper,
+                  const sp<Throttler>& throttler,
+                  const vector<BringYourOwnSection*>& registeredSections);
     virtual ~ReportHandler();
 
     virtual void handleMessage(const Message& message);
@@ -79,6 +83,8 @@
     nsecs_t mBacklogDelay;
     sp<Throttler> mThrottler;
 
+    const vector<BringYourOwnSection*>& mRegisteredSections;
+
     sp<ReportBatch> mBatch;
 
     /**
@@ -126,6 +132,11 @@
     virtual Status reportIncidentToDumpstate(unique_fd stream,
             const sp<IIncidentReportStatusListener>& listener);
 
+    virtual Status registerSection(int id, const String16& name,
+            const sp<IIncidentDumpCallback>& callback);
+
+    virtual Status unregisterSection(int id);
+
     virtual Status systemRunning();
 
     virtual Status getIncidentReportList(const String16& pkg, const String16& cls,
@@ -149,6 +160,7 @@
     sp<Broadcaster> mBroadcaster;
     sp<ReportHandler> mHandler;
     sp<Throttler> mThrottler;
+    vector<BringYourOwnSection*> mRegisteredSections;
 
     /**
      * Commands print out help.
diff --git a/cmds/incidentd/src/Reporter.cpp b/cmds/incidentd/src/Reporter.cpp
index 02b6bbe..aa40f85 100644
--- a/cmds/incidentd/src/Reporter.cpp
+++ b/cmds/incidentd/src/Reporter.cpp
@@ -364,7 +364,6 @@
     mSectionBufferSuccess = false;
     mHadError = false;
     mSectionErrors.clear();
-    
 }
 
 void ReportWriter::setSectionStats(const FdBuffer& buffer) {
@@ -470,10 +469,13 @@
 
 
 // ================================================================================
-Reporter::Reporter(const sp<WorkDirectory>& workDirectory, const sp<ReportBatch>& batch)
+Reporter::Reporter(const sp<WorkDirectory>& workDirectory,
+                   const sp<ReportBatch>& batch,
+                   const vector<BringYourOwnSection*>& registeredSections)
         :mWorkDirectory(workDirectory),
          mWriter(batch),
-         mBatch(batch) {
+         mBatch(batch),
+         mRegisteredSections(registeredSections) {
 }
 
 Reporter::~Reporter() {
@@ -580,50 +582,15 @@
     // For each of the report fields, see if we need it, and if so, execute the command
     // and report to those that care that we're doing it.
     for (const Section** section = SECTION_LIST; *section; section++) {
-        const int sectionId = (*section)->id;
-
-        // If nobody wants this section, skip it.
-        if (!mBatch->containsSection(sectionId)) {
-            continue;
-        }
-
-        ALOGD("Start incident report section %d '%s'", sectionId, (*section)->name.string());
-        IncidentMetadata::SectionStats* sectionMetadata = metadata.add_sections();
-
-        // Notify listener of starting
-        mBatch->forEachListener(sectionId, [sectionId](const auto& listener) {
-            listener->onReportSectionStatus(
-                    sectionId, IIncidentReportStatusListener::STATUS_STARTING);
-        });
-
-        // Go get the data and write it into the file descriptors.
-        mWriter.startSection(sectionId);
-        err = (*section)->Execute(&mWriter);
-        mWriter.endSection(sectionMetadata);
-
-        // Sections returning errors are fatal. Most errors should not be fatal.
-        if (err != NO_ERROR) {
-            mWriter.error((*section), err, "Section failed. Stopping report.");
+        if (execute_section(*section, &metadata, reportByteSize) != NO_ERROR) {
             goto DONE;
         }
+    }
 
-        // The returned max data size is used for throttling too many incident reports.
-        (*reportByteSize) += sectionMetadata->report_size_bytes();
-
-        // For any requests that failed during this section, remove them now.  We do this
-        // before calling back about section finished, so listeners do not erroniously get the
-        // impression that the section succeeded.  But we do it here instead of inside
-        // writeSection so that the callback is done from a known context and not from the
-        // bowels of a section, where changing the batch could cause odd errors.
-        cancel_and_remove_failed_requests();
-
-        // Notify listener of finishing
-        mBatch->forEachListener(sectionId, [sectionId](const auto& listener) {
-                listener->onReportSectionStatus(
-                        sectionId, IIncidentReportStatusListener::STATUS_FINISHED);
-        });
-
-        ALOGD("Finish incident report section %d '%s'", sectionId, (*section)->name.string());
+    for (const Section* section : mRegisteredSections) {
+        if (execute_section(section, &metadata, reportByteSize) != NO_ERROR) {
+            goto DONE;
+        }
     }
 
 DONE:
@@ -681,6 +648,55 @@
     ALOGI("Done taking incident report err=%s", strerror(-err));
 }
 
+status_t Reporter::execute_section(const Section* section, IncidentMetadata* metadata,
+        size_t* reportByteSize) {
+    const int sectionId = section->id;
+
+    // If nobody wants this section, skip it.
+    if (!mBatch->containsSection(sectionId)) {
+        return NO_ERROR;
+    }
+
+    ALOGD("Start incident report section %d '%s'", sectionId, section->name.string());
+    IncidentMetadata::SectionStats* sectionMetadata = metadata->add_sections();
+
+    // Notify listener of starting
+    mBatch->forEachListener(sectionId, [sectionId](const auto& listener) {
+        listener->onReportSectionStatus(
+                sectionId, IIncidentReportStatusListener::STATUS_STARTING);
+    });
+
+    // Go get the data and write it into the file descriptors.
+    mWriter.startSection(sectionId);
+    status_t err = section->Execute(&mWriter);
+    mWriter.endSection(sectionMetadata);
+
+    // Sections returning errors are fatal. Most errors should not be fatal.
+    if (err != NO_ERROR) {
+        mWriter.error(section, err, "Section failed. Stopping report.");
+        return err;
+    }
+
+    // The returned max data size is used for throttling too many incident reports.
+    (*reportByteSize) += sectionMetadata->report_size_bytes();
+
+    // For any requests that failed during this section, remove them now.  We do this
+    // before calling back about section finished, so listeners do not erroniously get the
+    // impression that the section succeeded.  But we do it here instead of inside
+    // writeSection so that the callback is done from a known context and not from the
+    // bowels of a section, where changing the batch could cause odd errors.
+    cancel_and_remove_failed_requests();
+
+    // Notify listener of finishing
+    mBatch->forEachListener(sectionId, [sectionId](const auto& listener) {
+            listener->onReportSectionStatus(
+                    sectionId, IIncidentReportStatusListener::STATUS_FINISHED);
+    });
+
+    ALOGD("Finish incident report section %d '%s'", sectionId, section->name.string());
+    return NO_ERROR;
+}
+
 void Reporter::cancel_and_remove_failed_requests() {
     // Handle a failure in the persisted file
     if (mPersistedFile != nullptr) {
diff --git a/cmds/incidentd/src/Reporter.h b/cmds/incidentd/src/Reporter.h
index fb3961a..cbc8b13 100644
--- a/cmds/incidentd/src/Reporter.h
+++ b/cmds/incidentd/src/Reporter.h
@@ -21,6 +21,7 @@
 #include "frameworks/base/core/proto/android/os/metadata.pb.h"
 #include <android/content/ComponentName.h>
 #include <android/os/IIncidentReportStatusListener.h>
+#include <android/os/IIncidentDumpCallback.h>
 #include <android/os/IncidentReportArgs.h>
 #include <android/util/protobuf.h>
 
@@ -39,6 +40,7 @@
 using namespace android::content;
 using namespace android::os;
 
+class BringYourOwnSection;
 class Section;
 
 // ================================================================================
@@ -122,7 +124,7 @@
     void forEachStreamingRequest(const function<void (const sp<ReportRequest>&)>& func);
 
     /**
-     * Call func(request) for each file descriptor that has 
+     * Call func(request) for each file descriptor.
      */
     void forEachFd(int sectionId, const function<void (const sp<ReportRequest>&)>& func);
 
@@ -251,7 +253,9 @@
 // ================================================================================
 class Reporter : public virtual RefBase {
 public:
-    Reporter(const sp<WorkDirectory>& workDirectory, const sp<ReportBatch>& batch);
+    Reporter(const sp<WorkDirectory>& workDirectory,
+             const sp<ReportBatch>& batch,
+             const vector<BringYourOwnSection*>& registeredSections);
 
     virtual ~Reporter();
 
@@ -263,6 +267,10 @@
     ReportWriter mWriter;
     sp<ReportBatch> mBatch;
     sp<ReportFile> mPersistedFile;
+    const vector<BringYourOwnSection*>& mRegisteredSections;
+
+    status_t execute_section(const Section* section, IncidentMetadata* metadata,
+        size_t* reportByteSize);
 
     void cancel_and_remove_failed_requests();
 };
diff --git a/cmds/incidentd/src/Section.cpp b/cmds/incidentd/src/Section.cpp
index c9277a5..2229e1c 100644
--- a/cmds/incidentd/src/Section.cpp
+++ b/cmds/incidentd/src/Section.cpp
@@ -267,7 +267,7 @@
     signal(SIGPIPE, sigpipe_handler);
 
     WorkerThreadData* data = (WorkerThreadData*)cookie;
-    status_t err = data->section->BlockingCall(data->pipe.writeFd().get());
+    status_t err = data->section->BlockingCall(data->pipe.writeFd());
 
     {
         unique_lock<mutex> lock(data->lock);
@@ -458,7 +458,7 @@
 
 DumpsysSection::~DumpsysSection() {}
 
-status_t DumpsysSection::BlockingCall(int pipeWriteFd) const {
+status_t DumpsysSection::BlockingCall(unique_fd& pipeWriteFd) const {
     // checkService won't wait for the service to show up like getService will.
     sp<IBinder> service = defaultServiceManager()->checkService(mService);
 
@@ -467,7 +467,7 @@
         return NAME_NOT_FOUND;
     }
 
-    service->dump(pipeWriteFd, mArgs);
+    service->dump(pipeWriteFd.get(), mArgs);
 
     return NO_ERROR;
 }
@@ -526,7 +526,7 @@
     return src[0] | (src[1] << 8) | (src[2] << 16) | (src[3] << 24);
 }
 
-status_t LogSection::BlockingCall(int pipeWriteFd) const {
+status_t LogSection::BlockingCall(unique_fd& pipeWriteFd) const {
     // Open log buffer and getting logs since last retrieved time if any.
     unique_ptr<logger_list, void (*)(logger_list*)> loggers(
             gLastLogsRetrieved.find(mLogID) == gLastLogsRetrieved.end()
@@ -643,7 +643,7 @@
         }
     }
     gLastLogsRetrieved[mLogID] = lastTimestamp;
-    if (!proto.flush(pipeWriteFd) && errno == EPIPE) {
+    if (!proto.flush(pipeWriteFd.get()) && errno == EPIPE) {
         ALOGE("[%s] wrote to a broken pipe\n", this->name.string());
         return EPIPE;
     }
@@ -660,7 +660,7 @@
 
 TombstoneSection::~TombstoneSection() {}
 
-status_t TombstoneSection::BlockingCall(int pipeWriteFd) const {
+status_t TombstoneSection::BlockingCall(unique_fd& pipeWriteFd) const {
     std::unique_ptr<DIR, decltype(&closedir)> proc(opendir("/proc"), closedir);
     if (proc.get() == nullptr) {
         ALOGE("opendir /proc failed: %s\n", strerror(errno));
@@ -768,7 +768,7 @@
         dumpPipe.readFd().reset();
     }
 
-    if (!proto.flush(pipeWriteFd) && errno == EPIPE) {
+    if (!proto.flush(pipeWriteFd.get()) && errno == EPIPE) {
         ALOGE("[%s] wrote to a broken pipe\n", this->name.string());
         if (err != NO_ERROR) {
             return EPIPE;
@@ -778,6 +778,22 @@
     return err;
 }
 
+// ================================================================================
+BringYourOwnSection::BringYourOwnSection(int id, const char* customName, const uid_t callingUid,
+        const sp<IIncidentDumpCallback>& callback)
+    : WorkerThreadSection(id, REMOTE_CALL_TIMEOUT_MS), uid(callingUid), mCallback(callback) {
+    name = "registered ";
+    name += customName;
+}
+
+BringYourOwnSection::~BringYourOwnSection() {}
+
+status_t BringYourOwnSection::BlockingCall(unique_fd& pipeWriteFd) const {
+    android::os::ParcelFileDescriptor pfd(std::move(pipeWriteFd));
+    mCallback->onDumpSection(pfd);
+    return NO_ERROR;
+}
+
 }  // namespace incidentd
 }  // namespace os
 }  // namespace android
diff --git a/cmds/incidentd/src/Section.h b/cmds/incidentd/src/Section.h
index fcf12f7..0bb9da9 100644
--- a/cmds/incidentd/src/Section.h
+++ b/cmds/incidentd/src/Section.h
@@ -23,6 +23,8 @@
 #include <stdarg.h>
 #include <map>
 
+#include <android/os/IIncidentDumpCallback.h>
+
 #include <utils/String16.h>
 #include <utils/String8.h>
 #include <utils/Vector.h>
@@ -89,7 +91,7 @@
 
     virtual status_t Execute(ReportWriter* writer) const;
 
-    virtual status_t BlockingCall(int pipeWriteFd) const = 0;
+    virtual status_t BlockingCall(unique_fd& pipeWriteFd) const = 0;
 };
 
 /**
@@ -117,7 +119,7 @@
     DumpsysSection(int id, const char* service, ...);
     virtual ~DumpsysSection();
 
-    virtual status_t BlockingCall(int pipeWriteFd) const;
+    virtual status_t BlockingCall(unique_fd& pipeWriteFd) const;
 
 private:
     String16 mService;
@@ -132,7 +134,7 @@
     SystemPropertyDumpsysSection(int id, const char* service, ...);
     virtual ~SystemPropertyDumpsysSection();
 
-    virtual status_t BlockingCall(int pipeWriteFd) const;
+    virtual status_t BlockingCall(unique_fd& pipeWriteFd) const;
 
 private:
     String16 mService;
@@ -153,7 +155,7 @@
     LogSection(int id, const char* logID, ...);
     virtual ~LogSection();
 
-    virtual status_t BlockingCall(int pipeWriteFd) const;
+    virtual status_t BlockingCall(unique_fd& pipeWriteFd) const;
 
 private:
     log_id_t mLogID;
@@ -169,12 +171,29 @@
     TombstoneSection(int id, const char* type, int64_t timeoutMs = 120000 /* 2 minutes */);
     virtual ~TombstoneSection();
 
-    virtual status_t BlockingCall(int pipeWriteFd) const;
+    virtual status_t BlockingCall(unique_fd& pipeWriteFd) const;
 
 private:
     std::string mType;
 };
 
+/**
+ * Section that gets data from a registered dump callback.
+ */
+class BringYourOwnSection : public WorkerThreadSection {
+public:
+    const uid_t uid;
+
+    BringYourOwnSection(int id, const char* customName, const uid_t callingUid,
+        const sp<IIncidentDumpCallback>& callback);
+    virtual ~BringYourOwnSection();
+
+    virtual status_t BlockingCall(unique_fd& pipeWriteFd) const;
+
+private:
+    const sp<IIncidentDumpCallback>& mCallback;
+};
+
 
 /**
  * These sections will not be generated when doing an 'all' report, either
diff --git a/cmds/statsd/src/external/StatsPullerManager.cpp b/cmds/statsd/src/external/StatsPullerManager.cpp
index 4baa08c..b5920cb 100644
--- a/cmds/statsd/src/external/StatsPullerManager.cpp
+++ b/cmds/statsd/src/external/StatsPullerManager.cpp
@@ -217,10 +217,6 @@
          {.additiveFields = {1, 2, 3, 4},
           .puller = new StatsCompanionServicePuller(android::util::DEBUG_FAILING_ELAPSED_CLOCK)}},
 
-        // BuildInformation.
-        {{.atomTag = android::util::BUILD_INFORMATION},
-         {.puller = new StatsCompanionServicePuller(android::util::BUILD_INFORMATION)}},
-
         // RoleHolder.
         {{.atomTag = android::util::ROLE_HOLDER},
          {.puller = new StatsCompanionServicePuller(android::util::ROLE_HOLDER)}},
diff --git a/core/java/android/app/DisabledWallpaperManager.java b/core/java/android/app/DisabledWallpaperManager.java
index 5185941..1f323c3 100644
--- a/core/java/android/app/DisabledWallpaperManager.java
+++ b/core/java/android/app/DisabledWallpaperManager.java
@@ -41,8 +41,7 @@
     // Don't need to worry about synchronization
     private static DisabledWallpaperManager sInstance;
 
-    // TODO(b/138939803): STOPSHIP changed to false and/or remove it
-    private static final boolean DEBUG = true;
+    private static final boolean DEBUG = false;
 
     @NonNull
     static DisabledWallpaperManager getInstance() {
@@ -53,7 +52,6 @@
     }
 
     private DisabledWallpaperManager() {
-        super(null, null, null);
     }
 
     @Override
@@ -66,10 +64,6 @@
         return false;
     }
 
-    // TODO(b/138939803): STOPSHIP methods below should not be necessary,
-    // callers should check if isWallpaperSupported(), consider removing them to keep this class
-    // simpler
-
     private static <T> T unsupported() {
         if (DEBUG) Log.w(TAG, "unsupported method called; returning null", new Exception());
         return null;
@@ -343,4 +337,9 @@
     public boolean isWallpaperBackupEligible(int which) {
         return unsupportedBoolean();
     }
+
+    @Override
+    public boolean wallpaperSupportsWcg(int which) {
+        return unsupportedBoolean();
+    }
 }
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index fcdb7cc..7af7a4a 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -94,7 +94,7 @@
     void updateNotificationChannelGroupForPackage(String pkg, int uid, in NotificationChannelGroup group);
     void updateNotificationChannelForPackage(String pkg, int uid, in NotificationChannel channel);
     NotificationChannel getNotificationChannel(String callingPkg, int userId, String pkg, String channelId);
-    NotificationChannel getConversationNotificationChannel(String callingPkg, int userId, String pkg, String channelId, String conversationId);
+    NotificationChannel getConversationNotificationChannel(String callingPkg, int userId, String pkg, String channelId, boolean returnParentIfNoConversationChannel, String conversationId);
     void createConversationNotificationChannelForPackage(String pkg, int uid, in NotificationChannel parentChannel, String conversationId);
     NotificationChannel getNotificationChannelForPackage(String pkg, int uid, String channelId, boolean includeDeleted);
     void deleteNotificationChannel(String pkg, String channelId);
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java
index a33c2c1..bdc7b99 100644
--- a/core/java/android/app/NotificationChannel.java
+++ b/core/java/android/app/NotificationChannel.java
@@ -31,6 +31,7 @@
 import android.provider.Settings;
 import android.service.notification.NotificationListenerService;
 import android.text.TextUtils;
+import android.util.Log;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.util.Preconditions;
@@ -71,7 +72,7 @@
      * Conversation id to use for apps that aren't providing them yet.
      * @hide
      */
-    public static final String PLACEHOLDER_CONVERSATION_ID = "placeholder_id";
+    public static final String PLACEHOLDER_CONVERSATION_ID = ":placeholder_id";
 
     /**
      * The maximum length for text fields in a NotificationChannel. Fields will be truncated at this
@@ -826,8 +827,8 @@
         setBlockableSystem(safeBool(parser, ATT_BLOCKABLE_SYSTEM, false));
         setAllowBubbles(safeBool(parser, ATT_ALLOW_BUBBLE, DEFAULT_ALLOW_BUBBLE));
         setOriginalImportance(safeInt(parser, ATT_ORIG_IMP, DEFAULT_IMPORTANCE));
-        setConversationId(parser.getAttributeValue(ATT_PARENT_CHANNEL, null),
-                parser.getAttributeValue(ATT_CONVERSATION_ID, null));
+        setConversationId(parser.getAttributeValue(null, ATT_PARENT_CHANNEL),
+                parser.getAttributeValue(null, ATT_CONVERSATION_ID));
     }
 
     @Nullable
@@ -1175,7 +1176,7 @@
                 + ", mImportanceLockedDefaultApp=" + mImportanceLockedDefaultApp
                 + ", mOriginalImp=" + mOriginalImportance
                 + ", mParent=" + mParentId
-                + ", mConversationId" + mConversationId;
+                + ", mConversationId=" + mConversationId;
     }
 
     /** @hide */
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 61c1098..1a8e15c 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -855,7 +855,8 @@
         INotificationManager service = getService();
         try {
             return service.getConversationNotificationChannel(mContext.getOpPackageName(),
-                    mContext.getUserId(), mContext.getPackageName(), channelId, conversationId);
+                    mContext.getUserId(), mContext.getPackageName(), channelId, true,
+                    conversationId);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index a9be9ea..7b21f12 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -54,6 +54,7 @@
 import android.content.integrity.IAppIntegrityManager;
 import android.content.om.IOverlayManager;
 import android.content.om.OverlayManager;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.CrossProfileApps;
 import android.content.pm.DataLoaderManager;
 import android.content.pm.ICrossProfileApps;
@@ -693,20 +694,20 @@
                     throws ServiceNotFoundException {
                 final IBinder b = ServiceManager.getService(Context.WALLPAPER_SERVICE);
                 if (b == null) {
-                    // There are 2 reason service can be null:
-                    // 1.Device doesn't support it - that's fine
-                    // 2.App is running on instant mode - should fail
-                    final boolean enabled = Resources.getSystem()
-                            .getBoolean(com.android.internal.R.bool.config_enableWallpaperService);
-                    if (!enabled) {
-                        // Life moves on...
-                        return DisabledWallpaperManager.getInstance();
-                    }
-                    if (ctx.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.P) {
+                    ApplicationInfo appInfo = ctx.getApplicationInfo();
+                    if (appInfo.targetSdkVersion >= Build.VERSION_CODES.P
+                            && appInfo.isInstantApp()) {
                         // Instant app
                         throw new ServiceNotFoundException(Context.WALLPAPER_SERVICE);
                     }
+                    final boolean enabled = Resources.getSystem()
+                            .getBoolean(com.android.internal.R.bool.config_enableWallpaperService);
+                    if (!enabled) {
+                        // Device doesn't support wallpaper, return a limited manager
+                        return DisabledWallpaperManager.getInstance();
+                    }
                     // Bad state - WallpaperManager methods will throw exception
+                    Log.e(TAG, "No wallpaper service");
                 }
                 IWallpaperManager service = IWallpaperManager.Stub.asInterface(b);
                 return new WallpaperManager(service, ctx.getOuterContext(),
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index 8eee73c..9ad3d38 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -543,6 +543,13 @@
         mCmProxy = new ColorManagementProxy(context);
     }
 
+    // no-op constructor called just by DisabledWallpaperManager
+    /*package*/ WallpaperManager() {
+        mContext = null;
+        mCmProxy = null;
+        mWcgEnabled = false;
+    }
+
     /**
      * Retrieve a WallpaperManager associated with the given Context.
      */
diff --git a/core/java/android/app/usage/UsageEvents.java b/core/java/android/app/usage/UsageEvents.java
index d840c1c..6ab880d 100644
--- a/core/java/android/app/usage/UsageEvents.java
+++ b/core/java/android/app/usage/UsageEvents.java
@@ -451,21 +451,7 @@
 
         /** @hide */
         public Event(Event orig) {
-            mPackage = orig.mPackage;
-            mClass = orig.mClass;
-            mInstanceId = orig.mInstanceId;
-            mTaskRootPackage = orig.mTaskRootPackage;
-            mTaskRootClass = orig.mTaskRootClass;
-            mTimeStamp = orig.mTimeStamp;
-            mEventType = orig.mEventType;
-            mConfiguration = orig.mConfiguration;
-            mShortcutId = orig.mShortcutId;
-            mAction = orig.mAction;
-            mContentType = orig.mContentType;
-            mContentAnnotations = orig.mContentAnnotations;
-            mFlags = orig.mFlags;
-            mBucketAndReason = orig.mBucketAndReason;
-            mNotificationChannelId = orig.mNotificationChannelId;
+            copyFrom(orig);
         }
 
         /**
@@ -622,6 +608,24 @@
             // which instant apps can't use anyway, so there's no need to hide them.
             return ret;
         }
+
+        private void copyFrom(Event orig) {
+            mPackage = orig.mPackage;
+            mClass = orig.mClass;
+            mInstanceId = orig.mInstanceId;
+            mTaskRootPackage = orig.mTaskRootPackage;
+            mTaskRootClass = orig.mTaskRootClass;
+            mTimeStamp = orig.mTimeStamp;
+            mEventType = orig.mEventType;
+            mConfiguration = orig.mConfiguration;
+            mShortcutId = orig.mShortcutId;
+            mAction = orig.mAction;
+            mContentType = orig.mContentType;
+            mContentAnnotations = orig.mContentAnnotations;
+            mFlags = orig.mFlags;
+            mBucketAndReason = orig.mBucketAndReason;
+            mNotificationChannelId = orig.mNotificationChannelId;
+        }
     }
 
     // Only used when creating the resulting events. Not used for reading/unparceling.
@@ -725,10 +729,14 @@
             return false;
         }
 
-        readEventFromParcel(mParcel, eventOut);
+        if (mParcel != null) {
+            readEventFromParcel(mParcel, eventOut);
+        } else {
+            eventOut.copyFrom(mEventsToWrite.get(mIndex));
+        }
 
         mIndex++;
-        if (mIndex >= mEventCount) {
+        if (mIndex >= mEventCount && mParcel != null) {
             mParcel.recycle();
             mParcel = null;
         }
diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java
index 456ebf2..1932f46 100644
--- a/core/java/android/hardware/soundtrigger/SoundTrigger.java
+++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java
@@ -34,6 +34,7 @@
 import android.media.AudioFormat;
 import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
 import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
+import android.media.soundtrigger_middleware.Status;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -41,6 +42,7 @@
 import android.os.Parcelable;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.ServiceSpecificException;
 import android.util.Log;
 
 import java.lang.annotation.Retention;
@@ -1676,6 +1678,45 @@
     }
 
     /**
+     * Translate an exception thrown from interaction with the underlying service to an error code.
+     * Throws a runtime exception for unexpected conditions.
+     * @param e The caught exception.
+     * @return The error code.
+     *
+     * @hide
+     */
+    static int handleException(Exception e) {
+        Log.w(TAG, "Exception caught", e);
+        if (e instanceof RemoteException) {
+            return STATUS_DEAD_OBJECT;
+        }
+        if (e instanceof ServiceSpecificException) {
+            switch (((ServiceSpecificException) e).errorCode) {
+                case Status.OPERATION_NOT_SUPPORTED:
+                    return STATUS_INVALID_OPERATION;
+                case Status.TEMPORARY_PERMISSION_DENIED:
+                    return STATUS_PERMISSION_DENIED;
+                case Status.DEAD_OBJECT:
+                    return STATUS_DEAD_OBJECT;
+            }
+            return STATUS_ERROR;
+        }
+        if (e instanceof SecurityException) {
+            return STATUS_PERMISSION_DENIED;
+        }
+        if (e instanceof IllegalStateException) {
+            return STATUS_INVALID_OPERATION;
+        }
+        if (e instanceof IllegalArgumentException || e instanceof NullPointerException) {
+            return STATUS_BAD_VALUE;
+        }
+        // This is not one of the conditions represented by our error code, escalate to a
+        // RuntimeException.
+        Log.e(TAG, "Escalating unexpected exception: ", e);
+        throw new RuntimeException(e);
+    }
+
+    /**
      * Returns a list of descriptors for all hardware modules loaded.
      * @param modules A ModuleProperties array where the list will be returned.
      * @return - {@link #STATUS_OK} in case of success
@@ -1697,9 +1738,8 @@
                 modules.add(ConversionUtil.aidl2apiModuleDescriptor(desc));
             }
             return STATUS_OK;
-        } catch (RemoteException e) {
-            Log.e(TAG, "Exception caught", e);
-            return STATUS_DEAD_OBJECT;
+        } catch (Exception e) {
+            return handleException(e);
         }
     }
 
diff --git a/core/java/android/hardware/soundtrigger/SoundTriggerModule.java b/core/java/android/hardware/soundtrigger/SoundTriggerModule.java
index 03d29a3..9bd3992 100644
--- a/core/java/android/hardware/soundtrigger/SoundTriggerModule.java
+++ b/core/java/android/hardware/soundtrigger/SoundTriggerModule.java
@@ -78,7 +78,7 @@
                 mService = null;
             }
         } catch (Exception e) {
-            handleException(e);
+            SoundTrigger.handleException(e);
         }
     }
 
@@ -115,7 +115,7 @@
             }
             return SoundTrigger.STATUS_BAD_VALUE;
         } catch (Exception e) {
-            return handleException(e);
+            return SoundTrigger.handleException(e);
         }
     }
 
@@ -137,7 +137,7 @@
             mService.unloadModel(soundModelHandle);
             return SoundTrigger.STATUS_OK;
         } catch (Exception e) {
-            return handleException(e);
+            return SoundTrigger.handleException(e);
         }
     }
 
@@ -166,7 +166,7 @@
                     ConversionUtil.api2aidlRecognitionConfig(config));
             return SoundTrigger.STATUS_OK;
         } catch (Exception e) {
-            return handleException(e);
+            return SoundTrigger.handleException(e);
         }
     }
 
@@ -189,7 +189,7 @@
             mService.stopRecognition(soundModelHandle);
             return SoundTrigger.STATUS_OK;
         } catch (Exception e) {
-            return handleException(e);
+            return SoundTrigger.handleException(e);
         }
     }
 
@@ -214,7 +214,7 @@
             mService.forceRecognitionEvent(soundModelHandle);
             return SoundTrigger.STATUS_OK;
         } catch (Exception e) {
-            return handleException(e);
+            return SoundTrigger.handleException(e);
         }
     }
 
@@ -242,7 +242,7 @@
                     ConversionUtil.api2aidlModelParameter(modelParam), value);
             return SoundTrigger.STATUS_OK;
         } catch (Exception e) {
-            return handleException(e);
+            return SoundTrigger.handleException(e);
         }
     }
 
@@ -296,23 +296,6 @@
         }
     }
 
-    private int handleException(Exception e) {
-        Log.e(TAG, "", e);
-        if (e instanceof NullPointerException) {
-            return SoundTrigger.STATUS_NO_INIT;
-        }
-        if (e instanceof RemoteException) {
-            return SoundTrigger.STATUS_DEAD_OBJECT;
-        }
-        if (e instanceof IllegalArgumentException) {
-            return SoundTrigger.STATUS_BAD_VALUE;
-        }
-        if (e instanceof IllegalStateException) {
-            return SoundTrigger.STATUS_INVALID_OPERATION;
-        }
-        return SoundTrigger.STATUS_ERROR;
-    }
-
     private class EventHandlerDelegate extends ISoundTriggerCallback.Stub implements
             IBinder.DeathRecipient {
         private final Handler mHandler;
diff --git a/core/java/android/net/NetworkRequest.java b/core/java/android/net/NetworkRequest.java
index 7fd86f6..301d203 100644
--- a/core/java/android/net/NetworkRequest.java
+++ b/core/java/android/net/NetworkRequest.java
@@ -300,22 +300,34 @@
          * this without a single transport set will generate an exception, as will
          * subsequently adding or removing transports after this is set.
          * </p>
-         * The interpretation of this {@code String} is bearer specific and bearers that use
-         * it should document their particulars.  For example, Bluetooth may use some sort of
-         * device id while WiFi could used ssid and/or bssid.  Cellular may use carrier spn.
+         * If the {@code networkSpecifier} is provided, it shall be interpreted as follows:
+         * <ul>
+         * <li>If the specifier can be parsed as an integer, it will be treated as a
+         * {@link android.net TelephonyNetworkSpecifier}, and the provided integer will be
+         * interpreted as a SubscriptionId.
+         * <li>If the value is an ethernet interface name, it will be treated as such.
+         * <li>For all other cases, the behavior is undefined.
+         * </ul>
          *
-         * @param networkSpecifier An {@code String} of opaque format used to specify the bearer
-         *                         specific network specifier where the bearer has a choice of
-         *                         networks.
+         * @param networkSpecifier A {@code String} of either a SubscriptionId in cellular
+         *                         network request or an ethernet interface name in ethernet
+         *                         network request.
+         *
+         * @deprecated Use {@link #setNetworkSpecifier(NetworkSpecifier)} instead.
          */
+        @Deprecated
         public Builder setNetworkSpecifier(String networkSpecifier) {
-            /*
-             * A StringNetworkSpecifier does not accept null or empty ("") strings. When network
-             * specifiers were strings a null string and an empty string were considered equivalent.
-             * Hence no meaning is attached to a null or empty ("") string.
-             */
-            return setNetworkSpecifier(TextUtils.isEmpty(networkSpecifier) ? null
-                    : new StringNetworkSpecifier(networkSpecifier));
+            try {
+                int subId = Integer.parseInt(networkSpecifier);
+                return setNetworkSpecifier(new TelephonyNetworkSpecifier.Builder()
+                        .setSubscriptionId(subId).build());
+            } catch (NumberFormatException nfe) {
+                // A StringNetworkSpecifier does not accept null or empty ("") strings. When network
+                // specifiers were strings a null string and an empty string were considered
+                // equivalent. Hence no meaning is attached to a null or empty ("") string.
+                return setNetworkSpecifier(TextUtils.isEmpty(networkSpecifier) ? null
+                        : new StringNetworkSpecifier(networkSpecifier));
+            }
         }
 
         /**
diff --git a/core/java/android/net/TelephonyNetworkSpecifier.java b/core/java/android/net/TelephonyNetworkSpecifier.java
new file mode 100644
index 0000000..726f770
--- /dev/null
+++ b/core/java/android/net/TelephonyNetworkSpecifier.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2020 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.net;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * NetworkSpecifier object for cellular network request. Apps should use the
+ * {@link TelephonyNetworkSpecifier.Builder} class to create an instance.
+ */
+public final class TelephonyNetworkSpecifier extends NetworkSpecifier implements Parcelable {
+
+    private final int mSubId;
+
+    /**
+     * Return the subscription Id of current TelephonyNetworkSpecifier object.
+     *
+     * @return The subscription id.
+     */
+    public int getSubscriptionId() {
+        return mSubId;
+    }
+
+    /**
+     * @hide
+     */
+    public TelephonyNetworkSpecifier(int subId) {
+        this.mSubId = subId;
+    }
+
+    public static final @NonNull Creator<TelephonyNetworkSpecifier> CREATOR =
+            new Creator<TelephonyNetworkSpecifier>() {
+                @Override
+                public TelephonyNetworkSpecifier createFromParcel(Parcel in) {
+                    int subId = in.readInt();
+                    return new TelephonyNetworkSpecifier(subId);
+                }
+
+                @Override
+                public TelephonyNetworkSpecifier[] newArray(int size) {
+                    return new TelephonyNetworkSpecifier[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mSubId);
+    }
+
+    @Override
+    public int hashCode() {
+        return mSubId;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof TelephonyNetworkSpecifier)) {
+            return false;
+        }
+
+        TelephonyNetworkSpecifier lhs = (TelephonyNetworkSpecifier) obj;
+        return mSubId == lhs.mSubId;
+    }
+
+    @Override
+    public String toString() {
+        return new StringBuilder()
+                .append("TelephonyNetworkSpecifier [")
+                .append("mSubId = ").append(mSubId)
+                .append("]")
+                .toString();
+    }
+
+    /** @hide */
+    @Override
+    public boolean satisfiedBy(NetworkSpecifier other) {
+        // Any generic requests should be satisfied by a specific telephony network.
+        // For simplicity, we treat null same as MatchAllNetworkSpecifier
+        return equals(other) || other == null || other instanceof MatchAllNetworkSpecifier;
+    }
+
+
+    /**
+     * Builder to create {@link TelephonyNetworkSpecifier} object.
+     */
+    public static final class Builder {
+        // Integer.MIN_VALUE which is not a valid subId, services as the sentinel to check if
+        // subId was set
+        private static final int SENTINEL_SUB_ID = Integer.MIN_VALUE;
+
+        private int mSubId;
+
+        public Builder() {
+            mSubId = SENTINEL_SUB_ID;
+        }
+
+        /**
+         * Set the subscription id.
+         *
+         * @param subId The subscription Id.
+         * @return Instance of {@link Builder} to enable the chaining of the builder method.
+         */
+        public @NonNull Builder setSubscriptionId(int subId) {
+            mSubId = subId;
+            return this;
+        }
+
+        /**
+         * Create a NetworkSpecifier for the cellular network request.
+         *
+         * @return TelephonyNetworkSpecifier object.
+         * @throws IllegalArgumentException when subscription Id is not provided through
+         *         {@link #setSubscriptionId(int)}.
+         */
+        public @NonNull TelephonyNetworkSpecifier build() {
+            if (mSubId == SENTINEL_SUB_ID) {
+                throw new IllegalArgumentException("Subscription Id is not provided.");
+            }
+            return new TelephonyNetworkSpecifier(mSubId);
+        }
+    }
+}
diff --git a/core/java/android/os/IIncidentDumpCallback.aidl b/core/java/android/os/IIncidentDumpCallback.aidl
new file mode 100644
index 0000000..09b5b01
--- /dev/null
+++ b/core/java/android/os/IIncidentDumpCallback.aidl
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2019, 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.os;
+
+import android.os.ParcelFileDescriptor;
+
+/**
+ * Callback from IIncidentManager to dump an extended section.
+ *
+ * @hide
+ */
+oneway interface IIncidentDumpCallback {
+    /**
+     * Dumps section data to the given ParcelFileDescriptor.
+     */
+    void onDumpSection(in ParcelFileDescriptor fd);
+}
diff --git a/core/java/android/os/IIncidentManager.aidl b/core/java/android/os/IIncidentManager.aidl
index 7e1b1e0..923234e 100644
--- a/core/java/android/os/IIncidentManager.aidl
+++ b/core/java/android/os/IIncidentManager.aidl
@@ -17,6 +17,7 @@
 package android.os;
 
 import android.os.IIncidentReportStatusListener;
+import android.os.IIncidentDumpCallback;
 import android.os.IncidentManager;
 import android.os.IncidentReportArgs;
 
@@ -52,6 +53,19 @@
             @nullable IIncidentReportStatusListener listener);
 
     /**
+     * Register a section callback with the given id and name. The callback function
+     * will be invoked when an incident report with all sections or sections matching
+     * the given id is being taken.
+     */
+    oneway void registerSection(int id, String name, IIncidentDumpCallback callback);
+
+    /**
+     * Unregister a section callback associated with the given id. The section must be
+     * previously registered with registerSection(int, String, IIncidentDumpCallback).
+     */
+    oneway void unregisterSection(int id);
+
+    /**
      * Tell the incident daemon that the android system server is up and running.
      */
     oneway void systemRunning();
diff --git a/core/java/android/os/IncidentManager.java b/core/java/android/os/IncidentManager.java
index 09e1c0f..f6563eb 100644
--- a/core/java/android/os/IncidentManager.java
+++ b/core/java/android/os/IncidentManager.java
@@ -31,6 +31,7 @@
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -421,6 +422,39 @@
     }
 
     /**
+     * Callback for dumping an extended (usually vendor-supplied) incident report section
+     *
+     * @see #registerSection
+     * @see #unregisterSection
+     *
+     * @hide
+     */
+    public static class DumpCallback {
+        private Executor mExecutor;
+
+        IIncidentDumpCallback.Stub mBinder = new IIncidentDumpCallback.Stub() {
+            @Override
+            public void onDumpSection(ParcelFileDescriptor pfd) {
+                if (mExecutor != null) {
+                    mExecutor.execute(() -> {
+                        DumpCallback.this.onDumpSection(
+                                new ParcelFileDescriptor.AutoCloseOutputStream(pfd));
+                    });
+                } else {
+                    DumpCallback.this.onDumpSection(
+                            new ParcelFileDescriptor.AutoCloseOutputStream(pfd));
+                }
+            }
+        };
+
+        /**
+         * Called when incidentd requests to dump this section.
+         */
+        public void onDumpSection(OutputStream out) {
+        }
+    }
+
+    /**
      * @hide
      */
     public IncidentManager(Context context) {
@@ -528,6 +562,61 @@
     }
 
     /**
+     * Register a callback to dump an extended incident report section with the given id and name.
+     * The callback function will be invoked when an incident report with all sections or sections
+     * matching the given id is being taken.
+     *
+     * @hide
+     */
+    public void registerSection(int id, String name, @NonNull DumpCallback callback) {
+        registerSection(id, name, mContext.getMainExecutor(), callback);
+    }
+
+    /**
+     * Register a callback to dump an extended incident report section with the given id and name,
+     * running on the supplied executor.
+     *
+     * @hide
+     */
+    public void registerSection(int id, String name, @NonNull @CallbackExecutor Executor executor,
+            @NonNull DumpCallback callback) {
+        try {
+            if (callback.mExecutor != null) {
+                throw new RuntimeException("Do not reuse DumpCallback objects when calling"
+                        + " registerSection");
+            }
+            callback.mExecutor = executor;
+            final IIncidentManager service = getIIncidentManagerLocked();
+            if (service == null) {
+                Slog.e(TAG, "registerSection can't find incident binder service");
+                return;
+            }
+            service.registerSection(id, name, callback.mBinder);
+        } catch (RemoteException ex) {
+            Slog.e(TAG, "registerSection failed", ex);
+        }
+    }
+
+    /**
+     * Unregister an extended section dump function. The section must be previously registered with
+     * {@link #registerSection(int, String, DumpCallback)}
+     *
+     * @hide
+     */
+    public void unregisterSection(int id) {
+        try {
+            final IIncidentManager service = getIIncidentManagerLocked();
+            if (service == null) {
+                Slog.e(TAG, "unregisterSection can't find incident binder service");
+                return;
+            }
+            service.unregisterSection(id);
+        } catch (RemoteException ex) {
+            Slog.e(TAG, "unregisterSection failed", ex);
+        }
+    }
+
+    /**
      * Get the incident reports that are available for upload for the supplied
      * broadcast recevier.
      *
diff --git a/core/java/android/telephony/PhoneStateListener.java b/core/java/android/telephony/PhoneStateListener.java
index e08a06a..c9d3b92 100644
--- a/core/java/android/telephony/PhoneStateListener.java
+++ b/core/java/android/telephony/PhoneStateListener.java
@@ -384,6 +384,17 @@
     @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
     public static final int LISTEN_REGISTRATION_FAILURE = 0x40000000;
 
+    /**
+     * Listen for Barring Information for the current registered / camped cell.
+     *
+     * <p>Requires permission {@link android.Manifest.permission#READ_PHONE_STATE} or the calling
+     * app has carrier privileges (see {@link TelephonyManager#hasCarrierPrivileges}).
+     *
+     * @see #onBarringInfoChanged()
+     */
+    @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
+    public static final int LISTEN_BARRING_INFO = 0x80000000;
+
     /*
      * Subscription used to listen to the phone state changes
      * @hide
@@ -979,6 +990,20 @@
     }
 
     /**
+     * Report updated barring information for the current camped/registered cell.
+     *
+     * <p>Barring info is provided for all services applicable to the current camped/registered
+     * cell, for the registered PLMN and current access class/access category.
+     *
+     * @param barringInfo for all services on the current cell.
+     *
+     * @see android.telephony.BarringInfo
+     */
+    public void onBarringInfoChanged(@NonNull BarringInfo barringInfo) {
+        // default implementation empty
+    }
+
+    /**
      * The callback methods need to be called on the handler thread where
      * this object was created.  If the binder did that for us it'd be nice.
      *
@@ -1262,6 +1287,14 @@
                             cellIdentity, chosenPlmn, domain, causeCode, additionalCauseCode)));
             // default implementation empty
         }
+
+        public void onBarringInfoChanged(BarringInfo barringInfo) {
+            PhoneStateListener psl = mPhoneStateListenerWeakRef.get();
+            if (psl == null) return;
+
+            Binder.withCleanCallingIdentity(
+                    () -> mExecutor.execute(() -> psl.onBarringInfoChanged(barringInfo)));
+        }
     }
 
 
diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java
index 9387a2c..4dffa62 100644
--- a/core/java/android/telephony/TelephonyRegistryManager.java
+++ b/core/java/android/telephony/TelephonyRegistryManager.java
@@ -708,4 +708,21 @@
         } catch (RemoteException ex) {
         }
     }
+
+    /**
+     * Notify {@link BarringInfo} has changed for a specific subscription.
+     *
+     * @param slotIndex for the phone object that got updated barring info.
+     * @param subId for which the BarringInfo changed.
+     * @param barringInfo updated BarringInfo.
+     */
+    public void notifyBarringInfoChanged(
+            int slotIndex, int subId, @NonNull BarringInfo barringInfo) {
+        try {
+            sRegistry.notifyBarringInfoChanged(slotIndex, subId, barringInfo);
+        } catch (RemoteException ex) {
+            // system server crash
+        }
+    }
+
 }
diff --git a/core/java/android/view/WindowContainerTransaction.java b/core/java/android/view/WindowContainerTransaction.java
index 253794f..c55caa3 100644
--- a/core/java/android/view/WindowContainerTransaction.java
+++ b/core/java/android/view/WindowContainerTransaction.java
@@ -74,6 +74,18 @@
         return this;
     }
 
+    /**
+     * Sets whether a container or any of its children can be focusable. When {@code false}, no
+     * child can be focused; however, when {@code true}, it is still possible for children to be
+     * non-focusable due to WM policy.
+     */
+    public WindowContainerTransaction setFocusable(IWindowContainer container, boolean focusable) {
+        Change chg = getOrCreateChange(container.asBinder());
+        chg.mFocusable = focusable;
+        chg.mChangeMask |= Change.CHANGE_FOCUSABLE;
+        return this;
+    }
+
     public Map<IBinder, Change> getChanges() {
         return mChanges;
     }
@@ -112,7 +124,11 @@
      * @hide
      */
     public static class Change implements Parcelable {
+        public static final int CHANGE_FOCUSABLE = 1;
+
         private final Configuration mConfiguration = new Configuration();
+        private boolean mFocusable = true;
+        private int mChangeMask = 0;
         private @ActivityInfo.Config int mConfigSetMask = 0;
         private @WindowConfiguration.WindowConfig int mWindowSetMask = 0;
 
@@ -123,6 +139,8 @@
 
         protected Change(Parcel in) {
             mConfiguration.readFromParcel(in);
+            mFocusable = in.readBoolean();
+            mChangeMask = in.readInt();
             mConfigSetMask = in.readInt();
             mWindowSetMask = in.readInt();
             mSchedulePipCallback = (in.readInt() != 0);
@@ -136,6 +154,18 @@
             return mConfiguration;
         }
 
+        /** Gets the requested focusable value */
+        public boolean getFocusable() {
+            if ((mChangeMask & CHANGE_FOCUSABLE) == 0) {
+                throw new RuntimeException("Focusable not set. check CHANGE_FOCUSABLE first");
+            }
+            return mFocusable;
+        }
+
+        public int getChangeMask() {
+            return mChangeMask;
+        }
+
         @ActivityInfo.Config
         public int getConfigSetMask() {
             return mConfigSetMask;
@@ -170,6 +200,9 @@
             if (changesSss) {
                 sb.append("ssw:" + mConfiguration.smallestScreenWidthDp + ",");
             }
+            if ((mChangeMask & CHANGE_FOCUSABLE) != 0) {
+                sb.append("focusable:" + mFocusable + ",");
+            }
             sb.append("}");
             return sb.toString();
         }
@@ -177,6 +210,8 @@
         @Override
         public void writeToParcel(Parcel dest, int flags) {
             mConfiguration.writeToParcel(dest, flags);
+            dest.writeBoolean(mFocusable);
+            dest.writeInt(mChangeMask);
             dest.writeInt(mConfigSetMask);
             dest.writeInt(mWindowSetMask);
 
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index f851e10..b891af5 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -152,7 +152,7 @@
 
     // Specifies whether to allow starting a cursor drag by dragging anywhere over the text.
     @VisibleForTesting
-    public static boolean FLAG_ENABLE_CURSOR_DRAG = false;
+    public static boolean FLAG_ENABLE_CURSOR_DRAG = true;
     // Specifies whether to use the magnifier when pressing the insertion or selection handles.
     private static final boolean FLAG_USE_MAGNIFIER = true;
 
@@ -5741,10 +5741,10 @@
                 return;
             }
             switch (event.getActionMasked()) {
-                case MotionEvent.ACTION_DOWN:
-                    mIsDraggingCursor = false;
-                    break;
                 case MotionEvent.ACTION_MOVE:
+                    if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
+                        break;
+                    }
                     if (mIsDraggingCursor) {
                         performCursorDrag(event);
                     } else if (FLAG_ENABLE_CURSOR_DRAG
diff --git a/core/java/com/android/internal/app/AccessibilityButtonChooserActivity.java b/core/java/com/android/internal/app/AccessibilityButtonChooserActivity.java
index b670316..a9f0175 100644
--- a/core/java/com/android/internal/app/AccessibilityButtonChooserActivity.java
+++ b/core/java/com/android/internal/app/AccessibilityButtonChooserActivity.java
@@ -325,11 +325,11 @@
         return false;
     }
 
-    private void disableWhiteListingService(String componentId) {
+    private void setWhiteListingServiceEnabled(String componentId, int settingsValue) {
         for (int i = 0; i < WHITE_LISTING_FEATURES.length; i++) {
             if (WHITE_LISTING_FEATURES[i][COMPONENT_ID].equals(componentId)) {
                 Settings.Secure.putInt(getContentResolver(),
-                        WHITE_LISTING_FEATURES[i][SETTINGS_KEY], /* settingsValueOn */ 1);
+                        WHITE_LISTING_FEATURES[i][SETTINGS_KEY], settingsValue);
                 return;
             }
         }
@@ -339,7 +339,8 @@
         final String componentId = componentName.flattenToString();
 
         if (isWhiteListingService(componentId)) {
-            disableWhiteListingService(componentName.flattenToString());
+            setWhiteListingServiceEnabled(componentName.flattenToString(),
+                    /* settingsValueOff */ 0);
         } else {
             setAccessibilityServiceState(this, componentName, /* enabled= */ false);
         }
@@ -545,13 +546,65 @@
     }
 
     private void onTargetSelected(AdapterView<?> parent, View view, int position, long id) {
-        Settings.Secure.putString(getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_BUTTON_TARGET_COMPONENT,
-                mTargets.get(position).getId());
-        // TODO(b/146969684): notify accessibility button clicked.
+        final AccessibilityButtonTarget target = mTargets.get(position);
+        switch (target.getFragmentType()) {
+            case AccessibilityServiceFragmentType.LEGACY:
+                onLegacyTargetSelected(target);
+                break;
+            case AccessibilityServiceFragmentType.INVISIBLE:
+                onInvisibleTargetSelected(target);
+                break;
+            case AccessibilityServiceFragmentType.INTUITIVE:
+                onIntuitiveTargetSelected(target);
+                break;
+            case AccessibilityServiceFragmentType.BOUNCE:
+                // Do nothing
+                break;
+            default:
+                throw new IllegalStateException("Unexpected fragment type");
+        }
+
         mAlertDialog.dismiss();
     }
 
+    private void onLegacyTargetSelected(AccessibilityButtonTarget target) {
+        if (mShortcutType == ACCESSIBILITY_BUTTON) {
+            final AccessibilityManager ams = (AccessibilityManager) getSystemService(
+                    Context.ACCESSIBILITY_SERVICE);
+            ams.notifyAccessibilityButtonClicked(getDisplayId(), target.getId());
+        } else if (mShortcutType == ACCESSIBILITY_SHORTCUT_KEY) {
+            switchServiceState(target);
+        } else {
+            throw new IllegalArgumentException("Unsupported shortcut type:" + mShortcutType);
+        }
+    }
+
+    private void onInvisibleTargetSelected(AccessibilityButtonTarget target) {
+        final AccessibilityManager ams = (AccessibilityManager) getSystemService(
+                Context.ACCESSIBILITY_SERVICE);
+        ams.notifyAccessibilityButtonClicked(getDisplayId(), target.getId());
+    }
+
+    private void onIntuitiveTargetSelected(AccessibilityButtonTarget target) {
+        switchServiceState(target);
+    }
+
+    private void switchServiceState(AccessibilityButtonTarget target) {
+        final ComponentName componentName =
+                ComponentName.unflattenFromString(target.getId());
+        final String componentId = componentName.flattenToString();
+
+        if (isWhiteListingService(componentId)) {
+            setWhiteListingServiceEnabled(componentId,
+                    isWhiteListingServiceEnabled(this, target)
+                            ? /* settingsValueOff */ 0
+                            : /* settingsValueOn */ 1);
+        } else {
+            setAccessibilityServiceState(this, componentName,
+                    /* enabled= */!isAccessibilityServiceEnabled(this, target));
+        }
+    }
+
     private void onTargetDeleted(AdapterView<?> parent, View view, int position, long id) {
         final AccessibilityButtonTarget target = mTargets.get(position);
         final ComponentName targetComponentName =
diff --git a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
index 6fd271c..0f50596 100644
--- a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
+++ b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
@@ -16,6 +16,7 @@
 
 package com.android.internal.telephony;
 
+import android.telephony.BarringInfo;
 import android.telephony.CallAttributes;
 import android.telephony.CellIdentity;
 import android.telephony.CellInfo;
@@ -64,4 +65,5 @@
     void onImsCallDisconnectCauseChanged(in ImsReasonInfo imsReasonInfo);
     void onRegistrationFailed(in CellIdentity cellIdentity,
              String chosenPlmn, int domain, int causeCode, int additionalCauseCode);
+    void onBarringInfoChanged(in BarringInfo barringInfo);
 }
diff --git a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
index 8e97ae1..47752c5 100644
--- a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
+++ b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
@@ -19,6 +19,7 @@
 import android.content.Intent;
 import android.net.LinkProperties;
 import android.net.NetworkCapabilities;
+import android.telephony.BarringInfo;
 import android.telephony.CallQuality;
 import android.telephony.CellIdentity;
 import android.telephony.CellInfo;
@@ -99,4 +100,5 @@
     void notifyImsDisconnectCause(int subId, in ImsReasonInfo imsReasonInfo);
     void notifyRegistrationFailed(int slotIndex, int subId, in CellIdentity cellIdentity,
             String chosenPlmn, int domain, int causeCode, int additionalCauseCode);
+    void notifyBarringInfoChanged(int slotIndex, int subId, in BarringInfo barringInfo);
 }
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index 53327bc..c4ee195 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -2627,7 +2627,7 @@
     gMidAudioRecordRoutingProxy_release =
             android::GetMethodIDOrDie(env, gClsAudioRecordRoutingProxy, "native_release", "()V");
 
-    AudioSystem::setErrorCallback(android_media_AudioSystem_error_callback);
+    AudioSystem::addErrorCallback(android_media_AudioSystem_error_callback);
 
     RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods));
     return RegisterMethodsOrDie(env, kEventHandlerClassPathName, gEventHandlerMethods,
diff --git a/core/res/res/layout/autofill_inline_suggestion.xml b/core/res/res/layout/autofill_inline_suggestion.xml
new file mode 100644
index 0000000..f7ac164
--- /dev/null
+++ b/core/res/res/layout/autofill_inline_suggestion.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="wrap_content"
+    android:layout_height="56dp"
+    android:background="@color/white"
+    android:orientation="horizontal"
+    android:paddingStart="12dp"
+    android:paddingEnd="12dp">
+
+    <ImageView
+        android:id="@+id/autofill_inline_suggestion_start_icon"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:layout_gravity="center"
+        android:contentDescription="autofill_inline_suggestion_start_icon" />
+
+    <LinearLayout
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_gravity="center"
+        android:layout_weight="1"
+        android:layout_marginStart="12dp"
+        android:layout_marginEnd="12dp"
+        android:orientation="vertical"
+        android:gravity="center_vertical">
+
+        <TextView
+            android:id="@+id/autofill_inline_suggestion_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:maxLines="1"
+            tools:text="username1"/>
+
+        <TextView
+            android:id="@+id/autofill_inline_suggestion_subtitle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:maxLines="1"
+            tools:text="inline fill service"/>
+    </LinearLayout>
+
+    <ImageView
+        android:id="@+id/autofill_inline_suggestion_end_icon"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:layout_gravity="center"
+        android:contentDescription="autofill_inline_suggestion_end_icon" />
+</LinearLayout>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index f977ea8..4354c0d 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -723,11 +723,11 @@
     <string name="permgroupdesc_sms">send and view SMS messages</string>
 
     <!-- Title of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. -->
-    <string name="permgrouplab_storage">Storage</string>
+    <string name="permgrouplab_storage">Files and media</string>
     <!-- Description of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permgroupdesc_storage">access photos, media, and files on your device</string>
 
-    <!-- Title of a category of application permissioncds, listed so the user can choose whether they want to allow the application to do this. -->
+    <!-- Title of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permgrouplab_microphone">Microphone</string>
     <!-- Description of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permgroupdesc_microphone">record audio</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 30dbfc7..67e4743 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3235,6 +3235,7 @@
   <java-symbol type="layout" name="autofill_dataset_picker"/>
   <java-symbol type="layout" name="autofill_dataset_picker_fullscreen"/>
   <java-symbol type="layout" name="autofill_dataset_picker_header_footer"/>
+  <java-symbol type="layout" name="autofill_inline_suggestion" />
   <java-symbol type="id" name="autofill" />
   <java-symbol type="id" name="autofill_dataset_footer"/>
   <java-symbol type="id" name="autofill_dataset_header"/>
@@ -3242,6 +3243,10 @@
   <java-symbol type="id" name="autofill_dataset_list"/>
   <java-symbol type="id" name="autofill_dataset_picker"/>
   <java-symbol type="id" name="autofill_dataset_title" />
+  <java-symbol type="id" name="autofill_inline_suggestion_end_icon" />
+  <java-symbol type="id" name="autofill_inline_suggestion_start_icon" />
+  <java-symbol type="id" name="autofill_inline_suggestion_subtitle" />
+  <java-symbol type="id" name="autofill_inline_suggestion_title" />
   <java-symbol type="id" name="autofill_save_custom_subtitle" />
   <java-symbol type="id" name="autofill_save_icon" />
   <java-symbol type="id" name="autofill_save_no" />
diff --git a/core/tests/coretests/src/android/app/appsearch/AppSearchDocumentTest.java b/core/tests/coretests/src/android/app/appsearch/AppSearchDocumentTest.java
new file mode 100644
index 0000000..2091d55
--- /dev/null
+++ b/core/tests/coretests/src/android/app/appsearch/AppSearchDocumentTest.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2020 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.app.appsearch;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.app.appsearch.AppSearch.Document;
+
+import androidx.test.filters.SmallTest;
+
+import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.PropertyProto;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+@SmallTest
+public class AppSearchDocumentTest {
+
+    @Test
+    public void testDocumentEquals_Identical() {
+        Document document1 = Document.newBuilder("uri1", "schemaType1")
+                .setCreationTimestampSecs(0L)
+                .setProperty("longKey1", 1L, 2L, 3L)
+                .setProperty("doubleKey1", 1.0, 2.0, 3.0)
+                .setProperty("booleanKey1", true, false, true)
+                .setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
+                .build();
+        Document document2 = Document.newBuilder("uri1", "schemaType1")
+                .setCreationTimestampSecs(0L)
+                .setProperty("longKey1", 1L, 2L, 3L)
+                .setProperty("doubleKey1", 1.0, 2.0, 3.0)
+                .setProperty("booleanKey1", true, false, true)
+                .setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
+                .build();
+        assertThat(document1).isEqualTo(document2);
+        assertThat(document1.hashCode()).isEqualTo(document2.hashCode());
+    }
+
+    @Test
+    public void testDocumentEquals_DifferentOrder() {
+        Document document1 = Document.newBuilder("uri1", "schemaType1")
+                .setCreationTimestampSecs(0L)
+                .setProperty("longKey1", 1L, 2L, 3L)
+                .setProperty("doubleKey1", 1.0, 2.0, 3.0)
+                .setProperty("booleanKey1", true, false, true)
+                .setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
+                .build();
+
+        // Create second document with same parameter but different order.
+        Document document2 = Document.newBuilder("uri1", "schemaType1")
+                .setCreationTimestampSecs(0L)
+                .setProperty("booleanKey1", true, false, true)
+                .setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
+                .setProperty("doubleKey1", 1.0, 2.0, 3.0)
+                .setProperty("longKey1", 1L, 2L, 3L)
+                .build();
+        assertThat(document1).isEqualTo(document2);
+        assertThat(document1.hashCode()).isEqualTo(document2.hashCode());
+    }
+
+    @Test
+    public void testDocumentEquals_Failure() {
+        Document document1 = Document.newBuilder("uri1", "schemaType1")
+                .setProperty("longKey1", 1L, 2L, 3L)
+                .build();
+
+        // Create second document with same order but different value.
+        Document document2 = Document.newBuilder("uri1", "schemaType1")
+                .setProperty("longKey1", 1L, 2L, 4L) // Different
+                .build();
+        assertThat(document1).isNotEqualTo(document2);
+        assertThat(document1.hashCode()).isNotEqualTo(document2.hashCode());
+    }
+
+    @Test
+    public void testDocumentEquals_Failure_RepeatedFieldOrder() {
+        Document document1 = Document.newBuilder("uri1", "schemaType1")
+                .setProperty("booleanKey1", true, false, true)
+                .build();
+
+        // Create second document with same order but different value.
+        Document document2 = Document.newBuilder("uri1", "schemaType1")
+                .setProperty("booleanKey1", true, true, false) // Different
+                .build();
+        assertThat(document1).isNotEqualTo(document2);
+        assertThat(document1.hashCode()).isNotEqualTo(document2.hashCode());
+    }
+
+    @Test
+    public void testDocumentGetSingleValue() {
+        Document document = Document.newBuilder("uri1", "schemaType1")
+                .setProperty("longKey1", 1L)
+                .setProperty("doubleKey1", 1.0)
+                .setProperty("booleanKey1", true)
+                .setProperty("stringKey1", "test-value1").build();
+        assertThat(document.getUri()).isEqualTo("uri1");
+        assertThat(document.getSchemaType()).isEqualTo("schemaType1");
+        assertThat(document.getPropertyLong("longKey1")).isEqualTo(1L);
+        assertThat(document.getPropertyDouble("doubleKey1")).isEqualTo(1.0);
+        assertThat(document.getPropertyBoolean("booleanKey1")).isTrue();
+        assertThat(document.getPropertyString("stringKey1")).isEqualTo("test-value1");
+    }
+
+    @Test
+    public void testDocumentGetArrayValues() {
+        Document document = Document.newBuilder("uri1", "schemaType1")
+                .setScore(1)
+                .setProperty("longKey1", 1L, 2L, 3L)
+                .setProperty("doubleKey1", 1.0, 2.0, 3.0)
+                .setProperty("booleanKey1", true, false, true)
+                .setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
+                .build();
+
+        assertThat(document.getUri()).isEqualTo("uri1");
+        assertThat(document.getSchemaType()).isEqualTo("schemaType1");
+        assertThat(document.getScore()).isEqualTo(1);
+        assertThat(document.getPropertyLongArray("longKey1")).asList().containsExactly(1L, 2L, 3L);
+        assertThat(document.getPropertyDoubleArray("doubleKey1")).usingExactEquality()
+                .containsExactly(1.0, 2.0, 3.0);
+        assertThat(document.getPropertyBooleanArray("booleanKey1")).asList()
+                .containsExactly(true, false, true);
+        assertThat(document.getPropertyStringArray("stringKey1")).asList()
+                .containsExactly("test-value1", "test-value2", "test-value3");
+    }
+
+    @Test
+    public void testDocumentGetValues_DifferentTypes() {
+        Document document = Document.newBuilder("uri1", "schemaType1")
+                .setScore(1)
+                .setProperty("longKey1", 1L)
+                .setProperty("booleanKey1", true, false, true)
+                .setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
+                .build();
+
+        // Get a value for a key that doesn't exist
+        assertThat(document.getPropertyDouble("doubleKey1")).isNull();
+        assertThat(document.getPropertyDoubleArray("doubleKey1")).isNull();
+
+        // Get a value with a single element as an array and as a single value
+        assertThat(document.getPropertyLong("longKey1")).isEqualTo(1L);
+        assertThat(document.getPropertyLongArray("longKey1")).asList().containsExactly(1L);
+
+        // Get a value with multiple elements as an array and as a single value
+        assertThat(document.getPropertyString("stringKey1")).isEqualTo("test-value1");
+        assertThat(document.getPropertyStringArray("stringKey1")).asList()
+                .containsExactly("test-value1", "test-value2", "test-value3");
+
+        // Get a value of the wrong type
+        assertThat(document.getPropertyDouble("longKey1")).isNull();
+        assertThat(document.getPropertyDoubleArray("longKey1")).isNull();
+    }
+
+    @Test
+    public void testDocumentInvalid() {
+        Document.Builder builder = Document.newBuilder("uri1", "schemaType1");
+        assertThrows(
+                IllegalArgumentException.class, () -> builder.setProperty("test", new boolean[]{}));
+    }
+
+    @Test
+    public void testDocumentProtoPopulation() {
+        Document document = Document.newBuilder("uri1", "schemaType1")
+                .setScore(1)
+                .setCreationTimestampSecs(0)
+                .setProperty("longKey1", 1L)
+                .setProperty("doubleKey1", 1.0)
+                .setProperty("booleanKey1", true)
+                .setProperty("stringKey1", "test-value1")
+                .build();
+
+        // Create the Document proto. Need to sort the property order by key.
+        DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder()
+                .setUri("uri1").setSchema("schemaType1").setScore(1).setCreationTimestampSecs(0);
+        HashMap<String, PropertyProto.Builder> propertyProtoMap = new HashMap<>();
+        propertyProtoMap.put("longKey1",
+                PropertyProto.newBuilder().setName("longKey1").addInt64Values(1L));
+        propertyProtoMap.put("doubleKey1",
+                PropertyProto.newBuilder().setName("doubleKey1").addDoubleValues(1.0));
+        propertyProtoMap.put("booleanKey1",
+                PropertyProto.newBuilder().setName("booleanKey1").addBooleanValues(true));
+        propertyProtoMap.put("stringKey1",
+                PropertyProto.newBuilder().setName("stringKey1").addStringValues("test-value1"));
+        List<String> sortedKey = new ArrayList<>(propertyProtoMap.keySet());
+        Collections.sort(sortedKey);
+        for (String key : sortedKey) {
+            documentProtoBuilder.addProperties(propertyProtoMap.get(key));
+        }
+        assertThat(document.getProto()).isEqualTo(documentProtoBuilder.build());
+    }
+}
diff --git a/core/tests/coretests/src/android/app/appsearch/AppSearchEmailTest.java b/core/tests/coretests/src/android/app/appsearch/AppSearchEmailTest.java
new file mode 100644
index 0000000..c50b1da
--- /dev/null
+++ b/core/tests/coretests/src/android/app/appsearch/AppSearchEmailTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2020 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.app.appsearch;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.appsearch.AppSearch.Email;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+@SmallTest
+public class AppSearchEmailTest {
+
+    @Test
+    public void testBuildEmailAndGetValue() {
+        Email email = Email.newBuilder("uri")
+                .setFrom("FakeFromAddress")
+                .setCc("CC1", "CC2")
+                // Score and Property are mixed into the middle to make sure DocumentBuilder's
+                // methods can be interleaved with EmailBuilder's methods.
+                .setScore(1)
+                .setProperty("propertyKey", "propertyValue1", "propertyValue2")
+                .setSubject("subject")
+                .setBody("EmailBody")
+                .build();
+
+        assertThat(email.getUri()).isEqualTo("uri");
+        assertThat(email.getFrom()).isEqualTo("FakeFromAddress");
+        assertThat(email.getTo()).isNull();
+        assertThat(email.getCc()).asList().containsExactly("CC1", "CC2");
+        assertThat(email.getBcc()).isNull();
+        assertThat(email.getScore()).isEqualTo(1);
+        assertThat(email.getPropertyString("propertyKey")).isEqualTo("propertyValue1");
+        assertThat(email.getPropertyStringArray("propertyKey")).asList().containsExactly(
+                "propertyValue1", "propertyValue2");
+        assertThat(email.getSubject()).isEqualTo("subject");
+        assertThat(email.getBody()).isEqualTo("EmailBody");
+    }
+}
diff --git a/core/tests/coretests/src/android/app/appsearch/impl/CustomerDocumentTest.java b/core/tests/coretests/src/android/app/appsearch/impl/CustomerDocumentTest.java
new file mode 100644
index 0000000..4ee4aa6
--- /dev/null
+++ b/core/tests/coretests/src/android/app/appsearch/impl/CustomerDocumentTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2020 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.app.appsearch.impl;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearch.Document;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/** Tests that {@link Document} and {@link Document.Builder} are extendable by developers.
+ *
+ * <p>This class is intentionally in a different package than {@link Document} to make sure there
+ * are no package-private methods required for external developers to add custom types.
+ */
+@SmallTest
+public class CustomerDocumentTest {
+    @Test
+    public void testBuildCustomerDocument() {
+        CustomerDocument customerDocument = CustomerDocument.newBuilder("uri1")
+                .setScore(1)
+                .setCreationTimestampSecs(0)
+                .setProperty("longKey1", 1L, 2L, 3L)
+                .setProperty("doubleKey1", 1.0, 2.0, 3.0)
+                .setProperty("booleanKey1", true, false, true)
+                .setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
+                .build();
+
+        assertThat(customerDocument.getUri()).isEqualTo("uri1");
+        assertThat(customerDocument.getSchemaType()).isEqualTo("customerDocument");
+        assertThat(customerDocument.getScore()).isEqualTo(1);
+        assertThat(customerDocument.getCreationTimestampSecs()).isEqualTo(0L);
+        assertThat(customerDocument.getPropertyLongArray("longKey1")).asList()
+                .containsExactly(1L, 2L, 3L);
+        assertThat(customerDocument.getPropertyDoubleArray("doubleKey1")).usingExactEquality()
+                .containsExactly(1.0, 2.0, 3.0);
+        assertThat(customerDocument.getPropertyBooleanArray("booleanKey1")).asList()
+                .containsExactly(true, false, true);
+        assertThat(customerDocument.getPropertyStringArray("stringKey1")).asList()
+                .containsExactly("test-value1", "test-value2", "test-value3");
+    }
+
+    /**
+     * An example document type for test purposes, defined outside of
+     * {@link android.app.appsearch.AppSearch} (the way an external developer would define it).
+     */
+    private static class CustomerDocument extends Document {
+        private CustomerDocument(Document document) {
+            super(document);
+        }
+
+        public static CustomerDocument.Builder newBuilder(String uri) {
+            return new CustomerDocument.Builder(uri);
+        }
+
+        public static class Builder extends Document.Builder<CustomerDocument.Builder> {
+            private Builder(@NonNull String uri) {
+                super(uri, "customerDocument");
+            }
+
+            @Override
+            public CustomerDocument build() {
+                return new CustomerDocument(super.build());
+            }
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/widget/EditorCursorDragTest.java b/core/tests/coretests/src/android/widget/EditorCursorDragTest.java
index 8c9b4d0..a602fa3 100644
--- a/core/tests/coretests/src/android/widget/EditorCursorDragTest.java
+++ b/core/tests/coretests/src/android/widget/EditorCursorDragTest.java
@@ -32,6 +32,7 @@
 
 import android.app.Activity;
 import android.app.Instrumentation;
+import android.view.InputDevice;
 import android.view.MotionEvent;
 
 import androidx.test.InstrumentationRegistry;
@@ -282,6 +283,35 @@
     }
 
     @Test
+    public void testEditor_onTouchEvent_mouseDrag() throws Throwable {
+        String text = "testEditor_onTouchEvent_mouseDrag";
+        onView(withId(R.id.textview)).perform(replaceText(text));
+        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
+
+        TextView tv = mActivity.findViewById(R.id.textview);
+        Editor editor = tv.getEditorForTesting();
+
+        // Simulate a mouse click and drag. This should NOT trigger a cursor drag.
+        long event1Time = 1001;
+        MotionEvent event1 = mouseDownEvent(event1Time, event1Time, 20f, 30f);
+        mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event1));
+        assertFalse(editor.getInsertionController().isCursorBeingModified());
+        assertFalse(editor.getSelectionController().isCursorBeingModified());
+
+        long event2Time = 1002;
+        MotionEvent event2 = mouseMoveEvent(event1Time, event2Time, 120f, 30f);
+        mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event2));
+        assertFalse(editor.getInsertionController().isCursorBeingModified());
+        assertTrue(editor.getSelectionController().isCursorBeingModified());
+
+        long event3Time = 1003;
+        MotionEvent event3 = mouseUpEvent(event1Time, event3Time, 120f, 30f);
+        mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event3));
+        assertFalse(editor.getInsertionController().isCursorBeingModified());
+        assertFalse(editor.getSelectionController().isCursorBeingModified());
+    }
+
+    @Test
     public void testEditor_onTouchEvent_cursorDrag() throws Throwable {
         String text = "testEditor_onTouchEvent_cursorDrag";
         onView(withId(R.id.textview)).perform(replaceText(text));
@@ -385,4 +415,25 @@
     private static MotionEvent moveEvent(long downTime, long eventTime, float x, float y) {
         return MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 0);
     }
+
+    private static MotionEvent mouseDownEvent(long downTime, long eventTime, float x, float y) {
+        MotionEvent event = downEvent(downTime, eventTime, x, y);
+        event.setSource(InputDevice.SOURCE_MOUSE);
+        event.setButtonState(MotionEvent.BUTTON_PRIMARY);
+        return event;
+    }
+
+    private static MotionEvent mouseUpEvent(long downTime, long eventTime, float x, float y) {
+        MotionEvent event = upEvent(downTime, eventTime, x, y);
+        event.setSource(InputDevice.SOURCE_MOUSE);
+        event.setButtonState(0);
+        return event;
+    }
+
+    private static MotionEvent mouseMoveEvent(long downTime, long eventTime, float x, float y) {
+        MotionEvent event = moveEvent(downTime, eventTime, x, y);
+        event.setSource(InputDevice.SOURCE_MOUSE);
+        event.setButtonState(MotionEvent.BUTTON_PRIMARY);
+        return event;
+    }
 }
diff --git a/core/tests/coretests/src/android/widget/TextViewActivityMouseTest.java b/core/tests/coretests/src/android/widget/TextViewActivityMouseTest.java
index 8bfd0a0..a0cfb31 100644
--- a/core/tests/coretests/src/android/widget/TextViewActivityMouseTest.java
+++ b/core/tests/coretests/src/android/widget/TextViewActivityMouseTest.java
@@ -19,6 +19,7 @@
 import static android.widget.espresso.ContextMenuUtils.assertContextMenuContainsItemDisabled;
 import static android.widget.espresso.ContextMenuUtils.assertContextMenuContainsItemEnabled;
 import static android.widget.espresso.ContextMenuUtils.assertContextMenuIsNotDisplayed;
+import static android.widget.espresso.DragHandleUtils.assertNoSelectionHandles;
 import static android.widget.espresso.DragHandleUtils.onHandleView;
 import static android.widget.espresso.TextViewActions.mouseClick;
 import static android.widget.espresso.TextViewActions.mouseClickOnTextAtIndex;
@@ -87,11 +88,6 @@
                 mouseDragOnText(helloWorld.indexOf("llo"), helloWorld.indexOf("ld!")));
         onView(withId(R.id.textview)).check(hasSelection("llo wor"));
 
-        onHandleView(com.android.internal.R.id.selection_start_handle)
-                .check(matches(isDisplayed()));
-        onHandleView(com.android.internal.R.id.selection_end_handle)
-                .check(matches(isDisplayed()));
-
         onView(withId(R.id.textview)).perform(mouseClickOnTextAtIndex(helloWorld.indexOf("w")));
         onView(withId(R.id.textview)).check(hasSelection(""));
     }
@@ -393,4 +389,29 @@
                 mouseTripleClickAndDragOnText(text.indexOf("ird"), text.indexOf("First")));
         onView(withId(R.id.textview)).check(hasSelection(text));
     }
+
+    @Test
+    public void testSelectionHandlesDisplay() {
+        final String helloWorld = "Hello world!";
+        onView(withId(R.id.textview)).perform(mouseClick());
+        onView(withId(R.id.textview)).perform(replaceText(helloWorld));
+
+        onView(withId(R.id.textview)).perform(
+                mouseDragOnText(helloWorld.indexOf("llo"), helloWorld.indexOf("ld!")));
+        onView(withId(R.id.textview)).check(hasSelection("llo wor"));
+
+        // Confirm that selection handles are shown when there is a selection.
+        onHandleView(com.android.internal.R.id.selection_start_handle)
+                .check(matches(isDisplayed()));
+        onHandleView(com.android.internal.R.id.selection_end_handle)
+                .check(matches(isDisplayed()));
+
+        onView(withId(R.id.textview)).perform(mouseClickOnTextAtIndex(helloWorld.indexOf("w")));
+        onView(withId(R.id.textview)).check(hasSelection(""));
+
+        // Confirm that the selection handles are not shown when there is no selection. This
+        // assertion is slow so we only do it in this one test case. The rest of the tests just
+        // assert via `hasSelection("")`.
+        assertNoSelectionHandles();
+    }
 }
diff --git a/graphics/java/android/graphics/FontFamily.java b/graphics/java/android/graphics/FontFamily.java
index ae90995..447f043 100644
--- a/graphics/java/android/graphics/FontFamily.java
+++ b/graphics/java/android/graphics/FontFamily.java
@@ -20,6 +20,7 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.res.AssetManager;
 import android.graphics.fonts.FontVariationAxis;
+import android.os.Build;
 import android.text.TextUtils;
 
 import dalvik.annotation.optimization.CriticalNative;
@@ -58,7 +59,8 @@
      *
      * This cannot be deleted because it's in use by AndroidX.
      */
-    @UnsupportedAppUsage(trackingBug = 123768928)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q,
+            publicAlternatives = "Use {@link android.graphics.fonts.FontFamily} instead.")
     public long mNativePtr;
 
     // Points native font family builder. Must be zero after freezing this family.
@@ -67,7 +69,8 @@
     /**
      * This cannot be deleted because it's in use by AndroidX.
      */
-    @UnsupportedAppUsage(trackingBug = 123768928)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q,
+            publicAlternatives = "Use {@link android.graphics.fonts.FontFamily} instead.")
     public FontFamily() {
         mBuilderPtr = nInitBuilder(null, 0);
         mNativeBuilderCleaner = sBuilderRegistry.registerNativeAllocation(this, mBuilderPtr);
@@ -76,7 +79,8 @@
     /**
      * This cannot be deleted because it's in use by AndroidX.
      */
-    @UnsupportedAppUsage(trackingBug = 123768928)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q,
+            publicAlternatives = "Use {@link android.graphics.fonts.FontFamily} instead.")
     public FontFamily(@Nullable String[] langs, int variant) {
         final String langsString;
         if (langs == null || langs.length == 0) {
@@ -98,7 +102,8 @@
      *
      * This cannot be deleted because it's in use by AndroidX.
      */
-    @UnsupportedAppUsage(trackingBug = 123768928)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q,
+            publicAlternatives = "Use {@link android.graphics.fonts.FontFamily} instead.")
     public boolean freeze() {
         if (mBuilderPtr == 0) {
             throw new IllegalStateException("This FontFamily is already frozen");
@@ -115,7 +120,8 @@
     /**
      * This cannot be deleted because it's in use by AndroidX.
      */
-    @UnsupportedAppUsage(trackingBug = 123768928)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q,
+            publicAlternatives = "Use {@link android.graphics.fonts.FontFamily} instead.")
     public void abortCreation() {
         if (mBuilderPtr == 0) {
             throw new IllegalStateException("This FontFamily is already frozen or abandoned");
@@ -127,7 +133,8 @@
     /**
      * This cannot be deleted because it's in use by AndroidX.
      */
-    @UnsupportedAppUsage(trackingBug = 123768928)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q,
+            publicAlternatives = "Use {@link android.graphics.fonts.FontFamily} instead.")
     public boolean addFont(String path, int ttcIndex, FontVariationAxis[] axes, int weight,
             int italic) {
         if (mBuilderPtr == 0) {
@@ -151,7 +158,8 @@
     /**
      * This cannot be deleted because it's in use by AndroidX.
      */
-    @UnsupportedAppUsage(trackingBug = 123768928)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q,
+            publicAlternatives = "Use {@link android.graphics.fonts.FontFamily} instead.")
     public boolean addFontFromBuffer(ByteBuffer font, int ttcIndex, FontVariationAxis[] axes,
             int weight, int italic) {
         if (mBuilderPtr == 0) {
@@ -179,7 +187,8 @@
      *
      * This cannot be deleted because it's in use by AndroidX.
      */
-    @UnsupportedAppUsage(trackingBug = 123768928)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q,
+            publicAlternatives = "Use {@link android.graphics.fonts.FontFamily} instead.")
     public boolean addFontFromAssetManager(AssetManager mgr, String path, int cookie,
             boolean isAsset, int ttcIndex, int weight, int isItalic,
             FontVariationAxis[] axes) {
diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp
index 12681ae..5790150 100644
--- a/libs/hwui/SkiaCanvas.cpp
+++ b/libs/hwui/SkiaCanvas.cpp
@@ -217,13 +217,16 @@
         canvas->setMatrix(mMatrix);
         switch (mType) {
             case Type::Rect:
-                canvas->clipRect(mRRect.rect(), mOp);
+                // Don't anti-alias rectangular clips
+                canvas->clipRect(mRRect.rect(), mOp, false);
                 break;
             case Type::RRect:
-                canvas->clipRRect(mRRect, mOp);
+                // Ensure rounded rectangular clips are anti-aliased
+                canvas->clipRRect(mRRect, mOp, true);
                 break;
             case Type::Path:
-                canvas->clipPath(mPath.value(), mOp);
+                // Ensure path clips are anti-aliased
+                canvas->clipPath(mPath.value(), mOp, true);
                 break;
         }
     }
@@ -392,7 +395,7 @@
 
 bool SkiaCanvas::clipPath(const SkPath* path, SkClipOp op) {
     this->recordClip(*path, op);
-    mCanvas->clipPath(*path, op);
+    mCanvas->clipPath(*path, op, true);
     return !mCanvas->isClipEmpty();
 }
 
diff --git a/media/java/android/media/tv/tuner/TunerConstants.java b/media/java/android/media/tv/tuner/TunerConstants.java
index 4532122..42229a0 100644
--- a/media/java/android/media/tv/tuner/TunerConstants.java
+++ b/media/java/android/media/tv/tuner/TunerConstants.java
@@ -124,6 +124,34 @@
      */
     public static final int FILTER_STATUS_OVERFLOW = Constants.DemuxFilterStatus.OVERFLOW;
 
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = "INDEX_TYPE_", value =
+            {INDEX_TYPE_NONE, INDEX_TYPE_TS, INDEX_TYPE_SC, INDEX_TYPE_SC_HEVC})
+    public @interface IndexType {}
+
+    /**
+     * Index is not used.
+     * @hide
+     */
+    public static final int INDEX_TYPE_NONE = Constants.DemuxRecordIndexType.NONE;
+    /**
+     * TS index.
+     * @hide
+     */
+    public static final int INDEX_TYPE_TS = Constants.DemuxRecordIndexType.TS;
+    /**
+     * Start Code index.
+     * @hide
+     */
+    public static final int INDEX_TYPE_SC = Constants.DemuxRecordIndexType.SC;
+    /**
+     * Start Code index for HEVC.
+     * @hide
+     */
+    public static final int INDEX_TYPE_SC_HEVC = Constants.DemuxRecordIndexType.SC_HEVC;
+
     /**
      * Indexes can be tagged through TS (Transport Stream) header.
      *
diff --git a/media/java/android/media/tv/tuner/filter/AlpFilterConfiguration.java b/media/java/android/media/tv/tuner/filter/AlpFilterConfiguration.java
index f0fe533..fcca6a1 100644
--- a/media/java/android/media/tv/tuner/filter/AlpFilterConfiguration.java
+++ b/media/java/android/media/tv/tuner/filter/AlpFilterConfiguration.java
@@ -16,20 +16,123 @@
 
 package android.media.tv.tuner.filter;
 
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.hardware.tv.tuner.V1_0.Constants;
+import android.media.tv.tuner.TunerUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * Filter configuration for a ALP filter.
  * @hide
  */
 public class AlpFilterConfiguration extends FilterConfiguration {
-    private int mPacketType;
-    private int mLengthType;
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = "LENGTH_TYPE_", value =
+            {LENGTH_TYPE_UNDEFINED, LENGTH_TYPE_WITHOUT_ADDITIONAL_HEADER,
+            LENGTH_TYPE_WITH_ADDITIONAL_HEADER})
+    public @interface LengthType {}
 
-    public AlpFilterConfiguration(Settings settings) {
+    /**
+     * Length type not defined.
+     */
+    public static final int LENGTH_TYPE_UNDEFINED = Constants.DemuxAlpLengthType.UNDEFINED;
+    /**
+     * Length does NOT include additional header.
+     */
+    public static final int LENGTH_TYPE_WITHOUT_ADDITIONAL_HEADER =
+            Constants.DemuxAlpLengthType.WITHOUT_ADDITIONAL_HEADER;
+    /**
+     * Length includes additional header.
+     */
+    public static final int LENGTH_TYPE_WITH_ADDITIONAL_HEADER =
+            Constants.DemuxAlpLengthType.WITH_ADDITIONAL_HEADER;
+
+
+    private final int mPacketType;
+    private final int mLengthType;
+
+    public AlpFilterConfiguration(Settings settings, int packetType, int lengthType) {
         super(settings);
+        mPacketType = packetType;
+        mLengthType = lengthType;
     }
 
     @Override
     public int getType() {
         return FilterConfiguration.FILTER_TYPE_ALP;
     }
+
+    /**
+     * Gets packet type.
+     */
+    @FilterConfiguration.PacketType
+    public int getPacketType() {
+        return mPacketType;
+    }
+    /**
+     * Gets length type.
+     */
+    @LengthType
+    public int getLengthType() {
+        return mLengthType;
+    }
+
+    /**
+     * Creates a builder for {@link AlpFilterConfiguration}.
+     *
+     * @param context the context of the caller.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_TV_TUNER)
+    @NonNull
+    public static Builder builder(@NonNull Context context) {
+        TunerUtils.checkTunerPermission(context);
+        return new Builder();
+    }
+
+    /**
+     * Builder for {@link AlpFilterConfiguration}.
+     */
+    public static class Builder extends FilterConfiguration.Builder<Builder> {
+        private int mPacketType;
+        private int mLengthType;
+
+        private Builder() {
+        }
+
+        /**
+         * Sets packet type.
+         */
+        @NonNull
+        public Builder setPacketType(@FilterConfiguration.PacketType int packetType) {
+            mPacketType = packetType;
+            return this;
+        }
+        /**
+         * Sets length type.
+         */
+        @NonNull
+        public Builder setLengthType(@LengthType int lengthType) {
+            mLengthType = lengthType;
+            return this;
+        }
+
+        /**
+         * Builds a {@link AlpFilterConfiguration} object.
+         */
+        @NonNull
+        public AlpFilterConfiguration build() {
+            return new AlpFilterConfiguration(mSettings, mPacketType, mLengthType);
+        }
+
+        @Override
+        Builder self() {
+            return this;
+        }
+    }
 }
diff --git a/media/java/android/media/tv/tuner/filter/AvSettings.java b/media/java/android/media/tv/tuner/filter/AvSettings.java
index a7c49d5..940b5ae 100644
--- a/media/java/android/media/tv/tuner/filter/AvSettings.java
+++ b/media/java/android/media/tv/tuner/filter/AvSettings.java
@@ -16,21 +16,84 @@
 
 package android.media.tv.tuner.filter;
 
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.content.Context;
 import android.media.tv.tuner.TunerConstants;
 import android.media.tv.tuner.TunerUtils;
+import android.media.tv.tuner.filter.FilterConfiguration.FilterType;
 
 /**
  * Filter Settings for a Video and Audio.
+ *
  * @hide
  */
 public class AvSettings extends Settings {
-    private boolean mIsPassthrough;
+    private final boolean mIsPassthrough;
 
-    private AvSettings(int mainType, boolean isAudio) {
+    private AvSettings(int mainType, boolean isAudio, boolean isPassthrough) {
         super(TunerUtils.getFilterSubtype(
                 mainType,
                 isAudio
                         ? TunerConstants.FILTER_SUBTYPE_AUDIO
                         : TunerConstants.FILTER_SUBTYPE_VIDEO));
+        mIsPassthrough = isPassthrough;
+    }
+
+    /**
+     * Checks whether it's passthrough.
+     */
+    public boolean isPassthrough() {
+        return mIsPassthrough;
+    }
+
+    /**
+     * Creates a builder for {@link AvSettings}.
+     *
+     * @param context the context of the caller.
+     * @param mainType the filter main type.
+     * @param isAudio {@code true} if it's audio settings; {@code false} if it's video settings.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_TV_TUNER)
+    @NonNull
+    public static Builder builder(
+            @NonNull Context context, @FilterType int mainType, boolean isAudio) {
+        TunerUtils.checkTunerPermission(context);
+        return new Builder(mainType, isAudio);
+    }
+
+    /**
+     * Builder for {@link AvSettings}.
+     */
+    public static class Builder extends Settings.Builder<Builder> {
+        private final boolean mIsAudio;
+        private boolean mIsPassthrough;
+
+        private Builder(int mainType, boolean isAudio) {
+            super(mainType);
+            mIsAudio = isAudio;
+        }
+
+        /**
+         * Sets whether it's passthrough.
+         */
+        @NonNull
+        public Builder setPassthrough(boolean isPassthrough) {
+            mIsPassthrough = isPassthrough;
+            return this;
+        }
+
+        /**
+         * Builds a {@link AvSettings} object.
+         */
+        @NonNull
+        public AvSettings build() {
+            return new AvSettings(mMainType, mIsAudio, mIsPassthrough);
+        }
+
+        @Override
+        Builder self() {
+            return this;
+        }
     }
 }
diff --git a/media/java/android/media/tv/tuner/filter/DownloadSettings.java b/media/java/android/media/tv/tuner/filter/DownloadSettings.java
index 0742b11..e3e1df0 100644
--- a/media/java/android/media/tv/tuner/filter/DownloadSettings.java
+++ b/media/java/android/media/tv/tuner/filter/DownloadSettings.java
@@ -16,17 +16,75 @@
 
 package android.media.tv.tuner.filter;
 
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.content.Context;
 import android.media.tv.tuner.TunerConstants;
 import android.media.tv.tuner.TunerUtils;
+import android.media.tv.tuner.filter.FilterConfiguration.FilterType;
 
 /**
  * Filter Settings for a Download.
  * @hide
  */
 public class DownloadSettings extends Settings {
-    private int mDownloadId;
+    private final int mDownloadId;
 
-    public DownloadSettings(int mainType) {
+    private DownloadSettings(int mainType, int downloadId) {
         super(TunerUtils.getFilterSubtype(mainType, TunerConstants.FILTER_SUBTYPE_DOWNLOAD));
+        mDownloadId = downloadId;
+    }
+
+    /**
+     * Gets download ID.
+     */
+    public int getDownloadId() {
+        return mDownloadId;
+    }
+
+    /**
+     * Creates a builder for {@link DownloadSettings}.
+     *
+     * @param context the context of the caller.
+     * @param mainType the filter main type.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_TV_TUNER)
+    @NonNull
+    public static Builder builder(@NonNull Context context, @FilterType int mainType) {
+        TunerUtils.checkTunerPermission(context);
+        return new Builder(mainType);
+    }
+
+    /**
+     * Builder for {@link DownloadSettings}.
+     */
+    public static class Builder extends Settings.Builder<Builder> {
+        private int mDownloadId;
+
+        private Builder(int mainType) {
+            super(mainType);
+        }
+
+        /**
+         * Sets download ID.
+         */
+        @NonNull
+        public Builder setDownloadId(int downloadId) {
+            mDownloadId = downloadId;
+            return this;
+        }
+
+        /**
+         * Builds a {@link DownloadSettings} object.
+         */
+        @NonNull
+        public DownloadSettings build() {
+            return new DownloadSettings(mMainType, mDownloadId);
+        }
+
+        @Override
+        Builder self() {
+            return this;
+        }
     }
 }
diff --git a/media/java/android/media/tv/tuner/filter/FilterConfiguration.java b/media/java/android/media/tv/tuner/filter/FilterConfiguration.java
index 6496627..68c722f 100644
--- a/media/java/android/media/tv/tuner/filter/FilterConfiguration.java
+++ b/media/java/android/media/tv/tuner/filter/FilterConfiguration.java
@@ -33,7 +33,8 @@
 public abstract class FilterConfiguration {
 
     /** @hide */
-    @IntDef({FILTER_TYPE_TS, FILTER_TYPE_MMTP, FILTER_TYPE_IP, FILTER_TYPE_TLV, FILTER_TYPE_ALP})
+    @IntDef(prefix = "FILTER_TYPE_", value =
+            {FILTER_TYPE_TS, FILTER_TYPE_MMTP, FILTER_TYPE_IP, FILTER_TYPE_TLV, FILTER_TYPE_ALP})
     @Retention(RetentionPolicy.SOURCE)
     public @interface FilterType {}
 
@@ -58,6 +59,30 @@
      */
     public static final int FILTER_TYPE_ALP = Constants.DemuxFilterMainType.ALP;
 
+
+    /** @hide */
+    @IntDef(prefix = "PACKET_TYPE_", value =
+            {PACKET_TYPE_IPV4, PACKET_TYPE_COMPRESSED, PACKET_TYPE_SIGNALING})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PacketType {}
+
+    /**
+     * IP v4 packet type.
+     * @hide
+     */
+    public static final int PACKET_TYPE_IPV4 = 0;
+    /**
+     * Compressed packet type.
+     * @hide
+     */
+    public static final int PACKET_TYPE_COMPRESSED = 2;
+    /**
+     * Signaling packet type.
+     * @hide
+     */
+    public static final int PACKET_TYPE_SIGNALING = 4;
+
+
     @Nullable
     /* package */ final Settings mSettings;
 
@@ -77,4 +102,27 @@
     public Settings getSettings() {
         return mSettings;
     }
+
+    /**
+     * Builder for {@link FilterConfiguration}.
+     *
+     * @param <T> The subclass to be built.
+     * @hide
+     */
+    public abstract static class Builder<T extends Builder<T>> {
+        /* package */ Settings mSettings;
+
+        /* package */ Builder() {
+        }
+
+        /**
+         * Sets filter settings.
+         */
+        @Nullable
+        public T setFrequency(Settings settings) {
+            mSettings = settings;
+            return self();
+        }
+        /* package */ abstract T self();
+    }
 }
diff --git a/media/java/android/media/tv/tuner/filter/IpFilterConfiguration.java b/media/java/android/media/tv/tuner/filter/IpFilterConfiguration.java
index c896368..98edf10 100644
--- a/media/java/android/media/tv/tuner/filter/IpFilterConfiguration.java
+++ b/media/java/android/media/tv/tuner/filter/IpFilterConfiguration.java
@@ -16,23 +16,152 @@
 
 package android.media.tv.tuner.filter;
 
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.Size;
+import android.content.Context;
+import android.media.tv.tuner.TunerUtils;
+
 /**
  * Filter configuration for a IP filter.
  * @hide
  */
 public class IpFilterConfiguration extends FilterConfiguration {
-    private byte[] mSrcIpAddress;
-    private byte[] mDstIpAddress;
-    private int mSrcPort;
-    private int mDstPort;
-    private boolean mPassthrough;
+    private final byte[] mSrcIpAddress;
+    private final byte[] mDstIpAddress;
+    private final int mSrcPort;
+    private final int mDstPort;
+    private final boolean mPassthrough;
 
-    public IpFilterConfiguration(Settings settings) {
+    public IpFilterConfiguration(Settings settings, byte[] srcAddr, byte[] dstAddr, int srcPort,
+            int dstPort, boolean passthrough) {
         super(settings);
+        mSrcIpAddress = srcAddr;
+        mDstIpAddress = dstAddr;
+        mSrcPort = srcPort;
+        mDstPort = dstPort;
+        mPassthrough = passthrough;
     }
 
     @Override
     public int getType() {
         return FilterConfiguration.FILTER_TYPE_IP;
     }
+
+    /**
+     * Gets source IP address.
+     */
+    @Size(min = 4, max = 16)
+    public byte[] getSrcIpAddress() {
+        return mSrcIpAddress;
+    }
+    /**
+     * Gets destination IP address.
+     */
+    @Size(min = 4, max = 16)
+    public byte[] getDstIpAddress() {
+        return mDstIpAddress;
+    }
+    /**
+     * Gets source port.
+     */
+    public int getSrcPort() {
+        return mSrcPort;
+    }
+    /**
+     * Gets destination port.
+     */
+    public int getDstPort() {
+        return mDstPort;
+    }
+    /**
+     * Checks whether the filter is passthrough.
+     *
+     * @return {@code true} if the data from IP subtype go to next filter directly;
+     *         {@code false} otherwise.
+     */
+    public boolean isPassthrough() {
+        return mPassthrough;
+    }
+
+    /**
+     * Creates a builder for {@link IpFilterConfiguration}.
+     *
+     * @param context the context of the caller.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_TV_TUNER)
+    @NonNull
+    public static Builder builder(@NonNull Context context) {
+        TunerUtils.checkTunerPermission(context);
+        return new Builder();
+    }
+
+    /**
+     * Builder for {@link IpFilterConfiguration}.
+     */
+    public static class Builder extends FilterConfiguration.Builder<Builder> {
+        private byte[] mSrcIpAddress;
+        private byte[] mDstIpAddress;
+        private int mSrcPort;
+        private int mDstPort;
+        private boolean mPassthrough;
+
+        private Builder() {
+        }
+
+        /**
+         * Sets source IP address.
+         */
+        @NonNull
+        public Builder setSrcIpAddress(byte[] srcIpAddress) {
+            mSrcIpAddress = srcIpAddress;
+            return this;
+        }
+        /**
+         * Sets destination IP address.
+         */
+        @NonNull
+        public Builder setDstIpAddress(byte[] dstIpAddress) {
+            mDstIpAddress = dstIpAddress;
+            return this;
+        }
+        /**
+         * Sets source port.
+         */
+        @NonNull
+        public Builder setSrcPort(int srcPort) {
+            mSrcPort = srcPort;
+            return this;
+        }
+        /**
+         * Sets destination port.
+         */
+        @NonNull
+        public Builder setDstPort(int dstPort) {
+            mDstPort = dstPort;
+            return this;
+        }
+        /**
+         * Sets passthrough.
+         */
+        @NonNull
+        public Builder setPassthrough(boolean passthrough) {
+            mPassthrough = passthrough;
+            return this;
+        }
+
+        /**
+         * Builds a {@link IpFilterConfiguration} object.
+         */
+        @NonNull
+        public IpFilterConfiguration build() {
+            return new IpFilterConfiguration(
+                    mSettings, mSrcIpAddress, mDstIpAddress, mSrcPort, mDstPort, mPassthrough);
+        }
+
+        @Override
+        Builder self() {
+            return this;
+        }
+    }
 }
diff --git a/media/java/android/media/tv/tuner/filter/MmtpFilterConfiguration.java b/media/java/android/media/tv/tuner/filter/MmtpFilterConfiguration.java
index 9045ce6..83246e5 100644
--- a/media/java/android/media/tv/tuner/filter/MmtpFilterConfiguration.java
+++ b/media/java/android/media/tv/tuner/filter/MmtpFilterConfiguration.java
@@ -16,19 +16,78 @@
 
 package android.media.tv.tuner.filter;
 
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.media.tv.tuner.TunerUtils;
+
 /**
  * Filter configuration for a MMTP filter.
  * @hide
  */
 public class MmtpFilterConfiguration extends FilterConfiguration {
-    private int mMmtpPid;
+    private final int mMmtpPid;
 
-    public MmtpFilterConfiguration(Settings settings) {
+    public MmtpFilterConfiguration(Settings settings, int mmtpPid) {
         super(settings);
+        mMmtpPid = mmtpPid;
     }
 
     @Override
     public int getType() {
         return FilterConfiguration.FILTER_TYPE_MMTP;
     }
+
+    /**
+     * Gets MMPT PID.
+     *
+     * <p>Packet ID is used to specify packets in MMTP.
+     */
+    public int getMmtpPid() {
+        return mMmtpPid;
+    }
+
+    /**
+     * Creates a builder for {@link IpFilterConfiguration}.
+     *
+     * @param context the context of the caller.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_TV_TUNER)
+    @NonNull
+    public static Builder builder(@NonNull Context context) {
+        TunerUtils.checkTunerPermission(context);
+        return new Builder();
+    }
+
+    /**
+     * Builder for {@link IpFilterConfiguration}.
+     */
+    public static class Builder extends FilterConfiguration.Builder<Builder> {
+        private int mMmtpPid;
+
+        private Builder() {
+        }
+
+        /**
+         * Sets MMPT PID.
+         */
+        @NonNull
+        public Builder setMmtpPid(int mmtpPid) {
+            mMmtpPid = mmtpPid;
+            return this;
+        }
+
+        /**
+         * Builds a {@link IpFilterConfiguration} object.
+         */
+        @NonNull
+        public MmtpFilterConfiguration build() {
+            return new MmtpFilterConfiguration(mSettings, mMmtpPid);
+        }
+
+        @Override
+        Builder self() {
+            return this;
+        }
+    }
 }
diff --git a/media/java/android/media/tv/tuner/filter/RecordSettings.java b/media/java/android/media/tv/tuner/filter/RecordSettings.java
index 701868a..2930f2d 100644
--- a/media/java/android/media/tv/tuner/filter/RecordSettings.java
+++ b/media/java/android/media/tv/tuner/filter/RecordSettings.java
@@ -16,18 +16,96 @@
 
 package android.media.tv.tuner.filter;
 
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.content.Context;
 import android.media.tv.tuner.TunerConstants;
+import android.media.tv.tuner.TunerConstants.IndexType;
 import android.media.tv.tuner.TunerUtils;
+import android.media.tv.tuner.filter.FilterConfiguration.FilterType;
 
 /**
  * The Settings for the record in DVR.
  * @hide
  */
 public class RecordSettings extends Settings {
-    private int mIndexType;
-    private int mIndexMask;
+    private final int mIndexType;
+    private final int mIndexMask;
 
-    public RecordSettings(int mainType) {
+    private RecordSettings(int mainType, int indexType, int indexMask) {
         super(TunerUtils.getFilterSubtype(mainType, TunerConstants.FILTER_SUBTYPE_RECORD));
+        mIndexType = indexType;
+        mIndexMask = indexMask;
     }
+
+    /**
+     * Gets index type.
+     */
+    @IndexType
+    public int getIndexType() {
+        return mIndexType;
+    }
+    /**
+     * Gets index mask.
+     */
+    @TsRecordEvent.IndexMask
+    public int getIndexMask() {
+        return mIndexMask;
+    }
+
+    /**
+     * Creates a builder for {@link RecordSettings}.
+     *
+     * @param context the context of the caller.
+     * @param mainType the filter main type.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_TV_TUNER)
+    @NonNull
+    public static Builder builder(@NonNull Context context, @FilterType int mainType) {
+        TunerUtils.checkTunerPermission(context);
+        return new Builder(mainType);
+    }
+
+    /**
+     * Builder for {@link RecordSettings}.
+     */
+    public static class Builder extends Settings.Builder<Builder> {
+        private int mIndexType;
+        private int mIndexMask;
+
+        private Builder(int mainType) {
+            super(mainType);
+        }
+
+        /**
+         * Sets index type.
+         */
+        @NonNull
+        public Builder setIndexType(@IndexType int indexType) {
+            mIndexType = indexType;
+            return this;
+        }
+        /**
+         * Sets index mask.
+         */
+        @NonNull
+        public Builder setIndexMask(@TsRecordEvent.IndexMask int indexMask) {
+            mIndexMask = indexMask;
+            return this;
+        }
+
+        /**
+         * Builds a {@link RecordSettings} object.
+         */
+        @NonNull
+        public RecordSettings build() {
+            return new RecordSettings(mMainType, mIndexType, mIndexMask);
+        }
+
+        @Override
+        Builder self() {
+            return this;
+        }
+    }
+
 }
diff --git a/media/java/android/media/tv/tuner/filter/SectionSettingsWithSectionBits.java b/media/java/android/media/tv/tuner/filter/SectionSettingsWithSectionBits.java
index 414ea67..0fa982e 100644
--- a/media/java/android/media/tv/tuner/filter/SectionSettingsWithSectionBits.java
+++ b/media/java/android/media/tv/tuner/filter/SectionSettingsWithSectionBits.java
@@ -16,18 +16,116 @@
 
 package android.media.tv.tuner.filter;
 
-import java.util.List;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.media.tv.tuner.TunerUtils;
+import android.media.tv.tuner.filter.FilterConfiguration.FilterType;
 
 /**
- * Bits Settings for Section Filter.
+ * Bits Settings for Section Filters.
  * @hide
  */
 public class SectionSettingsWithSectionBits extends SectionSettings {
-    private List<Byte> mFilter;
-    private List<Byte> mMask;
-    private List<Byte> mMode;
+    private final byte[] mFilter;
+    private final byte[] mMask;
+    private final byte[] mMode;
 
-    private SectionSettingsWithSectionBits(int mainType) {
+
+    private SectionSettingsWithSectionBits(int mainType, byte[] filter, byte[] mask, byte[] mode) {
         super(mainType);
+        mFilter = filter;
+        mMask = mask;
+        mMode = mode;
+    }
+
+    /**
+     * Gets the bytes configured for Section Filter
+     */
+    public byte[] getFilterBytes() {
+        return mFilter;
+    }
+    /**
+     * Gets bit mask.
+     *
+     * <p>The bits in the bytes are used for filtering.
+     */
+    public byte[] getMask() {
+        return mMask;
+    }
+    /**
+     * Gets mode.
+     *
+     * <p>Do positive match at the bit position of the configured bytes when the bit at same
+     * position of the mode is 0.
+     * <p>Do negative match at the bit position of the configured bytes when the bit at same
+     * position of the mode is 1.
+     */
+    public byte[] getMode() {
+        return mMode;
+    }
+
+    /**
+     * Creates a builder for {@link SectionSettingsWithSectionBits}.
+     *
+     * @param context the context of the caller.
+     * @param mainType the filter main type.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_TV_TUNER)
+    @NonNull
+    public static Builder builder(@NonNull Context context, @FilterType int mainType) {
+        TunerUtils.checkTunerPermission(context);
+        return new Builder(mainType);
+    }
+
+    /**
+     * Builder for {@link SectionSettingsWithSectionBits}.
+     */
+    public static class Builder extends Settings.Builder<Builder> {
+        private byte[] mFilter;
+        private byte[] mMask;
+        private byte[] mMode;
+
+        private Builder(int mainType) {
+            super(mainType);
+        }
+
+        /**
+         * Sets filter bytes.
+         */
+        @NonNull
+        public Builder setFilter(byte[] filter) {
+            mFilter = filter;
+            return this;
+        }
+        /**
+         * Sets bit mask.
+         */
+        @NonNull
+        public Builder setMask(byte[] mask) {
+            mMask = mask;
+            return this;
+        }
+        /**
+         * Sets mode.
+         */
+        @NonNull
+        public Builder setMode(byte[] mode) {
+            mMode = mode;
+            return this;
+        }
+
+        /**
+         * Builds a {@link SectionSettingsWithSectionBits} object.
+         */
+        @NonNull
+        public SectionSettingsWithSectionBits build() {
+            return new SectionSettingsWithSectionBits(mMainType, mFilter, mMask, mMode);
+        }
+
+        @Override
+        Builder self() {
+            return this;
+        }
     }
 }
diff --git a/media/java/android/media/tv/tuner/filter/SectionSettingsWithTableInfo.java b/media/java/android/media/tv/tuner/filter/SectionSettingsWithTableInfo.java
index 0df1d73..6542b89 100644
--- a/media/java/android/media/tv/tuner/filter/SectionSettingsWithTableInfo.java
+++ b/media/java/android/media/tv/tuner/filter/SectionSettingsWithTableInfo.java
@@ -16,15 +16,92 @@
 
 package android.media.tv.tuner.filter;
 
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.media.tv.tuner.TunerUtils;
+import android.media.tv.tuner.filter.FilterConfiguration.FilterType;
+
 /**
  * Table information for Section Filter.
  * @hide
  */
 public class SectionSettingsWithTableInfo extends SectionSettings {
-    private int mTableId;
-    private int mVersion;
+    private final int mTableId;
+    private final int mVersion;
 
-    private SectionSettingsWithTableInfo(int mainType) {
+    private SectionSettingsWithTableInfo(int mainType, int tableId, int version) {
         super(mainType);
+        mTableId = tableId;
+        mVersion = version;
     }
+
+    /**
+     * Gets table ID.
+     */
+    public int getTableId() {
+        return mTableId;
+    }
+    /**
+     * Gets version.
+     */
+    public int getVersion() {
+        return mVersion;
+    }
+
+    /**
+     * Creates a builder for {@link SectionSettingsWithTableInfo}.
+     *
+     * @param context the context of the caller.
+     * @param mainType the filter main type.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_TV_TUNER)
+    @NonNull
+    public static Builder builder(@NonNull Context context, @FilterType int mainType) {
+        TunerUtils.checkTunerPermission(context);
+        return new Builder(mainType);
+    }
+
+    /**
+     * Builder for {@link SectionSettingsWithTableInfo}.
+     */
+    public static class Builder extends Settings.Builder<Builder> {
+        private int mTableId;
+        private int mVersion;
+
+        private Builder(int mainType) {
+            super(mainType);
+        }
+
+        /**
+         * Sets table ID.
+         */
+        @NonNull
+        public Builder setTableId(int tableId) {
+            mTableId = tableId;
+            return this;
+        }
+        /**
+         * Sets version.
+         */
+        @NonNull
+        public Builder setVersion(int version) {
+            mVersion = version;
+            return this;
+        }
+
+        /**
+         * Builds a {@link SectionSettingsWithTableInfo} object.
+         */
+        @NonNull
+        public SectionSettingsWithTableInfo build() {
+            return new SectionSettingsWithTableInfo(mMainType, mTableId, mVersion);
+        }
+
+        @Override
+        Builder self() {
+            return this;
+        }
+    }
+
 }
diff --git a/media/java/android/media/tv/tuner/filter/Settings.java b/media/java/android/media/tv/tuner/filter/Settings.java
index 9155926..d697280 100644
--- a/media/java/android/media/tv/tuner/filter/Settings.java
+++ b/media/java/android/media/tv/tuner/filter/Settings.java
@@ -39,4 +39,20 @@
     public int getType() {
         return mType;
     }
+
+
+    /**
+     * Builder for {@link Settings}.
+     *
+     * @param <T> The subclass to be built.
+     * @hide
+     */
+    public abstract static class Builder<T extends Builder<T>> {
+        /* package */ final int mMainType;
+
+        /* package */ Builder(int mainType) {
+            mMainType = mainType;
+        }
+        /* package */ abstract T self();
+    }
 }
diff --git a/media/java/android/media/tv/tuner/filter/TlvFilterConfiguration.java b/media/java/android/media/tv/tuner/filter/TlvFilterConfiguration.java
index de8ee75..eb97fc0 100644
--- a/media/java/android/media/tv/tuner/filter/TlvFilterConfiguration.java
+++ b/media/java/android/media/tv/tuner/filter/TlvFilterConfiguration.java
@@ -16,21 +16,118 @@
 
 package android.media.tv.tuner.filter;
 
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.media.tv.tuner.TunerUtils;
+
 /**
  * Filter configuration for a TLV filter.
  * @hide
  */
 public class TlvFilterConfiguration extends FilterConfiguration {
-    private int mPacketType;
-    private boolean mIsCompressedIpPacket;
-    private boolean mPassthrough;
+    private final int mPacketType;
+    private final boolean mIsCompressedIpPacket;
+    private final boolean mPassthrough;
 
-    public TlvFilterConfiguration(Settings settings) {
+    public TlvFilterConfiguration(Settings settings, int packetType, boolean isCompressed,
+            boolean passthrough) {
         super(settings);
+        mPacketType = packetType;
+        mIsCompressedIpPacket = isCompressed;
+        mPassthrough = passthrough;
     }
 
     @Override
     public int getType() {
         return FilterConfiguration.FILTER_TYPE_TLV;
     }
+
+    /**
+     * Gets packet type.
+     */
+    @FilterConfiguration.PacketType
+    public int getPacketType() {
+        return mPacketType;
+    }
+    /**
+     * Checks whether the data is compressed IP packet.
+     *
+     * @return {@code true} if the filtered data is compressed IP packet; {@code false} otherwise.
+     */
+    public boolean isCompressedIpPacket() {
+        return mIsCompressedIpPacket;
+    }
+    /**
+     * Checks whether it's passthrough.
+     *
+     * @return {@code true} if the data from TLV subtype go to next filter directly;
+     *         {@code false} otherwise.
+     */
+    public boolean isPassthrough() {
+        return mPassthrough;
+    }
+
+    /**
+     * Creates a builder for {@link TlvFilterConfiguration}.
+     *
+     * @param context the context of the caller.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_TV_TUNER)
+    @NonNull
+    public static Builder builder(@NonNull Context context) {
+        TunerUtils.checkTunerPermission(context);
+        return new Builder();
+    }
+
+    /**
+     * Builder for {@link TlvFilterConfiguration}.
+     */
+    public static class Builder extends FilterConfiguration.Builder<Builder> {
+        private int mPacketType;
+        private boolean mIsCompressedIpPacket;
+        private boolean mPassthrough;
+
+        private Builder() {
+        }
+
+        /**
+         * Sets packet type.
+         */
+        @NonNull
+        public Builder setPacketType(@FilterConfiguration.PacketType int packetType) {
+            mPacketType = packetType;
+            return this;
+        }
+        /**
+         * Sets whether the data is compressed IP packet.
+         */
+        @NonNull
+        public Builder setIsCompressedIpPacket(boolean isCompressedIpPacket) {
+            mIsCompressedIpPacket = isCompressedIpPacket;
+            return this;
+        }
+        /**
+         * Sets whether it's passthrough.
+         */
+        @NonNull
+        public Builder setPassthrough(boolean passthrough) {
+            mPassthrough = passthrough;
+            return this;
+        }
+
+        /**
+         * Builds a {@link TlvFilterConfiguration} object.
+         */
+        @NonNull
+        public TlvFilterConfiguration build() {
+            return new TlvFilterConfiguration(
+                    mSettings, mPacketType, mIsCompressedIpPacket, mPassthrough);
+        }
+
+        @Override
+        Builder self() {
+            return this;
+        }
+    }
 }
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java
index 375a650..266bfe0 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java
@@ -2554,7 +2554,7 @@
             for (int phoneId = 0; phoneId < phoneCount; phoneId++) {
                 int mode = defaultNetworks.size() <= phoneId
                         || defaultNetworks.get(phoneId) == null
-                        ? RILConstants.PREFERRED_NETWORK_MODE : defaultNetworks.get(phoneId);
+                        ? TelephonyManager.DEFAULT_PREFERRED_NETWORK_MODE : defaultNetworks.get(phoneId);
                 if (phoneId > 0) val.append(',');
                 val.append(mode);
             }
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index df77b5b..bdb594f 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -308,7 +308,8 @@
         </receiver>
 
         <activity android:name=".screenrecord.ScreenRecordDialog"
-            android:theme="@style/ScreenRecord" />
+            android:theme="@style/ScreenRecord"
+            android:excludeFromRecents="true" />
         <service android:name=".screenrecord.RecordingService" />
 
         <receiver android:name=".SysuiRestartReceiver"
@@ -637,6 +638,23 @@
             </intent-filter>
         </activity>
 
+        <activity android:name=".controls.management.ControlsProviderSelectorActivity"
+                  android:label="Controls Providers"
+                  android:theme="@style/Theme.SystemUI"
+                  android:exported="true"
+                  android:excludeFromRecents="true"
+                  android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
+                  android:visibleToInstantApps="true">
+        </activity>
+
+        <activity android:name=".controls.management.ControlsFavoritingActivity"
+                  android:parentActivityName=".controls.management.ControlsProviderSelectorActivity"
+                  android:theme="@style/Theme.SystemUI"
+                  android:excludeFromRecents="true"
+                  android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
+                  android:visibleToInstantApps="true">
+        </activity>
+
         <!-- Doze with notifications, run in main sysui process for every user  -->
         <service
             android:name=".doze.DozeService"
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationSwipeActionHelper.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationSwipeActionHelper.java
index 8db0d02..02c4c5e 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationSwipeActionHelper.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationSwipeActionHelper.java
@@ -46,6 +46,8 @@
      */
     public void snooze(StatusBarNotification sbn, SnoozeOption snoozeOption);
 
+    public void snooze(StatusBarNotification sbn, int hours);
+
     public float getMinDismissVelocity();
 
     public boolean isDismissGesture(MotionEvent ev);
diff --git a/packages/SystemUI/res/drawable/ic_add_to_home.xml b/packages/SystemUI/res/drawable/ic_add_to_home.xml
new file mode 100644
index 0000000..2b40c62
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_add_to_home.xml
@@ -0,0 +1,26 @@
+<!--
+  Copyright (C) 2020 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0"
+        android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M18,1.01L8,1c-1.1,0 -2,0.9 -2,2v3h2V5h10v14H8v-1H6v3c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3c0,-1.1 -0.9,-1.99 -2,-1.99zM10,15h2V8H5v2h3.59L3,15.59 4.41,17 10,11.41V15z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_demote_conversation.xml b/packages/SystemUI/res/drawable/ic_demote_conversation.xml
new file mode 100644
index 0000000..5a88160
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_demote_conversation.xml
@@ -0,0 +1,25 @@
+<!--
+  Copyright (C) 2020 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M20,2L4.83,2l2,2L20,4v12h-1.17l1.87,1.87c0.75,-0.29 1.3,-1.02 1.3,-1.87L22,4c0,-1.1 -0.9,-2 -2,-2zM6,12h2v2L6,14zM18,11L18,9h-6.17l2,2zM18,6h-8v1.17l0.83,0.83L18,8zM0.69,3.51l1.32,1.32L2,22l4,-4h9.17l5.31,5.31 1.41,-1.41L2.1,2.1 0.69,3.51zM6,16h-0.83l-0.59,0.59 -0.58,0.58L4,6.83l2,2L6,11h2.17l5,5L6,16z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_qs_screenrecord.xml b/packages/SystemUI/res/drawable/ic_qs_screenrecord.xml
new file mode 100644
index 0000000..687c9c4
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_qs_screenrecord.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="M18,10.48L18,6c0,-1.1 -0.9,-2 -2,-2L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-4.48l4,3.98v-11l-4,3.98zM16,9.69L16,18L4,18L4,6h12v3.69z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_snooze.xml b/packages/SystemUI/res/drawable/ic_snooze.xml
index b0b03a9..f4c074d 100644
--- a/packages/SystemUI/res/drawable/ic_snooze.xml
+++ b/packages/SystemUI/res/drawable/ic_snooze.xml
@@ -1,12 +1,24 @@
+<!--
+  Copyright (C) 2017 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License
+  -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
         android:width="24dp"
         android:height="24dp"
         android:viewportWidth="24.0"
         android:viewportHeight="24.0">
     <path
-        android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"
-        android:fillColor="#757575"/>
-    <path
-        android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"
-        android:fillColor="#757575"/>
+        android:fillColor="@android:color/white"
+        android:pathData="M9,11h3.63L9,15.2L9,17h6v-2h-3.63L15,10.8L15,9L9,9v2zM16.056,3.346l1.282,-1.535 4.607,3.85 -1.28,1.54zM3.336,7.19l-1.28,-1.536L6.662,1.81l1.28,1.536zM12,6c3.86,0 7,3.14 7,7s-3.14,7 -7,7 -7,-3.14 -7,-7 3.14,-7 7,-7m0,-2c-4.97,0 -9,4.03 -9,9s4.03,9 9,9 9,-4.03 9,-9 -4.03,-9 -9,-9z"/>
 </vector>
diff --git a/packages/SystemUI/res/drawable/ic_star.xml b/packages/SystemUI/res/drawable/ic_star.xml
new file mode 100644
index 0000000..4a731b3
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_star.xml
@@ -0,0 +1,25 @@
+<!--
+  Copyright (C) 2020 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_star_border.xml b/packages/SystemUI/res/drawable/ic_star_border.xml
new file mode 100644
index 0000000..9ede40b
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_star_border.xml
@@ -0,0 +1,25 @@
+<!--
+  Copyright (C) 2020 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/app_item.xml b/packages/SystemUI/res/layout/app_item.xml
new file mode 100644
index 0000000..83e7887
--- /dev/null
+++ b/packages/SystemUI/res/layout/app_item.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2019 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.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?android:attr/selectableItemBackground"
+    android:gravity="center_vertical"
+    android:minHeight="?android:attr/listPreferredItemHeightSmall"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+    <LinearLayout
+        android:id="@+id/icon_frame"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="start|center_vertical"
+        android:minWidth="56dp"
+        android:orientation="horizontal"
+        android:paddingEnd="8dp"
+        android:paddingTop="4dp"
+        android:paddingBottom="4dp">
+        <ImageView
+            android:id="@android:id/icon"
+            android:layout_width="@dimen/app_icon_size"
+            android:layout_height="@dimen/app_icon_size"/>
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:orientation="vertical"
+        android:paddingTop="16dp"
+        android:paddingBottom="16dp">
+
+        <TextView
+            android:id="@android:id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:ellipsize="marquee"
+            android:fadingEdge="horizontal"
+            android:singleLine="true"
+            android:textAppearance="?android:attr/textAppearanceListItem"/>
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@android:id/widget_frame"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:gravity="center_vertical|end"
+        android:minWidth="64dp"
+        android:orientation="vertical"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/control_item.xml b/packages/SystemUI/res/layout/control_item.xml
new file mode 100644
index 0000000..85701aa
--- /dev/null
+++ b/packages/SystemUI/res/layout/control_item.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2020 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.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="100dp"
+    android:padding="15dp"
+    android:clickable="true"
+    android:focusable="true">
+
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <TextView
+        android:id="@+id/status"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="12sp"
+        android:textColor="?android:attr/textColorPrimary"
+        android:fontFamily="@*android:string/config_bodyFontFamily"
+        android:paddingLeft="3dp"
+        app:layout_constraintBottom_toBottomOf="@+id/icon"
+        app:layout_constraintStart_toEndOf="@+id/icon" />
+
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="18sp"
+        android:textColor="?android:attr/textColorPrimary"
+        android:fontFamily="@*android:string/config_headlineFontFamily"
+        app:layout_constraintBottom_toTopOf="@+id/subtitle"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/icon" />
+
+    <TextView
+        android:id="@+id/subtitle"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="16sp"
+        android:textColor="?android:attr/textColorSecondary"
+        android:fontFamily="@*android:string/config_headlineFontFamily"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent" />
+
+    <CheckBox
+        android:id="@+id/favorite"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"/>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/packages/SystemUI/res/layout/notification_conversation_info.xml b/packages/SystemUI/res/layout/notification_conversation_info.xml
new file mode 100644
index 0000000..8749b1a
--- /dev/null
+++ b/packages/SystemUI/res/layout/notification_conversation_info.xml
@@ -0,0 +1,254 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright 2020, 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.
+-->
+
+<com.android.systemui.statusbar.notification.row.NotificationConversationInfo
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/notification_guts"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:clickable="true"
+    android:clipChildren="false"
+    android:clipToPadding="true"
+    android:orientation="vertical"
+    android:background="@color/notification_material_background_color"
+    android:paddingStart="@*android:dimen/notification_content_margin_start">
+
+    <!-- Package Info -->
+    <RelativeLayout
+        android:id="@+id/header"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/notification_guts_conversation_header_height"
+        android:gravity="center_vertical"
+        android:clipChildren="false"
+        android:clipToPadding="false">
+        <ImageView
+            android:id="@+id/conversation_icon"
+            android:layout_width="@dimen/notification_guts_conversation_icon_size"
+            android:layout_height="@dimen/notification_guts_conversation_icon_size"
+            android:layout_centerVertical="true"
+            android:layout_alignParentStart="true"
+            android:layout_marginEnd="6dp" />
+        <LinearLayout
+            android:id="@+id/names"
+            android:orientation="vertical"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minHeight="@dimen/notification_guts_conversation_icon_size"
+            android:layout_centerVertical="true"
+            android:gravity="center_vertical"
+            android:layout_toEndOf="@id/conversation_icon">
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:gravity="start"
+                android:orientation="horizontal">
+                <TextView
+                    android:id="@+id/parent_channel_name"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    style="@style/TextAppearance.NotificationImportanceChannel"/>
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_centerVertical="true"
+                    style="@style/TextAppearance.NotificationImportanceHeader"
+                    android:layout_marginStart="2dp"
+                    android:layout_marginEnd="2dp"
+                    android:text="@*android:string/notification_header_divider_symbol" />
+                <TextView
+                    android:id="@+id/name"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    style="@style/TextAppearance.NotificationImportanceChannel"/>
+            </LinearLayout>
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:gravity="start"
+                android:orientation="horizontal">
+                <TextView
+                    android:id="@+id/pkg_name"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    style="@style/TextAppearance.NotificationImportanceChannelGroup"
+                    android:ellipsize="end"
+                    android:maxLines="1"/>
+                <TextView
+                    android:id="@+id/group_divider"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_centerVertical="true"
+                    style="@style/TextAppearance.NotificationImportanceHeader"
+                    android:layout_marginStart="2dp"
+                    android:layout_marginEnd="2dp"
+                    android:text="@*android:string/notification_header_divider_symbol" />
+                <TextView
+                    android:id="@+id/group_name"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    style="@style/TextAppearance.NotificationImportanceChannel"/>
+            </LinearLayout>
+
+        </LinearLayout>
+
+        <TextView
+            android:id="@+id/pkg_divider"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerVertical="true"
+            style="@style/TextAppearance.NotificationImportanceHeader"
+            android:layout_marginStart="2dp"
+            android:layout_marginEnd="2dp"
+            android:layout_toEndOf="@id/name"
+            android:text="@*android:string/notification_header_divider_symbol" />
+        <TextView
+            android:id="@+id/delegate_name"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerVertical="true"
+            style="@style/TextAppearance.NotificationImportanceHeader"
+            android:layout_marginStart="2dp"
+            android:layout_marginEnd="2dp"
+            android:ellipsize="end"
+            android:text="@string/notification_delegate_header"
+            android:layout_toEndOf="@id/pkg_divider"
+            android:maxLines="1" />
+
+        <!-- end aligned fields -->
+        <ImageButton
+            android:id="@+id/demote"
+            android:layout_width="@dimen/notification_importance_toggle_size"
+            android:layout_height="@dimen/notification_importance_toggle_size"
+            android:layout_centerVertical="true"
+            android:background="@drawable/ripple_drawable"
+            android:contentDescription="@string/demote"
+            android:src="@drawable/ic_demote_conversation"
+            android:layout_toStartOf="@id/app_settings"
+            android:tint="@color/notification_guts_link_icon_tint"/>
+        <!-- Optional link to app. Only appears if the channel is not disabled and the app
+asked for it -->
+        <ImageButton
+            android:id="@+id/app_settings"
+            android:layout_width="@dimen/notification_importance_toggle_size"
+            android:layout_height="@dimen/notification_importance_toggle_size"
+            android:layout_centerVertical="true"
+            android:visibility="gone"
+            android:background="@drawable/ripple_drawable"
+            android:contentDescription="@string/notification_app_settings"
+            android:src="@drawable/ic_info"
+            android:layout_toStartOf="@id/info"
+            android:tint="@color/notification_guts_link_icon_tint"/>
+        <ImageButton
+            android:id="@+id/info"
+            android:layout_width="@dimen/notification_importance_toggle_size"
+            android:layout_height="@dimen/notification_importance_toggle_size"
+            android:layout_centerVertical="true"
+            android:background="@drawable/ripple_drawable"
+            android:contentDescription="@string/notification_more_settings"
+            android:src="@drawable/ic_settings"
+            android:layout_alignParentEnd="true"
+            android:tint="@color/notification_guts_link_icon_tint"/>
+    </RelativeLayout>
+
+    <LinearLayout
+        android:id="@+id/actions"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@*android:dimen/notification_content_margin_end"
+        android:clipChildren="false"
+        android:clipToPadding="false"
+        android:orientation="vertical">
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="0.5dp"
+            android:background="@color/GM2_grey_300" />
+        <Button
+            android:id="@+id/bubble"
+            android:layout_height="@dimen/notification_guts_conversation_action_height"
+            android:layout_width="match_parent"
+            style="?android:attr/borderlessButtonStyle"
+            android:text="@string/notification_conversation_favorite"
+            android:gravity="left|center_vertical"
+            android:drawableStart="@drawable/ic_create_bubble"
+            android:drawablePadding="@dimen/notification_guts_conversation_action_text_padding_start"
+            android:drawableTint="@color/notification_guts_link_icon_tint"/>
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="0.5dp"
+            android:background="@color/GM2_grey_300" />
+        <Button
+            android:id="@+id/home"
+            android:layout_height="@dimen/notification_guts_conversation_action_height"
+            android:layout_width="match_parent"
+            style="?android:attr/borderlessButtonStyle"
+            android:text="@string/notification_conversation_home_screen"
+            android:gravity="left|center_vertical"
+            android:drawableStart="@drawable/ic_add_to_home"
+            android:drawablePadding="@dimen/notification_guts_conversation_action_text_padding_start"
+            android:drawableTint="@color/notification_guts_link_icon_tint"/>
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="0.5dp"
+            android:background="@color/GM2_grey_300" />
+        <Button
+            android:id="@+id/fave"
+            android:layout_height="@dimen/notification_guts_conversation_action_height"
+            android:layout_width="match_parent"
+            style="?android:attr/borderlessButtonStyle"
+            android:gravity="left|center_vertical"
+            android:drawablePadding="@dimen/notification_guts_conversation_action_text_padding_start"
+            android:drawableTint="@color/notification_guts_link_icon_tint"/>
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="0.5dp"
+            android:background="@color/GM2_grey_300" />
+        <Button
+            android:id="@+id/snooze"
+            android:layout_height="@dimen/notification_guts_conversation_action_height"
+            android:layout_width="match_parent"
+            style="?android:attr/borderlessButtonStyle"
+            android:text="@string/notification_menu_snooze_action"
+            android:gravity="left|center_vertical"
+            android:drawableStart="@drawable/ic_snooze"
+            android:drawablePadding="@dimen/notification_guts_conversation_action_text_padding_start"
+            android:drawableTint="@color/notification_guts_link_icon_tint"/>
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="0.5dp"
+            android:background="@color/GM2_grey_300" />
+        <Button
+            android:id="@+id/mute"
+            android:layout_height="@dimen/notification_guts_conversation_action_height"
+            android:layout_width="match_parent"
+            style="?android:attr/borderlessButtonStyle"
+            android:text="@string/notification_conversation_mute"
+            android:gravity="left|center_vertical"
+            android:drawableStart="@drawable/ic_notifications_silence"
+            android:drawablePadding="@dimen/notification_guts_conversation_action_text_padding_start"
+            android:drawableTint="@color/notification_guts_link_icon_tint"/>
+
+        </LinearLayout>
+
+</com.android.systemui.statusbar.notification.row.NotificationConversationInfo>
diff --git a/packages/SystemUI/res/layout/screen_record_dialog.xml b/packages/SystemUI/res/layout/screen_record_dialog.xml
deleted file mode 100644
index 3d63b7d..0000000
--- a/packages/SystemUI/res/layout/screen_record_dialog.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-              android:layout_width="match_parent"
-              android:layout_height="match_parent"
-              android:clipChildren="false"
-              android:clipToPadding="false"
-              android:gravity="top"
-              android:orientation="vertical"
-              android:padding="@dimen/global_actions_padding"
-              android:background="@drawable/rounded_bg_full">
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="vertical">
-        <CheckBox
-            android:id="@+id/checkbox_mic"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:textAppearance="?android:attr/textAppearanceMedium"
-            android:text="@string/screenrecord_mic_label"/>
-        <CheckBox
-            android:id="@+id/checkbox_taps"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:textAppearance="?android:attr/textAppearanceMedium"
-            android:text="@string/screenrecord_taps_label"/>
-        <Button
-            android:id="@+id/record_button"
-            android:layout_width="match_parent"
-            android:layout_height="50dp"
-            android:text="@string/screenrecord_start_label"
-        />
-    </LinearLayout>
-
-</LinearLayout>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 640f31b..8963157 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -109,7 +109,7 @@
 
     <!-- The default tiles to display in QuickSettings -->
     <string name="quick_settings_tiles_default" translatable="false">
-        wifi,bt,dnd,flashlight,rotation,battery,cell,airplane,cast
+        wifi,bt,dnd,flashlight,rotation,battery,cell,airplane,cast,screenrecord
     </string>
 
     <!-- The minimum number of tiles to display in QuickSettings -->
@@ -117,7 +117,7 @@
 
     <!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" -->
     <string name="quick_settings_tiles_stock" translatable="false">
-        wifi,cell,battery,dnd,flashlight,rotation,bt,airplane,location,hotspot,inversion,saver,dark,work,cast,night,controls
+        wifi,cell,battery,dnd,flashlight,rotation,bt,airplane,location,hotspot,inversion,saver,dark,work,cast,night,controls,screenrecord
     </string>
 
     <!-- The tiles to display in QuickSettings -->
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index c26cb9a..53df025 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -213,6 +213,11 @@
     <!-- The horizontal space around the buttons in the inline settings -->
     <dimen name="notification_guts_button_horizontal_spacing">8dp</dimen>
 
+    <dimen name="notification_guts_conversation_header_height">84dp</dimen>
+    <dimen name="notification_guts_conversation_icon_size">52dp</dimen>
+    <dimen name="notification_guts_conversation_action_height">56dp</dimen>
+    <dimen name="notification_guts_conversation_action_text_padding_start">32dp</dimen>
+
     <!-- The height of the header in inline settings -->
     <dimen name="notification_guts_header_height">24dp</dimen>
 
@@ -1160,4 +1165,5 @@
     <dimen name="magnifier_up_down_controls_width">45dp</dimen>
     <dimen name="magnifier_up_down_controls_height">40dp</dimen>
 
+    <dimen name="app_icon_size">32dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 4f532b7..9129938 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -928,6 +928,13 @@
     <!-- QuickSettings: NFC (on) [CHAR LIMIT=NONE] -->
     <string name="quick_settings_nfc_on">NFC is enabled</string>
 
+    <!-- QuickSettings: Screen record tile [CHAR LIMIT=NONE] -->
+    <string name="quick_settings_screen_record_label">Screen Record</string>
+    <!-- QuickSettings: Text to prompt the user to begin a new recording [CHAR LIMIT=20] -->
+    <string name="quick_settings_screen_record_start">Start</string>
+    <!-- QuickSettings: Text to prompt the user to stop an ongoing recording [CHAR LIMIT=20] -->
+    <string name="quick_settings_screen_record_stop">Stop</string>
+
     <!-- Recents: Text that shows above the navigation bar after launching a few apps. [CHAR LIMIT=NONE] -->
     <string name="recents_swipe_up_onboarding">Swipe up to switch apps</string>
     <!-- Recents: Text that shows above the navigation bar after launching several apps. [CHAR LIMIT=NONE] -->
@@ -1791,6 +1798,29 @@
     <string name="notification_done">Done</string>
     <!-- Notification: inline controls: undo block button -->
     <string name="inline_undo">Undo</string>
+    <!-- Notification: Conversation: control panel, label for button that demotes notification from conversation to normal notification -->
+    <string name="demote">Mark this notification as not a conversation</string>
+
+    <!-- [CHAR LIMIT=100] Mark this conversation as a favorite -->
+    <string name="notification_conversation_favorite">Favorite</string>
+
+    <!-- [CHAR LIMIT=100] Unmark this conversation as a favorite -->
+    <string name="notification_conversation_unfavorite">Unfavorite</string>
+
+    <!-- [CHAR LIMIT=100] Mute this conversation -->
+    <string name="notification_conversation_mute">Mute</string>
+
+    <!-- [CHAR LIMIT=100] Umute this conversation -->
+    <string name="notification_conversation_unmute">Unmute</string>
+
+    <!-- [CHAR LIMIT=100] Show notification as bubble -->
+    <string name="notification_conversation_bubble">Show as bubble</string>
+
+    <!-- [CHAR LIMIT=100] Turn off bubbles for notification -->
+    <string name="notification_conversation_unbubble">Turn off bubbles</string>
+
+    <!-- [CHAR LIMIT=100] Add this conversation to home screen -->
+    <string name="notification_conversation_home_screen">Add to home screen</string>
 
     <!-- Notification: Menu row: Content description for menu items. [CHAR LIMIT=NONE] -->
     <string name="notification_menu_accessibility"><xliff:g id="app_name" example="YouTube">%1$s</xliff:g> <xliff:g id="menu_description" example="notification controls">%2$s</xliff:g></string>
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index bbe972d..d149591 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -56,6 +56,7 @@
 import com.android.systemui.power.PowerUI;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.recents.Recents;
+import com.android.systemui.screenrecord.RecordingController;
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.DevicePolicyManagerWrapper;
@@ -323,6 +324,7 @@
     @Inject Lazy<DisplayWindowController> mDisplayWindowController;
     @Inject Lazy<SystemWindows> mSystemWindows;
     @Inject Lazy<DisplayImeController> mDisplayImeController;
+    @Inject Lazy<RecordingController> mRecordingController;
 
     @Inject
     public Dependency() {
@@ -519,6 +521,8 @@
         //                    Dependency problem.
         mProviders.put(AutoHideController.class, mAutoHideController::get);
 
+        mProviders.put(RecordingController.class, mRecordingController::get);
+
         sDependency = this;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlStatus.kt b/packages/SystemUI/src/com/android/systemui/controls/ControlStatus.kt
new file mode 100644
index 0000000..e6cdf50
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/ControlStatus.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2020 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.controls
+
+import android.service.controls.Control
+
+data class ControlStatus(val control: Control, val favorite: Boolean, val removed: Boolean = false)
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
new file mode 100644
index 0000000..265ddd8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2019 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.controls
+
+import android.content.Context
+import android.content.pm.ServiceInfo
+import com.android.settingslib.applications.DefaultAppInfo
+
+class ControlsServiceInfo(
+    context: Context,
+    serviceInfo: ServiceInfo
+) : DefaultAppInfo(
+    context,
+    context.packageManager,
+    context.userId,
+    serviceInfo.componentName
+)
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlInfo.kt
new file mode 100644
index 0000000..b6cca3f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlInfo.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2020 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.controls.controller
+
+import android.content.ComponentName
+import android.service.controls.DeviceTypes
+import android.util.Log
+
+/**
+ * Stores basic information about a [Control] to persist and keep track of favorites.
+ */
+data class ControlInfo(
+    val component: ComponentName,
+    val controlId: String,
+    val controlTitle: CharSequence,
+    @DeviceTypes.DeviceType val deviceType: Int
+) {
+
+    companion object {
+        private const val TAG = "ControlInfo"
+        private const val SEPARATOR = ":"
+        fun createFromString(string: String): ControlInfo? {
+            val parts = string.split(SEPARATOR)
+            val component = ComponentName.unflattenFromString(parts[0])
+            if (parts.size != 4 || component == null) {
+                Log.e(TAG, "Cannot parse ControlInfo from $string")
+                return null
+            }
+            val type = try {
+                parts[3].toInt()
+            } catch (e: Exception) {
+                Log.e(TAG, "Cannot parse deviceType from ${parts[3]}")
+                return null
+            }
+            return ControlInfo(
+                    component,
+                    parts[1],
+                    parts[2],
+                    if (DeviceTypes.validDeviceType(type)) type else DeviceTypes.TYPE_UNKNOWN)
+        }
+    }
+    override fun toString(): String {
+        return component.flattenToString() +
+                "$SEPARATOR$controlId$SEPARATOR$controlTitle$SEPARATOR$deviceType"
+    }
+
+    class Builder {
+        lateinit var componentName: ComponentName
+        lateinit var controlId: String
+        lateinit var controlTitle: CharSequence
+        var deviceType: Int = DeviceTypes.TYPE_UNKNOWN
+
+        fun build() = ControlInfo(componentName, controlId, controlTitle, deviceType)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt
new file mode 100644
index 0000000..6b7fc4b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2019 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.controls.controller
+
+import android.content.ComponentName
+import android.service.controls.Control
+import android.service.controls.actions.ControlAction
+
+interface ControlsBindingController {
+    fun bindAndLoad(component: ComponentName, callback: (List<Control>) -> Unit)
+    fun bindServices(components: List<ComponentName>)
+    fun subscribe(controls: List<ControlInfo>)
+    fun action(controlInfo: ControlInfo, action: ControlAction)
+    fun unsubscribe()
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
new file mode 100644
index 0000000..80e48b9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2019 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.controls.controller
+
+import android.content.ComponentName
+import android.content.Context
+import android.os.IBinder
+import android.service.controls.Control
+import android.service.controls.IControlsProviderCallback
+import android.service.controls.actions.ControlAction
+import android.util.ArrayMap
+import android.util.Log
+import com.android.internal.annotations.GuardedBy
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.concurrency.DelayableExecutor
+import dagger.Lazy
+import java.util.concurrent.atomic.AtomicBoolean
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+@VisibleForTesting
+open class ControlsBindingControllerImpl @Inject constructor(
+    private val context: Context,
+    @Background private val backgroundExecutor: DelayableExecutor,
+    private val lazyController: Lazy<ControlsController>
+) : ControlsBindingController {
+
+    companion object {
+        private const val TAG = "ControlsBindingControllerImpl"
+    }
+
+    private val refreshing = AtomicBoolean(false)
+
+    @GuardedBy("componentMap")
+    private val tokenMap: MutableMap<IBinder, ControlsProviderLifecycleManager> =
+            ArrayMap<IBinder, ControlsProviderLifecycleManager>()
+    @GuardedBy("componentMap")
+    private val componentMap: MutableMap<ComponentName, ControlsProviderLifecycleManager> =
+            ArrayMap<ComponentName, ControlsProviderLifecycleManager>()
+
+    private val serviceCallback = object : IControlsProviderCallback.Stub() {
+        override fun onLoad(token: IBinder, controls: MutableList<Control>) {
+            backgroundExecutor.execute(OnLoadRunnable(token, controls))
+        }
+
+        override fun onRefreshState(token: IBinder, controlStates: List<Control>) {
+            if (!refreshing.get()) {
+                Log.d(TAG, "Refresh outside of window for token:$token")
+            } else {
+                backgroundExecutor.execute(OnRefreshStateRunnable(token, controlStates))
+            }
+        }
+
+        override fun onControlActionResponse(
+            token: IBinder,
+            controlId: String,
+            @ControlAction.ResponseResult response: Int
+        ) {
+            backgroundExecutor.execute(OnActionResponseRunnable(token, controlId, response))
+        }
+    }
+
+    @VisibleForTesting
+    internal open fun createProviderManager(component: ComponentName):
+            ControlsProviderLifecycleManager {
+        return ControlsProviderLifecycleManager(
+                context,
+                backgroundExecutor,
+                serviceCallback,
+                component
+        )
+    }
+
+    private fun retrieveLifecycleManager(component: ComponentName):
+            ControlsProviderLifecycleManager {
+        synchronized(componentMap) {
+            val provider = componentMap.getOrPut(component) {
+                createProviderManager(component)
+            }
+            tokenMap.putIfAbsent(provider.token, provider)
+            return provider
+        }
+    }
+
+    override fun bindAndLoad(component: ComponentName, callback: (List<Control>) -> Unit) {
+        val provider = retrieveLifecycleManager(component)
+        provider.maybeBindAndLoad(callback)
+    }
+
+    override fun subscribe(controls: List<ControlInfo>) {
+        val controlsByComponentName = controls.groupBy { it.component }
+        if (refreshing.compareAndSet(false, true)) {
+            controlsByComponentName.forEach {
+                val provider = retrieveLifecycleManager(it.key)
+                backgroundExecutor.execute {
+                    provider.maybeBindAndSubscribe(it.value.map { it.controlId })
+                }
+            }
+        }
+        // Unbind unneeded providers
+        val providersWithFavorites = controlsByComponentName.keys
+        synchronized(componentMap) {
+            componentMap.forEach {
+                if (it.key !in providersWithFavorites) {
+                    backgroundExecutor.execute { it.value.unbindService() }
+                }
+            }
+        }
+    }
+
+    override fun unsubscribe() {
+        if (refreshing.compareAndSet(true, false)) {
+            val providers = synchronized(componentMap) {
+                componentMap.values.toList()
+            }
+            providers.forEach {
+                backgroundExecutor.execute { it.unsubscribe() }
+            }
+        }
+    }
+
+    override fun action(controlInfo: ControlInfo, action: ControlAction) {
+        val provider = retrieveLifecycleManager(controlInfo.component)
+        provider.maybeBindAndSendAction(controlInfo.controlId, action)
+    }
+
+    override fun bindServices(components: List<ComponentName>) {
+        components.forEach {
+            val provider = retrieveLifecycleManager(it)
+            backgroundExecutor.execute { provider.bindPermanently() }
+        }
+    }
+
+    private abstract inner class CallbackRunnable(val token: IBinder) : Runnable {
+        protected val provider: ControlsProviderLifecycleManager? =
+                synchronized(componentMap) {
+                    tokenMap.get(token)
+                }
+    }
+
+    private inner class OnLoadRunnable(
+        token: IBinder,
+        val list: List<Control>
+    ) : CallbackRunnable(token) {
+        override fun run() {
+            if (provider == null) {
+                Log.e(TAG, "No provider found for token:$token")
+                return
+            }
+            synchronized(componentMap) {
+                if (token !in tokenMap.keys) {
+                    Log.e(TAG, "Provider for token:$token does not exist anymore")
+                    return
+                }
+            }
+            provider.lastLoadCallback?.invoke(list) ?: run {
+                Log.w(TAG, "Null callback")
+            }
+            provider.maybeUnbindAndRemoveCallback()
+        }
+    }
+
+    private inner class OnRefreshStateRunnable(
+        token: IBinder,
+        val list: List<Control>
+    ) : CallbackRunnable(token) {
+        override fun run() {
+            if (!refreshing.get()) {
+                Log.d(TAG, "onRefresh outside of window from:${provider?.componentName}")
+            }
+            provider?.let {
+                lazyController.get().refreshStatus(it.componentName, list)
+            }
+        }
+    }
+
+    private inner class OnActionResponseRunnable(
+        token: IBinder,
+        val controlId: String,
+        @ControlAction.ResponseResult val response: Int
+    ) : CallbackRunnable(token) {
+        override fun run() {
+            provider?.let {
+                lazyController.get().onActionResponse(it.componentName, controlId, response)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
new file mode 100644
index 0000000..4d95822
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2020 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.controls.controller
+
+import android.content.ComponentName
+import android.service.controls.Control
+import android.service.controls.actions.ControlAction
+import com.android.systemui.controls.ControlStatus
+
+interface ControlsController {
+    val available: Boolean
+
+    fun getFavoriteControls(): List<ControlInfo>
+    fun loadForComponent(componentName: ComponentName, callback: (List<ControlStatus>) -> Unit)
+    fun subscribeToFavorites()
+    fun changeFavoriteStatus(controlInfo: ControlInfo, state: Boolean)
+    fun unsubscribe()
+    fun action(controlInfo: ControlInfo, action: ControlAction)
+    fun refreshStatus(componentName: ComponentName, controls: List<Control>)
+    fun onActionResponse(
+        componentName: ComponentName,
+        controlId: String,
+        @ControlAction.ResponseResult response: Int
+    )
+    fun clearFavorites()
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
new file mode 100644
index 0000000..7e328e4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2019 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.controls.controller
+
+import android.app.PendingIntent
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.os.Environment
+import android.provider.Settings
+import android.service.controls.Control
+import android.service.controls.actions.ControlAction
+import android.util.ArrayMap
+import android.util.Log
+import com.android.internal.annotations.GuardedBy
+import com.android.systemui.DumpController
+import com.android.systemui.Dumpable
+import com.android.systemui.controls.ControlStatus
+import com.android.systemui.controls.management.ControlsFavoritingActivity
+import com.android.systemui.controls.ui.ControlsUiController
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.concurrency.DelayableExecutor
+import java.io.FileDescriptor
+import java.io.PrintWriter
+import java.util.Optional
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class ControlsControllerImpl @Inject constructor (
+    private val context: Context,
+    @Background private val executor: DelayableExecutor,
+    private val uiController: ControlsUiController,
+    private val bindingController: ControlsBindingController,
+    private val optionalWrapper: Optional<ControlsFavoritePersistenceWrapper>,
+    dumpController: DumpController
+) : Dumpable, ControlsController {
+
+    companion object {
+        private const val TAG = "ControlsControllerImpl"
+        const val CONTROLS_AVAILABLE = "systemui.controls_available"
+    }
+
+    override val available = Settings.Secure.getInt(
+            context.contentResolver, CONTROLS_AVAILABLE, 0) != 0
+    val persistenceWrapper = optionalWrapper.orElseGet {
+        ControlsFavoritePersistenceWrapper(
+                Environment.buildPath(
+                        context.filesDir,
+                        ControlsFavoritePersistenceWrapper.FILE_NAME),
+                executor
+        )
+    }
+
+    // Map of map: ComponentName -> (String -> ControlInfo)
+    @GuardedBy("currentFavorites")
+    private val currentFavorites = ArrayMap<ComponentName, MutableMap<String, ControlInfo>>()
+
+    init {
+        if (available) {
+            dumpController.registerDumpable(this)
+            loadFavorites()
+        }
+    }
+
+    private fun loadFavorites() {
+        val infos = persistenceWrapper.readFavorites()
+        synchronized(currentFavorites) {
+            infos.forEach {
+                currentFavorites.getOrPut(it.component, { ArrayMap<String, ControlInfo>() })
+                        .put(it.controlId, it)
+            }
+        }
+    }
+
+    override fun loadForComponent(
+        componentName: ComponentName,
+        callback: (List<ControlStatus>) -> Unit
+    ) {
+        if (!available) {
+            Log.d(TAG, "Controls not available")
+            return
+        }
+        bindingController.bindAndLoad(componentName) {
+            synchronized(currentFavorites) {
+                val favoritesForComponentKeys: Set<String> =
+                        currentFavorites.get(componentName)?.keys ?: emptySet()
+                val changed = updateFavoritesLocked(componentName, it)
+                if (changed) {
+                    persistenceWrapper.storeFavorites(favoritesAsListLocked())
+                }
+                val removed = findRemovedLocked(favoritesForComponentKeys, it)
+                callback(removed.map { currentFavorites.getValue(componentName).getValue(it) }
+                            .map(::createRemovedStatus) +
+                        it.map { ControlStatus(it, it.controlId in favoritesForComponentKeys) })
+            }
+        }
+    }
+
+    private fun createRemovedStatus(controlInfo: ControlInfo): ControlStatus {
+        val intent = Intent(context, ControlsFavoritingActivity::class.java).apply {
+            putExtra(ControlsFavoritingActivity.EXTRA_COMPONENT, controlInfo.component)
+            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
+        }
+        val pendingIntent = PendingIntent.getActivity(context,
+                controlInfo.component.hashCode(),
+                intent,
+                0)
+        val control = Control.StatelessBuilder(controlInfo.controlId, pendingIntent)
+                .setTitle(controlInfo.controlTitle)
+                .setDeviceType(controlInfo.deviceType)
+                .build()
+        return ControlStatus(control, true, true)
+    }
+
+    @GuardedBy("currentFavorites")
+    private fun findRemovedLocked(favoriteKeys: Set<String>, list: List<Control>): Set<String> {
+        val controlsKeys = list.map { it.controlId }
+        return favoriteKeys.minus(controlsKeys)
+    }
+
+    @GuardedBy("currentFavorites")
+    private fun updateFavoritesLocked(componentName: ComponentName, list: List<Control>): Boolean {
+        val favorites = currentFavorites.get(componentName) ?: mutableMapOf()
+        val favoriteKeys = favorites.keys
+        if (favoriteKeys.isEmpty()) return false // early return
+        var changed = false
+        list.forEach {
+            if (it.controlId in favoriteKeys) {
+                val value = favorites.getValue(it.controlId)
+                if (value.controlTitle != it.title || value.deviceType != it.deviceType) {
+                    favorites[it.controlId] = value.copy(controlTitle = it.title,
+                            deviceType = it.deviceType)
+                    changed = true
+                }
+            }
+        }
+        return changed
+    }
+
+    @GuardedBy("currentFavorites")
+    private fun favoritesAsListLocked(): List<ControlInfo> {
+        return currentFavorites.flatMap { it.value.values }
+    }
+
+    override fun subscribeToFavorites() {
+        if (!available) {
+            Log.d(TAG, "Controls not available")
+            return
+        }
+        // Make a copy of the favorites list
+        val favorites = synchronized(currentFavorites) {
+            currentFavorites.flatMap { it.value.values.toList() }
+        }
+        bindingController.subscribe(favorites)
+    }
+
+    override fun unsubscribe() {
+        if (!available) {
+            Log.d(TAG, "Controls not available")
+            return
+        }
+        bindingController.unsubscribe()
+    }
+
+    override fun changeFavoriteStatus(controlInfo: ControlInfo, state: Boolean) {
+        if (!available) {
+            Log.d(TAG, "Controls not available")
+            return
+        }
+        var changed = false
+        val listOfControls = synchronized(currentFavorites) {
+            if (state) {
+                if (controlInfo.component !in currentFavorites) {
+                    currentFavorites.put(controlInfo.component, ArrayMap<String, ControlInfo>())
+                    changed = true
+                }
+                val controlsForComponent = currentFavorites.getValue(controlInfo.component)
+                if (controlInfo.controlId !in controlsForComponent) {
+                    controlsForComponent.put(controlInfo.controlId, controlInfo)
+                    changed = true
+                } else {
+                    if (controlsForComponent.getValue(controlInfo.controlId) != controlInfo) {
+                        controlsForComponent.put(controlInfo.controlId, controlInfo)
+                        changed = true
+                    }
+                }
+            } else {
+                changed = currentFavorites.get(controlInfo.component)
+                        ?.remove(controlInfo.controlId) != null
+            }
+            favoritesAsListLocked()
+        }
+        if (changed) {
+            persistenceWrapper.storeFavorites(listOfControls)
+        }
+    }
+
+    override fun refreshStatus(componentName: ComponentName, controls: List<Control>) {
+        if (!available) {
+            Log.d(TAG, "Controls not available")
+            return
+        }
+        executor.execute {
+            synchronized(currentFavorites) {
+                val changed = updateFavoritesLocked(componentName, controls)
+                if (changed) {
+                    persistenceWrapper.storeFavorites(favoritesAsListLocked())
+                }
+            }
+        }
+        uiController.onRefreshState(componentName, controls)
+    }
+
+    override fun onActionResponse(componentName: ComponentName, controlId: String, response: Int) {
+        if (!available) {
+            Log.d(TAG, "Controls not available")
+            return
+        }
+        uiController.onActionResponse(componentName, controlId, response)
+    }
+
+    override fun getFavoriteControls(): List<ControlInfo> {
+        if (!available) {
+            Log.d(TAG, "Controls not available")
+            return emptyList()
+        }
+        synchronized(currentFavorites) {
+            return favoritesAsListLocked()
+        }
+    }
+
+    override fun action(controlInfo: ControlInfo, action: ControlAction) {
+        bindingController.action(controlInfo, action)
+    }
+
+    override fun clearFavorites() {
+        val changed = synchronized(currentFavorites) {
+            currentFavorites.isNotEmpty().also {
+                currentFavorites.clear()
+            }
+        }
+        if (changed) {
+            persistenceWrapper.storeFavorites(emptyList())
+        }
+    }
+
+    override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
+        pw.println("ControlsController state:")
+        pw.println("  Favorites:")
+        synchronized(currentFavorites) {
+            currentFavorites.forEach {
+                it.value.forEach {
+                    pw.println("    ${it.value}")
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt
new file mode 100644
index 0000000..6f2d71f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2020 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.controls.controller
+
+import android.app.ActivityManager
+import android.content.ComponentName
+import android.util.AtomicFile
+import android.util.Log
+import android.util.Slog
+import android.util.Xml
+import com.android.systemui.util.concurrency.DelayableExecutor
+import libcore.io.IoUtils
+import org.xmlpull.v1.XmlPullParser
+import org.xmlpull.v1.XmlPullParserException
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileNotFoundException
+import java.io.IOException
+
+class ControlsFavoritePersistenceWrapper(
+    val file: File,
+    val executor: DelayableExecutor
+) {
+
+    companion object {
+        private const val TAG = "ControlsFavoritePersistenceWrapper"
+        const val FILE_NAME = "controls_favorites.xml"
+        private const val TAG_CONTROLS = "controls"
+        private const val TAG_CONTROL = "control"
+        private const val TAG_COMPONENT = "component"
+        private const val TAG_ID = "id"
+        private const val TAG_TITLE = "title"
+        private const val TAG_TYPE = "type"
+    }
+
+    val currentUser: Int
+        get() = ActivityManager.getCurrentUser()
+
+    fun storeFavorites(list: List<ControlInfo>) {
+        executor.execute {
+            val atomicFile = AtomicFile(file)
+            val writer = try {
+                atomicFile.startWrite()
+            } catch (e: IOException) {
+                Log.e(TAG, "Failed to start write file", e)
+                return@execute
+            }
+            try {
+                Xml.newSerializer().apply {
+                    setOutput(writer, "utf-8")
+                    setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true)
+                    startDocument(null, true)
+                    startTag(null, TAG_CONTROLS)
+                    list.forEach {
+                        startTag(null, TAG_CONTROL)
+                        attribute(null, TAG_COMPONENT, it.component.flattenToString())
+                        attribute(null, TAG_ID, it.controlId)
+                        attribute(null, TAG_TITLE, it.controlTitle.toString())
+                        attribute(null, TAG_TYPE, it.deviceType.toString())
+                        endTag(null, TAG_CONTROL)
+                    }
+                    endTag(null, TAG_CONTROLS)
+                    endDocument()
+                    atomicFile.finishWrite(writer)
+                }
+            } catch (t: Throwable) {
+                Log.e(TAG, "Failed to write file, reverting to previous version")
+                atomicFile.failWrite(writer)
+            } finally {
+                IoUtils.closeQuietly(writer)
+            }
+        }
+    }
+
+    fun readFavorites(): List<ControlInfo> {
+        if (!file.exists()) {
+            Log.d(TAG, "No favorites, returning empty list")
+            return emptyList()
+        }
+        val reader = try {
+            FileInputStream(file)
+        } catch (fnfe: FileNotFoundException) {
+            Slog.i(TAG, "No file found")
+            return emptyList()
+        }
+        try {
+            val parser = Xml.newPullParser()
+            parser.setInput(reader, null)
+            return parseXml(parser)
+        } catch (e: XmlPullParserException) {
+            throw IllegalStateException("Failed parsing favorites file: $file", e)
+        } catch (e: IOException) {
+            throw IllegalStateException("Failed parsing favorites file: $file", e)
+        } finally {
+            IoUtils.closeQuietly(reader)
+        }
+    }
+
+    private fun parseXml(parser: XmlPullParser): List<ControlInfo> {
+        var type: Int = 0
+        val infos = mutableListOf<ControlInfo>()
+        while (parser.next().also { type = it } != XmlPullParser.END_DOCUMENT) {
+            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                continue
+            }
+            val tagName = parser.name
+            if (tagName == TAG_CONTROL) {
+                val component = ComponentName.unflattenFromString(
+                        parser.getAttributeValue(null, TAG_COMPONENT))
+                val id = parser.getAttributeValue(null, TAG_ID)
+                val title = parser.getAttributeValue(null, TAG_TITLE)
+                val type = parser.getAttributeValue(null, TAG_TYPE)?.toInt()
+                if (component != null && id != null && title != null && type != null) {
+                    infos.add(ControlInfo(component, id, title, type))
+                }
+            }
+        }
+        return infos
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
new file mode 100644
index 0000000..79057ad
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2020 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.controls.controller
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.Binder
+import android.os.Bundle
+import android.os.IBinder
+import android.os.RemoteException
+import android.service.controls.Control
+import android.service.controls.ControlsProviderService.CALLBACK_BINDER
+import android.service.controls.ControlsProviderService.CALLBACK_BUNDLE
+import android.service.controls.ControlsProviderService.CALLBACK_TOKEN
+import android.service.controls.IControlsProvider
+import android.service.controls.IControlsProviderCallback
+import android.service.controls.actions.ControlAction
+import android.util.ArraySet
+import android.util.Log
+import com.android.internal.annotations.GuardedBy
+import com.android.systemui.util.concurrency.DelayableExecutor
+import java.util.concurrent.TimeUnit
+
+typealias LoadCallback = (List<Control>) -> Unit
+class ControlsProviderLifecycleManager(
+    private val context: Context,
+    private val executor: DelayableExecutor,
+    private val serviceCallback: IControlsProviderCallback.Stub,
+    val componentName: ComponentName
+) : IBinder.DeathRecipient {
+
+    var lastLoadCallback: LoadCallback? = null
+        private set
+    val token: IBinder = Binder()
+    private var unbindImmediate = false
+    private var requiresBound = false
+    private var isBound = false
+    @GuardedBy("queuedMessages")
+    private val queuedMessages: MutableSet<Message> = ArraySet()
+    private var wrapper: ControlsProviderServiceWrapper? = null
+    private var bindTryCount = 0
+    private val TAG = javaClass.simpleName
+    private var onLoadCanceller: Runnable? = null
+
+    companion object {
+        private const val MSG_LOAD = 0
+        private const val MSG_SUBSCRIBE = 1
+        private const val MSG_UNSUBSCRIBE = 2
+        private const val MSG_ON_ACTION = 3
+        private const val MSG_UNBIND = 4
+        private const val BIND_RETRY_DELAY = 1000L // ms
+        private const val LOAD_TIMEOUT = 5000L // ms
+        private const val MAX_BIND_RETRIES = 5
+        private const val DEBUG = true
+        private val BIND_FLAGS = Context.BIND_AUTO_CREATE or Context.BIND_FOREGROUND_SERVICE or
+                Context.BIND_WAIVE_PRIORITY
+    }
+
+    private val intent = Intent().apply {
+        component = componentName
+        putExtra(CALLBACK_BUNDLE, Bundle().apply {
+            putBinder(CALLBACK_BINDER, serviceCallback)
+            putBinder(CALLBACK_TOKEN, token)
+        })
+    }
+
+    private fun bindService(bind: Boolean) {
+        requiresBound = bind
+        if (bind) {
+            if (bindTryCount == MAX_BIND_RETRIES) {
+                return
+            }
+            if (DEBUG) {
+                Log.d(TAG, "Binding service $intent")
+            }
+            bindTryCount++
+            try {
+                isBound = context.bindService(intent, serviceConnection, BIND_FLAGS)
+            } catch (e: SecurityException) {
+                Log.e(TAG, "Failed to bind to service", e)
+                isBound = false
+            }
+        } else {
+            if (DEBUG) {
+                Log.d(TAG, "Unbinding service $intent")
+            }
+            bindTryCount = 0
+            wrapper = null
+            if (isBound) {
+                context.unbindService(serviceConnection)
+                isBound = false
+            }
+        }
+    }
+
+    fun bindPermanently() {
+        unbindImmediate = false
+        unqueueMessage(Message.Unbind)
+        bindService(true)
+    }
+
+    private val serviceConnection = object : ServiceConnection {
+        override fun onServiceConnected(name: ComponentName, service: IBinder) {
+            if (DEBUG) Log.d(TAG, "onServiceConnected $name")
+            bindTryCount = 0
+            wrapper = ControlsProviderServiceWrapper(IControlsProvider.Stub.asInterface(service))
+            try {
+                service.linkToDeath(this@ControlsProviderLifecycleManager, 0)
+            } catch (_: RemoteException) {}
+            handlePendingMessages()
+        }
+
+        override fun onServiceDisconnected(name: ComponentName?) {
+            if (DEBUG) Log.d(TAG, "onServiceDisconnected $name")
+            isBound = false
+            bindService(false)
+        }
+    }
+
+    private fun handlePendingMessages() {
+        val queue = synchronized(queuedMessages) {
+            ArraySet(queuedMessages).also {
+                queuedMessages.clear()
+            }
+        }
+        if (Message.Unbind in queue) {
+            bindService(false)
+            return
+        }
+        if (Message.Load in queue) {
+            load()
+        }
+        queue.filter { it is Message.Subscribe }.flatMap { (it as Message.Subscribe).list }.run {
+            subscribe(this)
+        }
+        queue.filter { it is Message.Action }.forEach {
+            val msg = it as Message.Action
+            onAction(msg.id, msg.action)
+        }
+    }
+
+    override fun binderDied() {
+        if (wrapper == null) return
+        wrapper = null
+        if (requiresBound) {
+            if (DEBUG) {
+                Log.d(TAG, "binderDied")
+            }
+            // Try rebinding some time later
+        }
+    }
+
+    private fun queueMessage(message: Message) {
+        synchronized(queuedMessages) {
+            queuedMessages.add(message)
+        }
+    }
+
+    private fun unqueueMessage(message: Message) {
+        synchronized(queuedMessages) {
+            queuedMessages.removeIf { it.type == message.type }
+        }
+    }
+
+    private fun load() {
+        if (DEBUG) {
+            Log.d(TAG, "load $componentName")
+        }
+        if (!(wrapper?.load() ?: false)) {
+            queueMessage(Message.Load)
+            binderDied()
+        }
+    }
+
+    fun maybeBindAndLoad(callback: LoadCallback) {
+        unqueueMessage(Message.Unbind)
+        lastLoadCallback = callback
+        onLoadCanceller = executor.executeDelayed({
+            // Didn't receive a response in time, log and send back empty list
+            Log.d(TAG, "Timeout waiting onLoad for $componentName")
+            serviceCallback.onLoad(token, emptyList())
+        }, LOAD_TIMEOUT, TimeUnit.MILLISECONDS)
+        if (isBound) {
+            load()
+        } else {
+            queueMessage(Message.Load)
+            unbindImmediate = true
+            bindService(true)
+        }
+    }
+
+    fun maybeBindAndSubscribe(controlIds: List<String>) {
+        if (isBound) {
+            subscribe(controlIds)
+        } else {
+            queueMessage(Message.Subscribe(controlIds))
+            bindService(true)
+        }
+    }
+
+    private fun subscribe(controlIds: List<String>) {
+        if (DEBUG) {
+            Log.d(TAG, "subscribe $componentName - $controlIds")
+        }
+        if (!(wrapper?.subscribe(controlIds) ?: false)) {
+            queueMessage(Message.Subscribe(controlIds))
+            binderDied()
+        }
+    }
+
+    fun maybeBindAndSendAction(controlId: String, action: ControlAction) {
+        if (isBound) {
+            onAction(controlId, action)
+        } else {
+            queueMessage(Message.Action(controlId, action))
+            bindService(true)
+        }
+    }
+
+    private fun onAction(controlId: String, action: ControlAction) {
+        if (DEBUG) {
+            Log.d(TAG, "onAction $componentName - $controlId")
+        }
+        if (!(wrapper?.onAction(controlId, action) ?: false)) {
+            queueMessage(Message.Action(controlId, action))
+            binderDied()
+        }
+    }
+
+    fun unsubscribe() {
+        if (DEBUG) {
+            Log.d(TAG, "unsubscribe $componentName")
+        }
+        unqueueMessage(Message.Subscribe(emptyList())) // Removes all subscribe messages
+        wrapper?.unsubscribe()
+    }
+
+    fun maybeUnbindAndRemoveCallback() {
+        lastLoadCallback = null
+        onLoadCanceller?.run()
+        onLoadCanceller = null
+        if (unbindImmediate) {
+            bindService(false)
+        }
+    }
+
+    fun unbindService() {
+        unbindImmediate = true
+        maybeUnbindAndRemoveCallback()
+    }
+
+    sealed class Message {
+        abstract val type: Int
+        object Load : Message() {
+            override val type = MSG_LOAD
+        }
+        object Unbind : Message() {
+            override val type = MSG_UNBIND
+        }
+        class Subscribe(val list: List<String>) : Message() {
+            override val type = MSG_SUBSCRIBE
+        }
+        class Action(val id: String, val action: ControlAction) : Message() {
+            override val type = MSG_ON_ACTION
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderServiceWrapper.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderServiceWrapper.kt
new file mode 100644
index 0000000..882a10d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderServiceWrapper.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2019 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.controls.controller
+
+import android.service.controls.actions.ControlAction
+import android.service.controls.IControlsProvider
+import android.util.Log
+
+class ControlsProviderServiceWrapper(val service: IControlsProvider) {
+    companion object {
+        private const val TAG = "ControlsProviderServiceWrapper"
+    }
+
+    private fun callThroughService(block: () -> Unit): Boolean {
+        try {
+            block()
+            return true
+        } catch (ex: Exception) {
+            Log.d(TAG, "Caught exception from ControlsProviderService", ex)
+            return false
+        }
+    }
+
+    fun load(): Boolean {
+        return callThroughService {
+            service.load()
+        }
+    }
+
+    fun subscribe(controlIds: List<String>): Boolean {
+        return callThroughService {
+            service.subscribe(controlIds)
+        }
+    }
+
+    fun unsubscribe(): Boolean {
+        return callThroughService {
+            service.unsubscribe()
+        }
+    }
+
+    fun onAction(controlId: String, action: ControlAction): Boolean {
+        return callThroughService {
+            service.onAction(controlId, action)
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
new file mode 100644
index 0000000..859311e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2019 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.controls.dagger
+
+import android.app.Activity
+import com.android.systemui.controls.controller.ControlsBindingController
+import com.android.systemui.controls.controller.ControlsBindingControllerImpl
+import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.controls.controller.ControlsControllerImpl
+import com.android.systemui.controls.controller.ControlsFavoritePersistenceWrapper
+import com.android.systemui.controls.management.ControlsFavoritingActivity
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.management.ControlsListingControllerImpl
+import com.android.systemui.controls.management.ControlsProviderSelectorActivity
+import com.android.systemui.controls.ui.ControlsUiController
+import com.android.systemui.controls.ui.ControlsUiControllerImpl
+import dagger.Binds
+import dagger.BindsOptionalOf
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+@Module
+abstract class ControlsModule {
+
+    @Binds
+    abstract fun provideControlsListingController(
+        controller: ControlsListingControllerImpl
+    ): ControlsListingController
+
+    @Binds
+    abstract fun provideControlsController(controller: ControlsControllerImpl): ControlsController
+
+    @Binds
+    abstract fun provideControlsBindingController(
+        controller: ControlsBindingControllerImpl
+    ): ControlsBindingController
+
+    @Binds
+    abstract fun provideUiController(controller: ControlsUiControllerImpl): ControlsUiController
+
+    @BindsOptionalOf
+    abstract fun optionalPersistenceWrapper(): ControlsFavoritePersistenceWrapper
+
+    @Binds
+    @IntoMap
+    @ClassKey(ControlsProviderSelectorActivity::class)
+    abstract fun provideControlsProviderActivity(
+        activity: ControlsProviderSelectorActivity
+    ): Activity
+
+    @Binds
+    @IntoMap
+    @ClassKey(ControlsFavoritingActivity::class)
+    abstract fun provideControlsFavoritingActivity(
+        activity: ControlsFavoritingActivity
+    ): Activity
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt
new file mode 100644
index 0000000..d62bb4d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2019 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.controls.management
+
+import android.content.ComponentName
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.lifecycle.Lifecycle
+import androidx.recyclerview.widget.RecyclerView
+import com.android.settingslib.widget.CandidateInfo
+import com.android.systemui.R
+import java.util.concurrent.Executor
+
+/**
+ * Adapter for binding [CandidateInfo] related to [ControlsProviderService].
+ *
+ * This class handles subscribing and keeping track of the list of valid applications for
+ * displaying.
+ *
+ * @param uiExecutor an executor on the view thread of the containing [RecyclerView]
+ * @param lifecycle the lifecycle of the containing [LifecycleOwner] to control listening status
+ * @param controlsListingController the controller to keep track of valid applications
+ * @param layoutInflater an inflater for the views in the containing [RecyclerView]
+ * @param onAppSelected a callback to indicate that an app has been selected in the list.
+ */
+class AppAdapter(
+    uiExecutor: Executor,
+    lifecycle: Lifecycle,
+    controlsListingController: ControlsListingController,
+    private val layoutInflater: LayoutInflater,
+    private val onAppSelected: (ComponentName?) -> Unit = {}
+) : RecyclerView.Adapter<AppAdapter.Holder>() {
+
+    private var listOfServices = emptyList<CandidateInfo>()
+
+    private val callback = object : ControlsListingController.ControlsListingCallback {
+        override fun onServicesUpdated(list: List<CandidateInfo>) {
+            uiExecutor.execute {
+                listOfServices = list
+                notifyDataSetChanged()
+            }
+        }
+    }
+
+    init {
+        controlsListingController.observe(lifecycle, callback)
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, i: Int): Holder {
+        return Holder(layoutInflater.inflate(R.layout.app_item, parent, false))
+    }
+
+    override fun getItemCount() = listOfServices.size
+
+    override fun onBindViewHolder(holder: Holder, index: Int) {
+        holder.bindData(listOfServices[index])
+        holder.itemView.setOnClickListener {
+            onAppSelected(ComponentName.unflattenFromString(listOfServices[index].key))
+        }
+    }
+
+    /**
+     * Holder for binding views in the [RecyclerView]-
+     */
+    class Holder(view: View) : RecyclerView.ViewHolder(view) {
+        private val icon: ImageView = itemView.requireViewById(com.android.internal.R.id.icon)
+        private val title: TextView = itemView.requireViewById(com.android.internal.R.id.title)
+
+        /**
+         * Bind data to the view
+         * @param data Information about the [ControlsProviderService] to bind to the data
+         */
+        fun bindData(data: CandidateInfo) {
+            icon.setImageDrawable(data.loadIcon())
+            title.text = data.loadLabel()
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
new file mode 100644
index 0000000..e6d3c26
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2020 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.controls.management
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.CheckBox
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.android.systemui.R
+import com.android.systemui.controls.ControlStatus
+import com.android.systemui.controls.controller.ControlInfo
+
+/**
+ * Adapter for binding [Control] information to views.
+ *
+ * @param layoutInflater an inflater for the views in the containing [RecyclerView]
+ * @param favoriteCallback a callback to be called when the favorite status of a [Control] is
+ *                         changed. The callback will take a [ControlInfo.Builder] that's
+ *                         pre-populated with the [Control] information and the new favorite
+ *                         status.
+ */
+class ControlAdapter(
+    private val layoutInflater: LayoutInflater,
+    private val favoriteCallback: (ControlInfo.Builder, Boolean) -> Unit
+) : RecyclerView.Adapter<ControlAdapter.Holder>() {
+
+    var listOfControls = emptyList<ControlStatus>()
+
+    override fun onCreateViewHolder(parent: ViewGroup, i: Int): Holder {
+        return Holder(layoutInflater.inflate(R.layout.control_item, parent, false))
+    }
+
+    override fun getItemCount() = listOfControls.size
+
+    override fun onBindViewHolder(holder: Holder, index: Int) {
+        holder.bindData(listOfControls[index], favoriteCallback)
+    }
+
+    /**
+     * Holder for binding views in the [RecyclerView]-
+     */
+    class Holder(view: View) : RecyclerView.ViewHolder(view) {
+        private val title: TextView = itemView.requireViewById(R.id.title)
+        private val subtitle: TextView = itemView.requireViewById(R.id.subtitle)
+        private val favorite: CheckBox = itemView.requireViewById(R.id.favorite)
+
+        /**
+         * Bind data to the view
+         * @param data information about the [Control]
+         * @param callback a callback to be called when the favorite status of the [Control] is
+         *                 changed. The callback will take a [ControlInfo.Builder] that's
+         *                 pre-populated with the [Control] information and the new favorite status.
+         */
+        fun bindData(data: ControlStatus, callback: (ControlInfo.Builder, Boolean) -> Unit) {
+            title.text = data.control.title
+            subtitle.text = data.control.subtitle
+            favorite.isChecked = data.favorite
+            favorite.setOnClickListener {
+                val infoBuilder = ControlInfo.Builder().apply {
+                    controlId = data.control.controlId
+                    controlTitle = data.control.title
+                    deviceType = data.control.deviceType
+                }
+                callback(infoBuilder, favorite.isChecked)
+            }
+        }
+    }
+
+    fun setItems(list: List<ControlStatus>) {
+        listOfControls = list
+        notifyDataSetChanged()
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
new file mode 100644
index 0000000..01c4fef
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2020 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.controls.management
+
+import android.app.Activity
+import android.content.ComponentName
+import android.os.Bundle
+import android.view.LayoutInflater
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.android.systemui.controls.controller.ControlInfo
+import com.android.systemui.controls.controller.ControlsControllerImpl
+import com.android.systemui.dagger.qualifiers.Main
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+class ControlsFavoritingActivity @Inject constructor(
+    @Main private val executor: Executor,
+    private val controller: ControlsControllerImpl
+) : Activity() {
+
+    companion object {
+        private const val TAG = "ControlsFavoritingActivity"
+        const val EXTRA_APP = "extra_app_label"
+        const val EXTRA_COMPONENT = "extra_component"
+    }
+
+    private lateinit var recyclerView: RecyclerView
+    private lateinit var adapter: ControlAdapter
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        val app = intent.getCharSequenceExtra(EXTRA_APP)
+        val component = intent.getParcelableExtra<ComponentName>(EXTRA_COMPONENT)
+
+        // If we have no component name, there's not much we can do.
+        val callback = component?.let {
+            { infoBuilder: ControlInfo.Builder, status: Boolean ->
+                infoBuilder.componentName = it
+                controller.changeFavoriteStatus(infoBuilder.build(), status)
+            }
+        } ?: { _, _ -> Unit }
+
+        recyclerView = RecyclerView(applicationContext)
+        adapter = ControlAdapter(LayoutInflater.from(applicationContext), callback)
+        recyclerView.adapter = adapter
+        recyclerView.layoutManager = LinearLayoutManager(applicationContext)
+
+        if (app != null) {
+            setTitle("Controls for $app")
+        } else {
+            setTitle("Controls")
+        }
+        setContentView(recyclerView)
+
+        component?.let {
+            controller.loadForComponent(it) {
+                executor.execute {
+                    adapter.setItems(it)
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingController.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingController.kt
new file mode 100644
index 0000000..09e0ce9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingController.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2019 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.controls.management
+
+import android.content.ComponentName
+import com.android.settingslib.widget.CandidateInfo
+import com.android.systemui.statusbar.policy.CallbackController
+
+interface ControlsListingController :
+        CallbackController<ControlsListingController.ControlsListingCallback> {
+
+    fun getCurrentServices(): List<CandidateInfo>
+    fun getAppLabel(name: ComponentName): CharSequence? = ""
+
+    interface ControlsListingCallback {
+        fun onServicesUpdated(list: List<CandidateInfo>)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt
new file mode 100644
index 0000000..9372162
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2019 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.controls.management
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.ServiceInfo
+import android.service.controls.ControlsProviderService
+import android.util.Log
+import com.android.internal.annotations.VisibleForTesting
+import com.android.settingslib.applications.DefaultAppInfo
+import com.android.settingslib.applications.ServiceListing
+import com.android.settingslib.widget.CandidateInfo
+import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.dagger.qualifiers.Background
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Provides a listing of components to be used as ControlsServiceProvider.
+ *
+ * This controller keeps track of components that satisfy:
+ *
+ * * Has an intent-filter responding to [ControlsProviderService.CONTROLS_ACTION]
+ * * Has the bind permission `android.permission.BIND_CONTROLS`
+ */
+@Singleton
+class ControlsListingControllerImpl @VisibleForTesting constructor(
+    private val context: Context,
+    @Background private val backgroundExecutor: Executor,
+    private val serviceListing: ServiceListing
+) : ControlsListingController {
+
+    @Inject
+    constructor(context: Context, executor: Executor): this(
+            context,
+            executor,
+            ServiceListing.Builder(context)
+                    .setIntentAction(ControlsProviderService.CONTROLS_ACTION)
+                    .setPermission("android.permission.BIND_CONTROLS")
+                    .setNoun("Controls Provider")
+                    .setSetting("controls_providers")
+                    .setTag("controls_providers")
+                    .build()
+    )
+
+    companion object {
+        private const val TAG = "ControlsListingControllerImpl"
+    }
+
+    private var availableServices = emptyList<ServiceInfo>()
+
+    init {
+        serviceListing.addCallback {
+            Log.d(TAG, "ServiceConfig reloaded")
+            availableServices = it.toList()
+
+            backgroundExecutor.execute {
+                callbacks.forEach {
+                    it.onServicesUpdated(getCurrentServices())
+                }
+            }
+        }
+    }
+
+    // All operations in background thread
+    private val callbacks = mutableSetOf<ControlsListingController.ControlsListingCallback>()
+
+    /**
+     * Adds a callback to this controller.
+     *
+     * The callback will be notified after it is added as well as any time that the valid
+     * components change.
+     *
+     * @param listener a callback to be notified
+     */
+    override fun addCallback(listener: ControlsListingController.ControlsListingCallback) {
+        backgroundExecutor.execute {
+            callbacks.add(listener)
+            if (callbacks.size == 1) {
+                serviceListing.setListening(true)
+                serviceListing.reload()
+            } else {
+                listener.onServicesUpdated(getCurrentServices())
+            }
+        }
+    }
+
+    /**
+     * Removes a callback from this controller.
+     *
+     * @param listener the callback to be removed.
+     */
+    override fun removeCallback(listener: ControlsListingController.ControlsListingCallback) {
+        backgroundExecutor.execute {
+            callbacks.remove(listener)
+            if (callbacks.size == 0) {
+                serviceListing.setListening(false)
+            }
+        }
+    }
+
+    /**
+     * @return a list of components that satisfy the requirements to be a
+     *         [ControlsProviderService]
+     */
+    override fun getCurrentServices(): List<CandidateInfo> =
+            availableServices.map { ControlsServiceInfo(context, it) }
+
+    /**
+     * Get the localized label for the component.
+     *
+     * @param name the name of the component
+     * @return a label as returned by [CandidateInfo.loadLabel] or `null`.
+     */
+    override fun getAppLabel(name: ComponentName): CharSequence? {
+        return getCurrentServices().firstOrNull { (it as? DefaultAppInfo)?.componentName == name }
+                ?.loadLabel()
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt
new file mode 100644
index 0000000..69af516
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2019 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.controls.management
+
+import android.content.ComponentName
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.util.LifecycleActivity
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+/**
+ * Activity to select an application to favorite the [Control] provided by them.
+ */
+class ControlsProviderSelectorActivity @Inject constructor(
+    @Main private val executor: Executor,
+    private val listingController: ControlsListingController
+) : LifecycleActivity() {
+
+    companion object {
+        private const val TAG = "ControlsProviderSelectorActivity"
+    }
+
+    private lateinit var recyclerView: RecyclerView
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        recyclerView = RecyclerView(applicationContext)
+        recyclerView.adapter = AppAdapter(executor, lifecycle, listingController,
+                LayoutInflater.from(this), ::launchFavoritingActivity)
+        recyclerView.layoutManager = LinearLayoutManager(applicationContext)
+
+        setContentView(recyclerView)
+    }
+
+    /**
+     * Launch the [ControlsFavoritingActivity] for the specified component.
+     * @param component a component name for a [ControlsProviderService]
+     */
+    fun launchFavoritingActivity(component: ComponentName?) {
+        component?.let {
+            val intent = Intent(applicationContext, ControlsFavoritingActivity::class.java).apply {
+                putExtra(ControlsFavoritingActivity.EXTRA_APP, listingController.getAppLabel(it))
+                putExtra(ControlsFavoritingActivity.EXTRA_COMPONENT, it)
+                flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
+            }
+            startActivity(intent)
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
new file mode 100644
index 0000000..0270c2b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2020 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.controls.ui
+
+import android.content.ComponentName
+import android.service.controls.Control
+import android.service.controls.actions.ControlAction
+
+interface ControlsUiController {
+    fun onRefreshState(componentName: ComponentName, controls: List<Control>)
+    fun onActionResponse(
+        componentName: ComponentName,
+        controlId: String,
+        @ControlAction.ResponseResult response: Int
+    )
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
new file mode 100644
index 0000000..0ace126
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2020 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.controls.ui
+
+import android.content.ComponentName
+import android.service.controls.Control
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class ControlsUiControllerImpl @Inject constructor() : ControlsUiController {
+
+    override fun onRefreshState(componentName: ComponentName, controls: List<Control>) {
+        TODO("not implemented")
+    }
+
+    override fun onActionResponse(componentName: ComponentName, controlId: String, response: Int) {
+        TODO("not implemented")
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
index df79310..9598e3c 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
@@ -20,6 +20,7 @@
 
 import com.android.systemui.ForegroundServicesDialog;
 import com.android.systemui.keyguard.WorkLockActivity;
+import com.android.systemui.screenrecord.ScreenRecordDialog;
 import com.android.systemui.settings.BrightnessDialog;
 import com.android.systemui.tuner.TunerActivity;
 
@@ -29,7 +30,7 @@
 import dagger.multibindings.IntoMap;
 
 /**
- * Services and Activities that are injectable should go here.
+ * Activities that are injectable should go here.
  */
 @Module
 public abstract class DefaultActivityBinder {
@@ -56,4 +57,10 @@
     @IntoMap
     @ClassKey(BrightnessDialog.class)
     public abstract Activity bindBrightnessDialog(BrightnessDialog activity);
+
+    /** Inject into ScreenRecordDialog */
+    @Binds
+    @IntoMap
+    @ClassKey(ScreenRecordDialog.class)
+    public abstract Activity bindScreenRecordDialog(ScreenRecordDialog activity);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java
index f790d99..f006acf 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java
@@ -22,6 +22,7 @@
 import com.android.systemui.SystemUIService;
 import com.android.systemui.doze.DozeService;
 import com.android.systemui.keyguard.KeyguardService;
+import com.android.systemui.screenrecord.RecordingService;
 import com.android.systemui.screenshot.TakeScreenshotService;
 
 import dagger.Binds;
@@ -63,4 +64,10 @@
     @IntoMap
     @ClassKey(TakeScreenshotService.class)
     public abstract Service bindTakeScreenshotService(TakeScreenshotService service);
+
+    /** Inject into RecordingService */
+    @Binds
+    @IntoMap
+    @ClassKey(RecordingService.class)
+    public abstract Service bindRecordingService(RecordingService service);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DependencyBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DependencyBinder.java
index 20917bd..2877ed0 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DependencyBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DependencyBinder.java
@@ -20,6 +20,7 @@
 import com.android.systemui.appops.AppOpsController;
 import com.android.systemui.appops.AppOpsControllerImpl;
 import com.android.systemui.classifier.FalsingManagerProxy;
+import com.android.systemui.controls.dagger.ControlsModule;
 import com.android.systemui.doze.DozeHost;
 import com.android.systemui.globalactions.GlobalActionsComponent;
 import com.android.systemui.globalactions.GlobalActionsImpl;
@@ -85,7 +86,7 @@
 /**
  * Maps interfaces to implementations for use with Dagger.
  */
-@Module
+@Module(includes = {ControlsModule.class})
 public abstract class DependencyBinder {
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
index f06c849..2b53727 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
@@ -42,6 +42,7 @@
 import com.android.systemui.qs.tiles.NfcTile;
 import com.android.systemui.qs.tiles.NightDisplayTile;
 import com.android.systemui.qs.tiles.RotationLockTile;
+import com.android.systemui.qs.tiles.ScreenRecordTile;
 import com.android.systemui.qs.tiles.UiModeNightTile;
 import com.android.systemui.qs.tiles.UserTile;
 import com.android.systemui.qs.tiles.WifiTile;
@@ -77,6 +78,7 @@
     private final Provider<NfcTile> mNfcTileProvider;
     private final Provider<GarbageMonitor.MemoryTile> mMemoryTileProvider;
     private final Provider<UiModeNightTile> mUiModeNightTileProvider;
+    private final Provider<ScreenRecordTile> mScreenRecordTileProvider;
 
     private QSTileHost mHost;
 
@@ -100,7 +102,8 @@
             Provider<NightDisplayTile> nightDisplayTileProvider,
             Provider<NfcTile> nfcTileProvider,
             Provider<GarbageMonitor.MemoryTile> memoryTileProvider,
-            Provider<UiModeNightTile> uiModeNightTileProvider) {
+            Provider<UiModeNightTile> uiModeNightTileProvider,
+            Provider<ScreenRecordTile> screenRecordTileProvider) {
         mWifiTileProvider = wifiTileProvider;
         mBluetoothTileProvider = bluetoothTileProvider;
         mControlsTileProvider = controlsTileProvider;
@@ -121,6 +124,7 @@
         mNfcTileProvider = nfcTileProvider;
         mMemoryTileProvider = memoryTileProvider;
         mUiModeNightTileProvider = uiModeNightTileProvider;
+        mScreenRecordTileProvider = screenRecordTileProvider;
     }
 
     public void setHost(QSTileHost host) {
@@ -179,6 +183,8 @@
                 return mNfcTileProvider.get();
             case "dark":
                 return mUiModeNightTileProvider.get();
+            case "screenrecord":
+                return mScreenRecordTileProvider.get();
         }
 
         // Custom tiles
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
new file mode 100644
index 0000000..596c3b9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2020 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.qs.tiles;
+
+import android.content.Intent;
+import android.service.quicksettings.Tile;
+import android.util.Log;
+
+import com.android.systemui.R;
+import com.android.systemui.plugins.qs.QSTile;
+import com.android.systemui.qs.QSHost;
+import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.screenrecord.RecordingController;
+
+import javax.inject.Inject;
+
+/**
+ * Quick settings tile for screen recording
+ */
+public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> {
+    private static final String TAG = "ScreenRecordTile";
+    private RecordingController mController;
+    private long mMillisUntilFinished = 0;
+
+    @Inject
+    public ScreenRecordTile(QSHost host, RecordingController controller) {
+        super(host);
+        mController = controller;
+    }
+
+    @Override
+    public BooleanState newTileState() {
+        return new BooleanState();
+    }
+
+    @Override
+    protected void handleClick() {
+        if (mController.isStarting()) {
+            cancelCountdown();
+        } else if (mController.isRecording()) {
+            stopRecording();
+        } else {
+            startCountdown();
+        }
+        refreshState();
+    }
+
+    /**
+     * Refresh tile state
+     * @param millisUntilFinished Time until countdown completes, or 0 if not counting down
+     */
+    public void refreshState(long millisUntilFinished) {
+        mMillisUntilFinished = millisUntilFinished;
+        refreshState();
+    }
+
+    @Override
+    protected void handleUpdateState(BooleanState state, Object arg) {
+        boolean isStarting = mController.isStarting();
+        boolean isRecording = mController.isRecording();
+
+        state.label = mContext.getString(R.string.quick_settings_screen_record_label);
+        state.value = isRecording || isStarting;
+        state.state = (isRecording || isStarting) ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
+        state.handlesLongClick = false;
+
+        if (isRecording) {
+            state.icon = ResourceIcon.get(R.drawable.ic_qs_screenrecord);
+            state.secondaryLabel = mContext.getString(R.string.quick_settings_screen_record_stop);
+        } else if (isStarting) {
+            // round, since the timer isn't exact
+            int countdown = (int) Math.floorDiv(mMillisUntilFinished + 500, 1000);
+            // TODO update icon
+            state.icon = ResourceIcon.get(R.drawable.ic_qs_screenrecord);
+            state.secondaryLabel = String.format("%d...", countdown);
+        } else {
+            // TODO update icon
+            state.icon = ResourceIcon.get(R.drawable.ic_qs_screenrecord);
+            state.secondaryLabel = mContext.getString(R.string.quick_settings_screen_record_start);
+        }
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        return 0;
+    }
+
+    @Override
+    public Intent getLongClickIntent() {
+        return null;
+    }
+
+    @Override
+    protected void handleSetListening(boolean listening) {
+    }
+
+    @Override
+    public CharSequence getTileLabel() {
+        return mContext.getString(R.string.quick_settings_screen_record_label);
+    }
+
+    private void startCountdown() {
+        Log.d(TAG, "Starting countdown");
+        // Close QS, otherwise the permission dialog appears beneath it
+        getHost().collapsePanels();
+        mController.launchRecordPrompt(this);
+    }
+
+    private void cancelCountdown() {
+        Log.d(TAG, "Cancelling countdown");
+        mController.cancelCountdown();
+    }
+
+    private void stopRecording() {
+        Log.d(TAG, "Stopping recording from tile");
+        mController.stopRecording();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
new file mode 100644
index 0000000..188501e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2020 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.screenrecord;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.CountDownTimer;
+import android.util.Log;
+
+import com.android.systemui.qs.tiles.ScreenRecordTile;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * Helper class to initiate a screen recording
+ */
+@Singleton
+public class RecordingController {
+    private static final String TAG = "RecordingController";
+    private static final String SYSUI_PACKAGE = "com.android.systemui";
+    private static final String SYSUI_SCREENRECORD_LAUNCHER =
+            "com.android.systemui.screenrecord.ScreenRecordDialog";
+
+    private final Context mContext;
+    private boolean mIsStarting;
+    private boolean mIsRecording;
+    private ScreenRecordTile mTileToUpdate;
+    private PendingIntent mStopIntent;
+    private CountDownTimer mCountDownTimer = null;
+
+    /**
+     * Create a new RecordingController
+     * @param context Context for the controller
+     */
+    @Inject
+    public RecordingController(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Show dialog of screen recording options to user.
+     */
+    public void launchRecordPrompt(ScreenRecordTile tileToUpdate) {
+        final ComponentName launcherComponent = new ComponentName(SYSUI_PACKAGE,
+                SYSUI_SCREENRECORD_LAUNCHER);
+        final Intent intent = new Intent();
+        intent.setComponent(launcherComponent);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra("com.android.systemui.screenrecord.EXTRA_SETTINGS_ONLY", true);
+        mContext.startActivity(intent);
+
+        mTileToUpdate = tileToUpdate;
+    }
+
+    /**
+     * Start counting down in preparation to start a recording
+     * @param ms Time in ms to count down
+     * @param startIntent Intent to start a recording
+     * @param stopIntent Intent to stop a recording
+     */
+    public void startCountdown(long ms, PendingIntent startIntent, PendingIntent stopIntent) {
+        mIsStarting = true;
+        mStopIntent = stopIntent;
+
+        mCountDownTimer = new CountDownTimer(ms, 1000) {
+            @Override
+            public void onTick(long millisUntilFinished) {
+                refreshTile(millisUntilFinished);
+            }
+
+            @Override
+            public void onFinish() {
+                mIsStarting = false;
+                mIsRecording = true;
+                refreshTile();
+                try {
+                    startIntent.send();
+                } catch (PendingIntent.CanceledException e) {
+                    Log.e(TAG, "Pending intent was cancelled: " + e.getMessage());
+                }
+            }
+        };
+
+        mCountDownTimer.start();
+    }
+
+    private void refreshTile() {
+        refreshTile(0);
+    }
+
+    private void refreshTile(long millisUntilFinished) {
+        if (mTileToUpdate != null) {
+            mTileToUpdate.refreshState(millisUntilFinished);
+        } else {
+            Log.e(TAG, "No tile to refresh");
+        }
+    }
+
+    /**
+     * Cancel a countdown in progress. This will not stop the recording if it already started.
+     */
+    public void cancelCountdown() {
+        if (mCountDownTimer != null) {
+            mCountDownTimer.cancel();
+        } else {
+            Log.e(TAG, "Timer was null");
+        }
+        mIsStarting = false;
+        refreshTile();
+    }
+
+    /**
+     * Check if the recording is currently counting down to begin
+     * @return
+     */
+    public boolean isStarting() {
+        return mIsStarting;
+    }
+
+    /**
+     * Check if the recording is ongoing
+     * @return
+     */
+    public boolean isRecording() {
+        return mIsRecording;
+    }
+
+    /**
+     * Stop the recording
+     */
+    public void stopRecording() {
+        updateState(false);
+        try {
+            mStopIntent.send();
+        } catch (PendingIntent.CanceledException e) {
+            Log.e(TAG, "Error stopping: " + e.getMessage());
+        }
+        refreshTile();
+    }
+
+    /**
+     * Update the current status
+     * @param isRecording
+     */
+    public void updateState(boolean isRecording) {
+        mIsRecording = isRecording;
+        refreshTile();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
index 77c3ad9..1b32168 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
@@ -53,10 +53,14 @@
 import java.text.SimpleDateFormat;
 import java.util.Date;
 
+import javax.inject.Inject;
+
 /**
  * A service which records the device screen and optionally microphone input.
  */
 public class RecordingService extends Service {
+    public static final int REQUEST_CODE = 2;
+
     private static final int NOTIFICATION_ID = 1;
     private static final String TAG = "RecordingService";
     private static final String CHANNEL_ID = "screen_record";
@@ -65,7 +69,6 @@
     private static final String EXTRA_PATH = "extra_path";
     private static final String EXTRA_USE_AUDIO = "extra_useAudio";
     private static final String EXTRA_SHOW_TAPS = "extra_showTaps";
-    private static final int REQUEST_CODE = 2;
 
     private static final String ACTION_START = "com.android.systemui.screenrecord.START";
     private static final String ACTION_STOP = "com.android.systemui.screenrecord.STOP";
@@ -81,6 +84,7 @@
     private static final int AUDIO_BIT_RATE = 16;
     private static final int AUDIO_SAMPLE_RATE = 44100;
 
+    private final RecordingController mController;
     private MediaProjectionManager mMediaProjectionManager;
     private MediaProjection mMediaProjection;
     private Surface mInputSurface;
@@ -92,6 +96,11 @@
     private boolean mShowTaps;
     private File mTempFile;
 
+    @Inject
+    public RecordingService(RecordingController controller) {
+        mController = controller;
+    }
+
     /**
      * Get an intent to start the recording service.
      *
@@ -272,6 +281,7 @@
                     null);
 
             mMediaRecorder.start();
+            mController.updateState(true);
         } catch (IOException e) {
             Log.e(TAG, "Error starting screen recording: " + e.getMessage());
             e.printStackTrace();
@@ -285,7 +295,7 @@
         NotificationChannel channel = new NotificationChannel(
                 CHANNEL_ID,
                 getString(R.string.screenrecord_name),
-                NotificationManager.IMPORTANCE_HIGH);
+                NotificationManager.IMPORTANCE_LOW);
         channel.setDescription(getString(R.string.screenrecord_channel_description));
         channel.enableVibration(true);
         NotificationManager notificationManager =
@@ -399,6 +409,7 @@
         mInputSurface.release();
         mVirtualDisplay.release();
         stopSelf();
+        mController.updateState(false);
     }
 
     private void saveRecording(NotificationManager notificationManager) {
@@ -439,7 +450,12 @@
                 Settings.System.SHOW_TOUCHES, value);
     }
 
-    private static Intent getStopIntent(Context context) {
+    /**
+     * Get an intent to stop the recording service.
+     * @param context Context from the requesting activity
+     * @return
+     */
+    public static Intent getStopIntent(Context context) {
         return new Intent(context, RecordingService.class).setAction(ACTION_STOP);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java
index 27e9fba..8324986 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java
@@ -18,55 +18,41 @@
 
 import android.Manifest;
 import android.app.Activity;
+import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.media.projection.MediaProjectionManager;
 import android.os.Bundle;
-import android.util.Log;
-import android.widget.Button;
-import android.widget.CheckBox;
 import android.widget.Toast;
 
 import com.android.systemui.R;
 
+import javax.inject.Inject;
+
 /**
  * Activity to select screen recording options
  */
 public class ScreenRecordDialog extends Activity {
-    private static final String TAG = "ScreenRecord";
     private static final int REQUEST_CODE_VIDEO_ONLY = 200;
     private static final int REQUEST_CODE_VIDEO_TAPS = 201;
     private static final int REQUEST_CODE_PERMISSIONS = 299;
     private static final int REQUEST_CODE_VIDEO_AUDIO = 300;
     private static final int REQUEST_CODE_VIDEO_AUDIO_TAPS = 301;
     private static final int REQUEST_CODE_PERMISSIONS_AUDIO = 399;
-    private boolean mUseAudio;
-    private boolean mShowTaps;
+    private static final long DELAY_MS = 3000;
+
+    private final RecordingController mController;
+
+    @Inject
+    public ScreenRecordDialog(RecordingController controller) {
+        mController = controller;
+    }
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        setContentView(R.layout.screen_record_dialog);
-
-        final CheckBox micCheckBox = findViewById(R.id.checkbox_mic);
-        final CheckBox tapsCheckBox = findViewById(R.id.checkbox_taps);
-
-        final Button recordButton = findViewById(R.id.record_button);
-        recordButton.setOnClickListener(v -> {
-            mUseAudio = micCheckBox.isChecked();
-            mShowTaps = tapsCheckBox.isChecked();
-            Log.d(TAG, "Record button clicked: audio " + mUseAudio + ", taps " + mShowTaps);
-
-            if (mUseAudio && checkSelfPermission(Manifest.permission.RECORD_AUDIO)
-                    != PackageManager.PERMISSION_GRANTED) {
-                Log.d(TAG, "Requesting permission for audio");
-                requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO},
-                        REQUEST_CODE_PERMISSIONS_AUDIO);
-            } else {
-                requestScreenCapture();
-            }
-        });
+        requestScreenCapture();
     }
 
     private void requestScreenCapture() {
@@ -74,18 +60,23 @@
                 Context.MEDIA_PROJECTION_SERVICE);
         Intent permissionIntent = mediaProjectionManager.createScreenCaptureIntent();
 
-        if (mUseAudio) {
+        // TODO get saved settings
+        boolean useAudio = false;
+        boolean showTaps = false;
+        if (useAudio) {
             startActivityForResult(permissionIntent,
-                    mShowTaps ? REQUEST_CODE_VIDEO_AUDIO_TAPS : REQUEST_CODE_VIDEO_AUDIO);
+                    showTaps ? REQUEST_CODE_VIDEO_AUDIO_TAPS : REQUEST_CODE_VIDEO_AUDIO);
         } else {
             startActivityForResult(permissionIntent,
-                    mShowTaps ? REQUEST_CODE_VIDEO_TAPS : REQUEST_CODE_VIDEO_ONLY);
+                    showTaps ? REQUEST_CODE_VIDEO_TAPS : REQUEST_CODE_VIDEO_ONLY);
         }
     }
 
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        mShowTaps = (requestCode == REQUEST_CODE_VIDEO_TAPS
+        boolean showTaps = (requestCode == REQUEST_CODE_VIDEO_TAPS
+                || requestCode == REQUEST_CODE_VIDEO_AUDIO_TAPS);
+        boolean useAudio = (requestCode == REQUEST_CODE_VIDEO_AUDIO
                 || requestCode == REQUEST_CODE_VIDEO_AUDIO_TAPS);
         switch (requestCode) {
             case REQUEST_CODE_VIDEO_TAPS:
@@ -93,11 +84,17 @@
             case REQUEST_CODE_VIDEO_ONLY:
             case REQUEST_CODE_VIDEO_AUDIO:
                 if (resultCode == RESULT_OK) {
-                    mUseAudio = (requestCode == REQUEST_CODE_VIDEO_AUDIO
-                            || requestCode == REQUEST_CODE_VIDEO_AUDIO_TAPS);
-                    startForegroundService(
-                            RecordingService.getStartIntent(this, resultCode, data, mUseAudio,
-                                    mShowTaps));
+                    PendingIntent startIntent = PendingIntent.getForegroundService(
+                            this, RecordingService.REQUEST_CODE, RecordingService.getStartIntent(
+                                    ScreenRecordDialog.this, resultCode, data, useAudio,
+                                    showTaps),
+                            PendingIntent.FLAG_UPDATE_CURRENT
+                            );
+                    PendingIntent stopIntent = PendingIntent.getService(
+                            this, RecordingService.REQUEST_CODE,
+                            RecordingService.getStopIntent(this),
+                            PendingIntent.FLAG_UPDATE_CURRENT);
+                    mController.startCountdown(DELAY_MS, startIntent, stopIntent);
                 } else {
                     Toast.makeText(this,
                             getResources().getString(R.string.screenrecord_permission_error),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListDumper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListDumper.java
index 73bfe25..eaa9d78 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListDumper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListDumper.java
@@ -16,42 +16,135 @@
 
 package com.android.systemui.statusbar.notification.collection;
 
+import java.util.Arrays;
 import java.util.List;
 
-
 /**
  * Utility class for dumping the results of a {@link ShadeListBuilder} to a debug string.
  */
 public class ListDumper {
 
-    /** See class description */
-    public static String dumpList(List<ListEntry> entries) {
+    /**
+     * Creates a debug string for a list of grouped notifications that will be printed
+     * in the order given in a tiered/tree structure.
+     * @param includeRecordKeeping whether to print out the Pluggables that caused the notification
+     *                             entry to be in its current state (ie: filter, lifeExtender)
+     */
+    public static String dumpTree(
+            List<ListEntry> entries,
+            boolean includeRecordKeeping,
+            String indent) {
         StringBuilder sb = new StringBuilder();
-        for (int i = 0; i < entries.size(); i++) {
-            ListEntry entry = entries.get(i);
-            dumpEntry(entry, Integer.toString(i), "", sb);
+        final String childEntryIndent = indent + INDENT;
+        for (int topEntryIndex = 0; topEntryIndex < entries.size(); topEntryIndex++) {
+            ListEntry entry = entries.get(topEntryIndex);
+            dumpEntry(entry,
+                    Integer.toString(topEntryIndex),
+                    indent,
+                    sb,
+                    true,
+                    includeRecordKeeping);
             if (entry instanceof GroupEntry) {
                 GroupEntry ge = (GroupEntry) entry;
-                for (int j = 0; j < ge.getChildren().size(); j++) {
-                    dumpEntry(
-                            ge.getChildren().get(j),
-                            Integer.toString(j),
-                            INDENT,
-                            sb);
+                List<NotificationEntry> children = ge.getChildren();
+                for (int childIndex = 0;  childIndex < children.size(); childIndex++) {
+                    dumpEntry(children.get(childIndex),
+                            Integer.toString(topEntryIndex) + "." + Integer.toString(childIndex),
+                            childEntryIndent,
+                            sb,
+                            true,
+                            includeRecordKeeping);
                 }
             }
         }
         return sb.toString();
     }
 
+    /**
+     * Creates a debug string for a flat list of notifications
+     * @param includeRecordKeeping whether to print out the Pluggables that caused the notification
+     *                             entry to be in its current state (ie: filter, lifeExtender)
+     */
+    public static String dumpList(
+            List<NotificationEntry> entries,
+            boolean includeRecordKeeping,
+            String indent) {
+        StringBuilder sb = new StringBuilder();
+        for (int j = 0; j < entries.size(); j++) {
+            dumpEntry(
+                    entries.get(j),
+                    Integer.toString(j),
+                    indent,
+                    sb,
+                    false,
+                    includeRecordKeeping);
+        }
+        return sb.toString();
+    }
+
     private static void dumpEntry(
-            ListEntry entry, String index, String indent, StringBuilder sb) {
+            ListEntry entry,
+            String index,
+            String indent,
+            StringBuilder sb,
+            boolean includeParent,
+            boolean includeRecordKeeping) {
         sb.append(indent)
                 .append("[").append(index).append("] ")
-                .append(entry.getKey())
-                .append(" (parent=")
-                .append(entry.getParent() != null ? entry.getParent().getKey() : null)
-                .append(")\n");
+                .append(entry.getKey());
+
+        if (includeParent) {
+            sb.append(" (parent=")
+                    .append(entry.getParent() != null ? entry.getParent().getKey() : null)
+                    .append(")");
+        }
+
+        if (entry.mNotifSection != null) {
+            sb.append(" sectionIndex=")
+                    .append(entry.getSection())
+                    .append(" sectionName=")
+                    .append(entry.mNotifSection.getName());
+        }
+
+        if (includeRecordKeeping) {
+            NotificationEntry notifEntry = entry.getRepresentativeEntry();
+            StringBuilder rksb = new StringBuilder();
+
+            if (!notifEntry.mLifetimeExtenders.isEmpty()) {
+                String[] lifetimeExtenderNames = new String[notifEntry.mLifetimeExtenders.size()];
+                for (int i = 0; i < lifetimeExtenderNames.length; i++) {
+                    lifetimeExtenderNames[i] = notifEntry.mLifetimeExtenders.get(i).getName();
+                }
+                rksb.append("lifetimeExtenders=")
+                        .append(Arrays.toString(lifetimeExtenderNames))
+                        .append(" ");
+            }
+
+            if (notifEntry.mExcludingFilter != null) {
+                rksb.append("filter=")
+                        .append(notifEntry.mExcludingFilter)
+                        .append(" ");
+            }
+
+            if (notifEntry.mNotifPromoter != null) {
+                rksb.append("promoter=")
+                        .append(notifEntry.mNotifPromoter)
+                        .append(" ");
+            }
+
+            if (notifEntry.hasInflationError()) {
+                rksb.append("hasInflationError ");
+            }
+
+            String rkString = rksb.toString();
+            if (!rkString.isEmpty()) {
+                sb.append("\n\t")
+                        .append(indent)
+                        .append(rkString);
+            }
+        }
+
+        sb.append("\n");
     }
 
     private static final String INDENT = "  ";
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java
index dc68c4b..56ad0e1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java
@@ -18,6 +18,8 @@
 
 import android.annotation.Nullable;
 
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSection;
+
 /**
  * Abstract superclass for top-level entries, i.e. things that can appear in the final notification
  * list shown to users. In practice, this means either GroupEntries or NotificationEntries.
@@ -27,7 +29,9 @@
 
     @Nullable private GroupEntry mParent;
     @Nullable private GroupEntry mPreviousParent;
-    private int mSection;
+    @Nullable NotifSection mNotifSection;
+
+    private int mSection = -1;
     int mFirstAddedIteration = -1;
 
     ListEntry(String key) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index 4b15b7f..c488c6b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -47,6 +47,8 @@
 import android.util.Log;
 
 import com.android.internal.statusbar.IStatusBarService;
+import com.android.systemui.DumpController;
+import com.android.systemui.Dumpable;
 import com.android.systemui.statusbar.notification.collection.coalescer.CoalescedEvent;
 import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer;
 import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer.BatchableNotificationHandler;
@@ -56,6 +58,8 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
 import com.android.systemui.util.Assert;
 
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -92,7 +96,7 @@
  */
 @MainThread
 @Singleton
-public class NotifCollection {
+public class NotifCollection implements Dumpable {
     private final IStatusBarService mStatusBarService;
 
     private final Map<String, NotificationEntry> mNotificationSet = new ArrayMap<>();
@@ -107,9 +111,10 @@
     private boolean mAmDispatchingToOtherCode;
 
     @Inject
-    public NotifCollection(IStatusBarService statusBarService) {
+    public NotifCollection(IStatusBarService statusBarService, DumpController dumpController) {
         Assert.isMainThread();
         mStatusBarService = statusBarService;
+        dumpController.registerDumpable(TAG, this);
     }
 
     /** Initializes the NotifCollection and registers it to receive notification events. */
@@ -442,4 +447,19 @@
     public @interface CancellationReason {}
 
     public static final int REASON_UNKNOWN = 0;
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        final List<NotificationEntry> entries = new ArrayList<>(getActiveNotifs());
+
+        pw.println("\t" + TAG + " unsorted/unfiltered notifications:");
+        if (entries.size() == 0) {
+            pw.println("\t\t None");
+        }
+        pw.println(
+                ListDumper.dumpList(
+                        entries,
+                        true,
+                        "\t\t"));
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.java
index 7124517..0377f57 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.java
@@ -22,7 +22,7 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.SectionsProvider;
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSection;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
 
@@ -48,7 +48,7 @@
  *   GroupEntry. These groups are then transformed in order to remove children or completely split
  *   them apart. To participate, see {@link #addPromoter}.
  * - Sorted: All top-level notifications are sorted. To participate, see
- *   {@link #setSectionsProvider} and {@link #setComparators}
+ *   {@link #setSections} and {@link #setComparators}
  *
  * The exact order of all hooks is as follows:
  *  0. Collection listeners are fired ({@link #addCollectionListener}).
@@ -58,7 +58,7 @@
  *  3. OnBeforeTransformGroupListeners are fired ({@link #addOnBeforeTransformGroupsListener})
  *  4. NotifPromoters are called on each notification with a parent ({@link #addPromoter})
  *  5. OnBeforeSortListeners are fired ({@link #addOnBeforeSortListener})
- *  6. SectionsProvider is called on each top-level entry in the list ({@link #setSectionsProvider})
+ *  6. Top-level entries are assigned sections by NotifSections ({@link #setSections})
  *  7. Top-level entries within the same section are sorted by NotifComparators
  *     ({@link #setComparators})
  *  8. Pre-render filters are fired on each notification ({@link #addPreRenderFilter})
@@ -142,14 +142,13 @@
     }
 
     /**
-     * Assigns sections to each top-level entry, where a section is simply an integer. Sections are
-     * the primary metric by which top-level entries are sorted; NotifComparators are only consulted
-     * when two entries are in the same section. The pipeline doesn't assign any particular meaning
-     * to section IDs -- from it's perspective they're just numbers and it sorts them by a simple
-     * numerical comparison.
+     * Sections that are used to sort top-level entries.  If two entries have the same section,
+     * NotifComparators are consulted. Sections from this list are called in order for each
+     * notification passed through the pipeline. The first NotifSection to return true for
+     * {@link NotifSection#isInSection(ListEntry)} sets the entry as part of its Section.
      */
-    public void setSectionsProvider(SectionsProvider provider) {
-        mShadeListBuilder.setSectionsProvider(provider);
+    public void setSections(List<NotifSection> sections) {
+        mShadeListBuilder.setSections(sections);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
index 76c524b..97f8ec5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.notification.collection;
 
 import static com.android.systemui.statusbar.notification.collection.GroupEntry.ROOT_ENTRY;
-import static com.android.systemui.statusbar.notification.collection.ListDumper.dumpList;
 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_BUILD_STARTED;
 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_FINALIZING;
 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_GROUPING;
@@ -31,7 +30,10 @@
 import android.annotation.MainThread;
 import android.annotation.Nullable;
 import android.util.ArrayMap;
+import android.util.Pair;
 
+import com.android.systemui.DumpController;
+import com.android.systemui.Dumpable;
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener;
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener;
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener;
@@ -39,13 +41,15 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.SectionsProvider;
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSection;
 import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener;
 import com.android.systemui.statusbar.notification.logging.NotifEvent;
 import com.android.systemui.statusbar.notification.logging.NotifLog;
 import com.android.systemui.util.Assert;
 import com.android.systemui.util.time.SystemClock;
 
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -63,7 +67,7 @@
  */
 @MainThread
 @Singleton
-public class ShadeListBuilder {
+public class ShadeListBuilder implements Dumpable {
     private final SystemClock mSystemClock;
     private final NotifLog mNotifLog;
 
@@ -79,7 +83,7 @@
     private final List<NotifPromoter> mNotifPromoters = new ArrayList<>();
     private final List<NotifFilter> mNotifPreRenderFilters = new ArrayList<>();
     private final List<NotifComparator> mNotifComparators = new ArrayList<>();
-    private SectionsProvider mSectionsProvider = new DefaultSectionsProvider();
+    private final List<NotifSection> mNotifSections = new ArrayList<>();
 
     private final List<OnBeforeTransformGroupsListener> mOnBeforeTransformGroupsListeners =
             new ArrayList<>();
@@ -92,10 +96,14 @@
     private final List<ListEntry> mReadOnlyNotifList = Collections.unmodifiableList(mNotifList);
 
     @Inject
-    public ShadeListBuilder(SystemClock systemClock, NotifLog notifLog) {
+    public ShadeListBuilder(
+            SystemClock systemClock,
+            NotifLog notifLog,
+            DumpController dumpController) {
         Assert.isMainThread();
         mSystemClock = systemClock;
         mNotifLog = notifLog;
+        dumpController.registerDumpable(TAG, this);
     }
 
     /**
@@ -163,12 +171,15 @@
         promoter.setInvalidationListener(this::onPromoterInvalidated);
     }
 
-    void setSectionsProvider(SectionsProvider provider) {
+    void setSections(List<NotifSection> sections) {
         Assert.isMainThread();
         mPipelineState.requireState(STATE_IDLE);
 
-        mSectionsProvider = provider;
-        provider.setInvalidationListener(this::onSectionsProviderInvalidated);
+        mNotifSections.clear();
+        for (NotifSection section : sections) {
+            mNotifSections.add(section);
+            section.setInvalidationListener(this::onNotifSectionInvalidated);
+        }
     }
 
     void setComparators(List<NotifComparator> comparators) {
@@ -223,12 +234,12 @@
         rebuildListIfBefore(STATE_TRANSFORMING);
     }
 
-    private void onSectionsProviderInvalidated(SectionsProvider provider) {
+    private void onNotifSectionInvalidated(NotifSection section) {
         Assert.isMainThread();
 
-        mNotifLog.log(NotifEvent.SECTIONS_PROVIDER_INVALIDATED, String.format(
-                "Sections provider \"%s\" invalidated; pipeline state is %d",
-                provider.getName(),
+        mNotifLog.log(NotifEvent.SECTION_INVALIDATED, String.format(
+                "Section \"%s\" invalidated; pipeline state is %d",
+                section.getName(),
                 mPipelineState.getState()));
 
         rebuildListIfBefore(STATE_SORTING);
@@ -311,7 +322,7 @@
         sortList();
 
         // Step 6: Filter out entries after pre-group filtering, grouping, promoting and sorting
-        // Now filters can see grouping information to determine whether to filter or not
+        // Now filters can see grouping information to determine whether to filter or not.
         mPipelineState.incrementTo(STATE_PRE_RENDER_FILTERING);
         filterNotifs(mNotifList, mNewNotifList, mNotifPreRenderFilters);
         applyNewNotifList();
@@ -324,7 +335,7 @@
 
         // Step 6: Dispatch the new list, first to any listeners and then to the view layer
         mNotifLog.log(NotifEvent.DISPATCH_FINAL_LIST, "List finalized, is:\n"
-                + dumpList(mNotifList));
+                + ListDumper.dumpTree(mNotifList, false, "\t\t"));
         dispatchOnBeforeRenderList(mReadOnlyNotifList);
         if (mOnRenderListListener != null) {
             mOnRenderListListener.onRenderList(mReadOnlyNotifList);
@@ -573,6 +584,8 @@
      * filtered out during any of the filtering steps.
      */
     private void annulAddition(ListEntry entry) {
+        entry.setSection(-1);
+        entry.mNotifSection = null;
         entry.setParent(null);
         if (entry.mFirstAddedIteration == mIterationCount) {
             entry.mFirstAddedIteration = -1;
@@ -582,11 +595,12 @@
     private void sortList() {
         // Assign sections to top-level elements and sort their children
         for (ListEntry entry : mNotifList) {
-            entry.setSection(mSectionsProvider.getSection(entry));
+            Pair<NotifSection, Integer> sectionWithIndex = applySections(entry);
             if (entry instanceof GroupEntry) {
                 GroupEntry parent = (GroupEntry) entry;
                 for (NotificationEntry child : parent.getChildren()) {
-                    child.setSection(0);
+                    child.mNotifSection = sectionWithIndex.first;
+                    child.setSection(sectionWithIndex.second);
                 }
                 parent.sortChildren(sChildComparator);
             }
@@ -747,6 +761,45 @@
         return null;
     }
 
+    private Pair<NotifSection, Integer> applySections(ListEntry entry) {
+        final Pair<NotifSection, Integer> sectionWithIndex = findSection(entry);
+        final NotifSection section = sectionWithIndex.first;
+        final Integer sectionIndex = sectionWithIndex.second;
+
+        if (section != entry.mNotifSection) {
+            if (entry.mNotifSection == null) {
+                mNotifLog.log(NotifEvent.SECTION_CHANGED, String.format(
+                        "%s: sectioned by '%s' [index=%d].",
+                        entry.getKey(),
+                        section.getName(),
+                        sectionIndex));
+            } else {
+                mNotifLog.log(NotifEvent.SECTION_CHANGED, String.format(
+                        "%s: section changed: '%s' [index=%d] -> '%s [index=%d]'.",
+                        entry.getKey(),
+                        entry.mNotifSection,
+                        entry.getSection(),
+                        section,
+                        sectionIndex));
+            }
+
+            entry.mNotifSection = section;
+            entry.setSection(sectionIndex);
+        }
+
+        return sectionWithIndex;
+    }
+
+    private Pair<NotifSection, Integer> findSection(ListEntry entry) {
+        for (int i = 0; i < mNotifSections.size(); i++) {
+            NotifSection sectioner = mNotifSections.get(i);
+            if (sectioner.isInSection(entry)) {
+                return new Pair<>(sectioner, i);
+            }
+        }
+        return new Pair<>(sDefaultSection, mNotifSections.size());
+    }
+
     private void rebuildListIfBefore(@PipelineState.StateName int state) {
         mPipelineState.requireIsBefore(state);
         if (mPipelineState.is(STATE_IDLE)) {
@@ -772,6 +825,19 @@
         }
     }
 
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("\t" + TAG + " shade notifications:");
+        if (getShadeList().size() == 0) {
+            pw.println("\t\t None");
+        }
+
+        pw.println(ListDumper.dumpTree(
+                getShadeList(),
+                true,
+                "\t\t"));
+    }
+
     /** See {@link #setOnRenderListListener(OnRenderListListener)} */
     public interface OnRenderListListener {
         /**
@@ -783,16 +849,13 @@
         void onRenderList(List<ListEntry> entries);
     }
 
-    private static class DefaultSectionsProvider extends SectionsProvider {
-        DefaultSectionsProvider() {
-            super("DefaultSectionsProvider");
-        }
-
-        @Override
-        public int getSection(ListEntry entry) {
-            return 0;
-        }
-    }
+    private static final NotifSection sDefaultSection =
+            new NotifSection("DefaultSection") {
+                @Override
+                public boolean isInSection(ListEntry entry) {
+                    return true;
+                }
+            };
 
     private static final String TAG = "NotifListBuilderImpl";
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/Coordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/Coordinator.java
index c1a11b2..d8b2e40 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/Coordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/Coordinator.java
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.notification.collection.coordinator;
 
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSection;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable;
 
 /**
@@ -28,4 +29,8 @@
      * Coordinators should register their listeners and {@link Pluggable}s to the pipeline.
      */
     void attach(NotifPipeline pipeline);
+
+    default NotifSection getSection() {
+        return null;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.java
index 562a618..8d0dd5b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.java
@@ -16,9 +16,11 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator;
 
+import com.android.systemui.DumpController;
 import com.android.systemui.Dumpable;
 import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSection;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
@@ -39,18 +41,21 @@
 public class NotifCoordinators implements Dumpable {
     private static final String TAG = "NotifCoordinators";
     private final List<Coordinator> mCoordinators = new ArrayList<>();
-
+    private final List<NotifSection> mOrderedSections = new ArrayList<>();
     /**
      * Creates all the coordinators.
      */
     @Inject
     public NotifCoordinators(
+            DumpController dumpController,
             FeatureFlags featureFlags,
             KeyguardCoordinator keyguardCoordinator,
             RankingCoordinator rankingCoordinator,
             ForegroundCoordinator foregroundCoordinator,
             DeviceProvisionedCoordinator deviceProvisionedCoordinator,
             PreparationCoordinator preparationCoordinator) {
+        dumpController.registerDumpable(TAG, this);
+
         mCoordinators.add(keyguardCoordinator);
         mCoordinators.add(rankingCoordinator);
         mCoordinators.add(foregroundCoordinator);
@@ -59,6 +64,13 @@
             mCoordinators.add(preparationCoordinator);
         }
         // TODO: add new Coordinators here! (b/145134683, b/112656837)
+
+        // TODO: add the sections in a particular ORDER (HeadsUp < People < Alerting)
+        for (Coordinator c : mCoordinators) {
+            if (c.getSection() != null) {
+                mOrderedSections.add(c.getSection());
+            }
+        }
     }
 
     /**
@@ -69,6 +81,8 @@
         for (Coordinator c : mCoordinators) {
             c.attach(pipeline);
         }
+
+        pipeline.setSections(mOrderedSections);
     }
 
     @Override
@@ -78,5 +92,9 @@
         for (Coordinator c : mCoordinators) {
             pw.println("\t" + c.getClass());
         }
+
+        for (NotifSection s : mOrderedSections) {
+            pw.println("\t" + s.getName());
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
index 1ab20a9..5cd3e94 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
@@ -41,6 +41,9 @@
 import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
+import com.android.systemui.statusbar.notification.people.NotificationPersonExtractorPluginBoundary;
+import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
+import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifierImpl;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder;
@@ -49,6 +52,7 @@
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.policy.ExtensionControllerImpl;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 
 import java.util.Objects;
@@ -66,7 +70,6 @@
     private final NotificationGroupManager mGroupManager;
     private final NotificationGutsManager mGutsManager;
     private final NotificationInterruptionStateProvider mNotificationInterruptionStateProvider;
-
     private final Context mContext;
     private final NotificationRowContentBinder mRowContentBinder;
     private final NotificationMessagingUtil mMessagingUtil;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifSection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifSection.java
new file mode 100644
index 0000000..fe5ba3c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifSection.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2020 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.notification.collection.listbuilder.pluggable;
+
+import com.android.systemui.statusbar.notification.collection.ListEntry;
+import com.android.systemui.statusbar.notification.collection.ShadeListBuilder;
+
+/**
+ * Pluggable for participating in notif sectioning. See {@link ShadeListBuilder#setSections}.
+ */
+public abstract class NotifSection extends Pluggable<NotifSection> {
+    protected NotifSection(String name) {
+        super(name);
+    }
+
+    /**
+     * If returns true, this notification is considered within this section.
+     * Sectioning is performed on each top level notification each time the pipeline is run.
+     * However, this doesn't necessarily mean that your section will get called on each top-level
+     * notification. The first section to return true determines the section of the notification.
+     */
+    public abstract boolean isInSection(ListEntry entry);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/SectionsProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/SectionsProvider.java
deleted file mode 100644
index 11ea850..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/SectionsProvider.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2019 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.notification.collection.listbuilder.pluggable;
-
-import com.android.systemui.statusbar.notification.collection.ListEntry;
-
-/**
- * Interface for sorting notifications into "sections", such as a heads-upping section, people
- * section, alerting section, silent section, etc.
- */
-public abstract class SectionsProvider extends Pluggable<SectionsProvider> {
-
-    protected SectionsProvider(String name) {
-        super(name);
-    }
-
-    /**
-     * Returns the section that this entry belongs to. A section can be any non-negative integer.
-     * When entries are sorted, they are first sorted by section and then by any remainining
-     * comparators.
-     */
-    public abstract int getSection(ListEntry entry);
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotifEvent.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotifEvent.java
index 02acc81..2374cde 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotifEvent.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotifEvent.java
@@ -94,7 +94,7 @@
             LIST_BUILD_COMPLETE,
             PRE_GROUP_FILTER_INVALIDATED,
             PROMOTER_INVALIDATED,
-            SECTIONS_PROVIDER_INVALIDATED,
+            SECTION_INVALIDATED,
             COMPARATOR_INVALIDATED,
             PARENT_CHANGED,
             FILTER_CHANGED,
@@ -132,12 +132,13 @@
                     "ListBuildComplete",
                     "FilterInvalidated",
                     "PromoterInvalidated",
-                    "SectionsProviderInvalidated",
+                    "SectionInvalidated",
                     "ComparatorInvalidated",
                     "ParentChanged",
                     "FilterChanged",
                     "PromoterChanged",
                     "FinalFilterInvalidated",
+                    "SectionerChanged",
 
                     // NEM event labels:
                     "NotifAdded",
@@ -170,13 +171,14 @@
     public static final int LIST_BUILD_COMPLETE = 4;
     public static final int PRE_GROUP_FILTER_INVALIDATED = 5;
     public static final int PROMOTER_INVALIDATED = 6;
-    public static final int SECTIONS_PROVIDER_INVALIDATED = 7;
+    public static final int SECTION_INVALIDATED = 7;
     public static final int COMPARATOR_INVALIDATED = 8;
     public static final int PARENT_CHANGED = 9;
     public static final int FILTER_CHANGED = 10;
     public static final int PROMOTER_CHANGED = 11;
     public static final int PRE_RENDER_FILTER_INVALIDATED = 12;
-    private static final int TOTAL_LIST_BUILDER_EVENT_TYPES = 13;
+    public static final int SECTION_CHANGED = 13;
+    private static final int TOTAL_LIST_BUILDER_EVENT_TYPES = 14;
 
     /**
      * Events related to {@link NotificationEntryManager}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index a8a35d0..b71beda 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -1150,6 +1150,7 @@
         mMenuRow = plugin;
         if (mMenuRow.shouldUseDefaultMenuItems()) {
             ArrayList<MenuItem> items = new ArrayList<>();
+            items.add(NotificationMenuRow.createConversationItem(mContext));
             items.add(NotificationMenuRow.createInfoItem(mContext));
             items.add(NotificationMenuRow.createSnoozeItem(mContext));
             items.add(NotificationMenuRow.createAppOpsItem(mContext));
@@ -1163,7 +1164,7 @@
     @Override
     public void onPluginDisconnected(NotificationMenuRowPlugin plugin) {
         boolean existed = mMenuRow.getMenuView() != null;
-        mMenuRow = new NotificationMenuRow(mContext); // Back to default
+        mMenuRow = new NotificationMenuRow(mContext);
         if (existed) {
             createMenu();
         }
@@ -1720,6 +1721,8 @@
      */
     public void reset() {
         mShowingPublicInitialized = false;
+        unDismiss();
+        resetTranslation();
         onHeightReset();
         requestLayout();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java
new file mode 100644
index 0000000..ec420f3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java
@@ -0,0 +1,633 @@
+/*
+ * Copyright (C) 2020 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.notification.row;
+
+import static android.app.Notification.EXTRA_IS_GROUP_CONVERSATION;
+import static android.app.NotificationChannel.PLACEHOLDER_CONVERSATION_ID;
+import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
+import static android.app.NotificationManager.IMPORTANCE_LOW;
+import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
+import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC;
+import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED;
+
+import static com.android.systemui.statusbar.notification.row.NotificationConversationInfo.UpdateChannelRunnable.ACTION_BUBBLE;
+import static com.android.systemui.statusbar.notification.row.NotificationConversationInfo.UpdateChannelRunnable.ACTION_FAVORITE;
+import static com.android.systemui.statusbar.notification.row.NotificationConversationInfo.UpdateChannelRunnable.ACTION_HOME;
+import static com.android.systemui.statusbar.notification.row.NotificationConversationInfo.UpdateChannelRunnable.ACTION_MUTE;
+import static com.android.systemui.statusbar.notification.row.NotificationConversationInfo.UpdateChannelRunnable.ACTION_SNOOZE;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.app.INotificationManager;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Slog;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.bubbles.BubbleController;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+
+import java.lang.annotation.Retention;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * The guts of a conversation notification revealed when performing a long press.
+ */
+public class NotificationConversationInfo extends LinearLayout implements
+        NotificationGuts.GutsContent {
+    private static final String TAG = "ConversationGuts";
+
+
+    private INotificationManager mINotificationManager;
+    private LauncherApps mLauncherApps;
+    ShortcutManager mShortcutManager;
+    private PackageManager mPm;
+    private VisualStabilityManager mVisualStabilityManager;
+
+    private String mPackageName;
+    private String mAppName;
+    private int mAppUid;
+    private String mDelegatePkg;
+    private NotificationChannel mNotificationChannel;
+    private ShortcutInfo mShortcutInfo;
+    private String mConversationId;
+    private NotificationEntry mEntry;
+    private StatusBarNotification mSbn;
+    private boolean mIsDeviceProvisioned;
+
+    private int mStartingChannelImportance;
+    private boolean mStartedAsBubble;
+    private boolean mIsBubbleable;
+    // TODO: remove when launcher api works
+    @VisibleForTesting
+    boolean mShowHomeScreen = false;
+
+    private @UpdateChannelRunnable.Action int mSelectedAction = -1;
+
+    private OnSnoozeClickListener mOnSnoozeClickListener;
+    private OnSettingsClickListener mOnSettingsClickListener;
+    private OnAppSettingsClickListener mAppSettingsClickListener;
+    private NotificationGuts mGutsContainer;
+    private BubbleController mBubbleController;
+
+    @VisibleForTesting
+    boolean mSkipPost = false;
+
+    private OnClickListener mOnBubbleClick = v -> {
+        mSelectedAction = ACTION_BUBBLE;
+        if (mStartedAsBubble) {
+            mBubbleController.onUserDemotedBubbleFromNotification(mEntry);
+        } else {
+            mBubbleController.onUserCreatedBubbleFromNotification(mEntry);
+        }
+        closeControls(v, true);
+    };
+
+    private OnClickListener mOnHomeClick = v -> {
+        mSelectedAction = ACTION_HOME;
+        mShortcutManager.requestPinShortcut(mShortcutInfo, null);
+        closeControls(v, true);
+    };
+
+    private OnClickListener mOnFavoriteClick = v -> {
+        mSelectedAction = ACTION_FAVORITE;
+        closeControls(v, true);
+    };
+
+    private OnClickListener mOnSnoozeClick = v -> {
+        mSelectedAction = ACTION_SNOOZE;
+        mOnSnoozeClickListener.onClick(v, 1);
+        closeControls(v, true);
+    };
+
+    private OnClickListener mOnMuteClick = v -> {
+      mSelectedAction = ACTION_MUTE;
+      closeControls(v, true);
+    };
+
+    public NotificationConversationInfo(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public interface OnSettingsClickListener {
+        void onClick(View v, NotificationChannel channel, int appUid);
+    }
+
+    public interface OnAppSettingsClickListener {
+        void onClick(View v, Intent intent);
+    }
+
+    public interface OnSnoozeClickListener {
+        void onClick(View v, int hoursToSnooze);
+    }
+
+    public void bindNotification(
+            ShortcutManager shortcutManager,
+            LauncherApps launcherApps,
+            PackageManager pm,
+            INotificationManager iNotificationManager,
+            VisualStabilityManager visualStabilityManager,
+            String pkg,
+            NotificationChannel notificationChannel,
+            NotificationEntry entry,
+            OnSettingsClickListener onSettingsClick,
+            OnAppSettingsClickListener onAppSettingsClick,
+            OnSnoozeClickListener onSnoozeClickListener,
+            boolean isDeviceProvisioned) {
+        mSelectedAction = -1;
+        mINotificationManager = iNotificationManager;
+        mVisualStabilityManager = visualStabilityManager;
+        mBubbleController = Dependency.get(BubbleController.class);
+        mPackageName = pkg;
+        mEntry = entry;
+        mSbn = entry.getSbn();
+        mPm = pm;
+        mAppSettingsClickListener = onAppSettingsClick;
+        mAppName = mPackageName;
+        mOnSettingsClickListener = onSettingsClick;
+        mNotificationChannel = notificationChannel;
+        mStartingChannelImportance = mNotificationChannel.getImportance();
+        mAppUid = mSbn.getUid();
+        mDelegatePkg = mSbn.getOpPkg();
+        mIsDeviceProvisioned = isDeviceProvisioned;
+        mOnSnoozeClickListener = onSnoozeClickListener;
+
+        mShortcutManager = shortcutManager;
+        mLauncherApps = launcherApps;
+        mConversationId = mNotificationChannel.getConversationId();
+        if (TextUtils.isEmpty(mNotificationChannel.getConversationId())) {
+            mConversationId = mSbn.getNotification().getShortcutId();
+        }
+        // TODO: flag this when flag exists
+        if (TextUtils.isEmpty(mConversationId)) {
+            mConversationId = mSbn.getId() + mSbn.getTag() + PLACEHOLDER_CONVERSATION_ID;
+        }
+        // TODO: consider querying this earlier in the notification pipeline and passing it in
+        LauncherApps.ShortcutQuery query = new LauncherApps.ShortcutQuery()
+                .setPackage(mPackageName)
+                .setQueryFlags(FLAG_MATCH_DYNAMIC | FLAG_MATCH_PINNED)
+                .setShortcutIds(Arrays.asList(mConversationId));
+        List<ShortcutInfo> shortcuts = mLauncherApps.getShortcuts(query, mSbn.getUser());
+        if (shortcuts != null && !shortcuts.isEmpty()) {
+            mShortcutInfo = shortcuts.get(0);
+        }
+
+        mIsBubbleable = mEntry.getBubbleMetadata() != null;
+        mStartedAsBubble = mEntry.isBubble();
+
+        createConversationChannelIfNeeded();
+
+        bindHeader();
+        bindActions();
+
+    }
+
+    void createConversationChannelIfNeeded() {
+        // If this channel is not already a customized conversation channel, create
+        // a custom channel
+        if (TextUtils.isEmpty(mNotificationChannel.getConversationId())) {
+            try {
+                // TODO: associate this key with this channel service side so the customization
+                // isn't forgotten on the next update
+                mINotificationManager.createConversationNotificationChannelForPackage(
+                        mPackageName, mAppUid, mNotificationChannel, mConversationId);
+                mNotificationChannel = mINotificationManager.getConversationNotificationChannel(
+                        mContext.getOpPackageName(), UserHandle.getUserId(mAppUid), mPackageName,
+                        mNotificationChannel.getId(), false, mConversationId);
+
+                // TODO: ask LA to pin the shortcut once api exists for pinning one shortcut at a
+                // time
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Could not create conversation channel", e);
+            }
+        }
+    }
+
+    private void bindActions() {
+        // TODO: figure out what should happen for non-configurable channels
+
+        Button bubble = findViewById(R.id.bubble);
+        bubble.setVisibility(mIsBubbleable ? VISIBLE : GONE);
+        bubble.setOnClickListener(mOnBubbleClick);
+        if (mStartedAsBubble) {
+            bubble.setText(R.string.notification_conversation_unbubble);
+        } else {
+            bubble.setText(R.string.notification_conversation_bubble);
+        }
+
+        Button home = findViewById(R.id.home);
+        home.setOnClickListener(mOnHomeClick);
+        home.setVisibility(mShowHomeScreen && mShortcutInfo != null
+                && mShortcutManager.isRequestPinShortcutSupported()
+                ? VISIBLE : GONE);
+
+        Button favorite = findViewById(R.id.fave);
+        favorite.setOnClickListener(mOnFavoriteClick);
+        if (mNotificationChannel.canBypassDnd()) {
+            favorite.setText(R.string.notification_conversation_unfavorite);
+            favorite.setCompoundDrawablesRelative(
+                    mContext.getDrawable(R.drawable.ic_star), null, null, null);
+        } else {
+            favorite.setText(R.string.notification_conversation_favorite);
+            favorite.setCompoundDrawablesRelative(
+                    mContext.getDrawable(R.drawable.ic_star_border), null, null, null);
+        }
+
+        Button snooze = findViewById(R.id.snooze);
+        snooze.setOnClickListener(mOnSnoozeClick);
+
+        Button mute = findViewById(R.id.mute);
+        mute.setOnClickListener(mOnMuteClick);
+        if (mStartingChannelImportance >= IMPORTANCE_DEFAULT
+                || mStartingChannelImportance == IMPORTANCE_UNSPECIFIED) {
+            mute.setText(R.string.notification_conversation_mute);
+            favorite.setCompoundDrawablesRelative(
+                    mContext.getDrawable(R.drawable.ic_notifications_silence), null, null, null);
+        } else {
+            mute.setText(R.string.notification_conversation_unmute);
+            favorite.setCompoundDrawablesRelative(
+                    mContext.getDrawable(R.drawable.ic_notifications_alert), null, null, null);
+        }
+
+    }
+
+    private void bindHeader() {
+        bindConversationDetails();
+
+        // Delegate
+        bindDelegate();
+
+        // Set up app settings link (i.e. Customize)
+        View settingsLinkView = findViewById(R.id.app_settings);
+        Intent settingsIntent = getAppSettingsIntent(mPm, mPackageName,
+                mNotificationChannel,
+                mSbn.getId(), mSbn.getTag());
+        if (settingsIntent != null
+                && !TextUtils.isEmpty(mSbn.getNotification().getSettingsText())) {
+            settingsLinkView.setVisibility(VISIBLE);
+            settingsLinkView.setOnClickListener((View view) -> {
+                mAppSettingsClickListener.onClick(view, settingsIntent);
+            });
+        } else {
+            settingsLinkView.setVisibility(View.GONE);
+        }
+
+        // System Settings button.
+        final View settingsButton = findViewById(R.id.info);
+        settingsButton.setOnClickListener(getSettingsOnClickListener());
+        settingsButton.setVisibility(settingsButton.hasOnClickListeners() ? VISIBLE : GONE);
+    }
+
+    private OnClickListener getSettingsOnClickListener() {
+        if (mAppUid >= 0 && mOnSettingsClickListener != null && mIsDeviceProvisioned) {
+            final int appUidF = mAppUid;
+            return ((View view) -> {
+                mOnSettingsClickListener.onClick(view, mNotificationChannel, appUidF);
+            });
+        }
+        return null;
+    }
+
+    private void bindConversationDetails() {
+        final TextView channelName = findViewById(R.id.parent_channel_name);
+        channelName.setText(mNotificationChannel.getName());
+
+        bindGroup();
+        bindName();
+        bindPackage();
+        bindIcon();
+
+    }
+
+    private void bindIcon() {
+        ImageView image = findViewById(R.id.conversation_icon);
+        if (mShortcutInfo != null) {
+            image.setImageDrawable(mLauncherApps.getShortcutBadgedIconDrawable(mShortcutInfo,
+                    mContext.getResources().getDisplayMetrics().densityDpi));
+        } else {
+            // TODO: flag this behavior
+            if (mSbn.getNotification().extras.getBoolean(EXTRA_IS_GROUP_CONVERSATION, false)) {
+                // TODO: maybe use a generic group icon, or a composite of recent senders
+                image.setImageDrawable(mPm.getDefaultActivityIcon());
+            } else {
+                final List<Notification.MessagingStyle.Message> messages =
+                        Notification.MessagingStyle.Message.getMessagesFromBundleArray(
+                                (Parcelable[]) mSbn.getNotification().extras.get(
+                                        Notification.EXTRA_MESSAGES));
+
+                final Notification.MessagingStyle.Message latestMessage =
+                        Notification.MessagingStyle.findLatestIncomingMessage(messages);
+                Icon personIcon = latestMessage.getSenderPerson().getIcon();
+                if (personIcon != null) {
+                    image.setImageIcon(latestMessage.getSenderPerson().getIcon());
+                } else {
+                    // TODO: choose something better
+                    image.setImageDrawable(mPm.getDefaultActivityIcon());
+                }
+            }
+        }
+    }
+
+    private void bindName() {
+        TextView name = findViewById(R.id.name);
+        if (mShortcutInfo != null) {
+            name.setText(mShortcutInfo.getShortLabel());
+        } else {
+            // TODO: flag this behavior
+            Bundle extras = mSbn.getNotification().extras;
+            String nameString = extras.getString(Notification.EXTRA_CONVERSATION_TITLE);
+            if (TextUtils.isEmpty(nameString)) {
+                nameString = extras.getString(Notification.EXTRA_TITLE);
+            }
+            name.setText(nameString);
+        }
+    }
+
+    private void bindPackage() {
+        ApplicationInfo info;
+        try {
+            info = mPm.getApplicationInfo(
+                    mPackageName,
+                    PackageManager.MATCH_UNINSTALLED_PACKAGES
+                            | PackageManager.MATCH_DISABLED_COMPONENTS
+                            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+                            | PackageManager.MATCH_DIRECT_BOOT_AWARE);
+            if (info != null) {
+                mAppName = String.valueOf(mPm.getApplicationLabel(info));
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+        }
+        ((TextView) findViewById(R.id.pkg_name)).setText(mAppName);
+    }
+
+    private void bindDelegate() {
+        TextView delegateView = findViewById(R.id.delegate_name);
+        TextView dividerView = findViewById(R.id.pkg_divider);
+
+        if (!TextUtils.equals(mPackageName, mDelegatePkg)) {
+            // this notification was posted by a delegate!
+            delegateView.setVisibility(View.VISIBLE);
+            dividerView.setVisibility(View.VISIBLE);
+        } else {
+            delegateView.setVisibility(View.GONE);
+            dividerView.setVisibility(View.GONE);
+        }
+    }
+
+    private void bindGroup() {
+        // Set group information if this channel has an associated group.
+        CharSequence groupName = null;
+        if (mNotificationChannel != null && mNotificationChannel.getGroup() != null) {
+            try {
+                final NotificationChannelGroup notificationChannelGroup =
+                        mINotificationManager.getNotificationChannelGroupForPackage(
+                                mNotificationChannel.getGroup(), mPackageName, mAppUid);
+                if (notificationChannelGroup != null) {
+                    groupName = notificationChannelGroup.getName();
+                }
+            } catch (RemoteException e) {
+            }
+        }
+        TextView groupNameView = findViewById(R.id.group_name);
+        View groupDivider = findViewById(R.id.group_divider);
+        if (groupName != null) {
+            groupNameView.setText(groupName);
+            groupNameView.setVisibility(VISIBLE);
+            groupDivider.setVisibility(VISIBLE);
+        } else {
+            groupNameView.setVisibility(GONE);
+            groupDivider.setVisibility(GONE);
+        }
+    }
+
+    @Override
+    public boolean post(Runnable action) {
+        if (mSkipPost) {
+            action.run();
+            return true;
+        } else {
+            return super.post(action);
+        }
+    }
+
+    @Override
+    public void onFinishedClosing() {
+        // TODO: do we need to do anything here?
+    }
+
+    @Override
+    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEvent(event);
+        if (mGutsContainer != null &&
+                event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
+            if (mGutsContainer.isExposed()) {
+                event.getText().add(mContext.getString(
+                        R.string.notification_channel_controls_opened_accessibility, mAppName));
+            } else {
+                event.getText().add(mContext.getString(
+                        R.string.notification_channel_controls_closed_accessibility, mAppName));
+            }
+        }
+    }
+
+    private Intent getAppSettingsIntent(PackageManager pm, String packageName,
+            NotificationChannel channel, int id, String tag) {
+        Intent intent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES)
+                .setPackage(packageName);
+        final List<ResolveInfo> resolveInfos = pm.queryIntentActivities(
+                intent,
+                PackageManager.MATCH_DEFAULT_ONLY
+        );
+        if (resolveInfos == null || resolveInfos.size() == 0 || resolveInfos.get(0) == null) {
+            return null;
+        }
+        final ActivityInfo activityInfo = resolveInfos.get(0).activityInfo;
+        intent.setClassName(activityInfo.packageName, activityInfo.name);
+        if (channel != null) {
+            intent.putExtra(Notification.EXTRA_CHANNEL_ID, channel.getId());
+        }
+        intent.putExtra(Notification.EXTRA_NOTIFICATION_ID, id);
+        intent.putExtra(Notification.EXTRA_NOTIFICATION_TAG, tag);
+        return intent;
+    }
+
+    /**
+     * Closes the controls and commits the updated importance values (indirectly).
+     *
+     * <p><b>Note,</b> this will only get called once the view is dismissing. This means that the
+     * user does not have the ability to undo the action anymore.
+     */
+    @VisibleForTesting
+    void closeControls(View v, boolean save) {
+        int[] parentLoc = new int[2];
+        int[] targetLoc = new int[2];
+        mGutsContainer.getLocationOnScreen(parentLoc);
+        v.getLocationOnScreen(targetLoc);
+        final int centerX = v.getWidth() / 2;
+        final int centerY = v.getHeight() / 2;
+        final int x = targetLoc[0] - parentLoc[0] + centerX;
+        final int y = targetLoc[1] - parentLoc[1] + centerY;
+        mGutsContainer.closeControls(x, y, save, false /* force */);
+    }
+
+    @Override
+    public void setGutsParent(NotificationGuts guts) {
+        mGutsContainer = guts;
+    }
+
+    @Override
+    public boolean willBeRemoved() {
+        return false;
+    }
+
+    @Override
+    public boolean shouldBeSaved() {
+        return mSelectedAction > -1;
+    }
+
+    @Override
+    public View getContentView() {
+        return this;
+    }
+
+    @Override
+    public boolean handleCloseControls(boolean save, boolean force) {
+        if (save && mSelectedAction > -1) {
+            Handler bgHandler = new Handler(Dependency.get(Dependency.BG_LOOPER));
+            bgHandler.post(
+                    new UpdateChannelRunnable(mINotificationManager, mPackageName,
+                            mAppUid, mSelectedAction, mNotificationChannel));
+            mVisualStabilityManager.temporarilyAllowReordering();
+        }
+        return false;
+    }
+
+    @Override
+    public int getActualHeight() {
+        return getHeight();
+    }
+
+    @VisibleForTesting
+    public boolean isAnimating() {
+        return false;
+    }
+
+    static class UpdateChannelRunnable implements Runnable {
+
+        @Retention(SOURCE)
+        @IntDef({ACTION_BUBBLE, ACTION_HOME, ACTION_FAVORITE, ACTION_SNOOZE, ACTION_MUTE,
+                ACTION_DEMOTE})
+        private @interface Action {}
+        static final int ACTION_BUBBLE = 0;
+        static final int ACTION_HOME = 1;
+        static final int ACTION_FAVORITE = 2;
+        static final int ACTION_SNOOZE = 3;
+        static final int ACTION_MUTE = 4;
+        static final int ACTION_DEMOTE = 5;
+
+        private final INotificationManager mINotificationManager;
+        private final String mAppPkg;
+        private final int mAppUid;
+        private  NotificationChannel mChannelToUpdate;
+        private final @Action int mAction;
+
+        public UpdateChannelRunnable(INotificationManager notificationManager,
+                String packageName, int appUid, @Action int action,
+                @NonNull NotificationChannel channelToUpdate) {
+            mINotificationManager = notificationManager;
+            mAppPkg = packageName;
+            mAppUid = appUid;
+            mChannelToUpdate = channelToUpdate;
+            mAction = action;
+        }
+
+        @Override
+        public void run() {
+            try {
+                switch (mAction) {
+                    case ACTION_BUBBLE:
+                        mChannelToUpdate.setAllowBubbles(!mChannelToUpdate.canBubble());
+                        break;
+                    case ACTION_FAVORITE:
+                        // TODO: extend beyond DND
+                        mChannelToUpdate.setBypassDnd(!mChannelToUpdate.canBypassDnd());
+                        break;
+                    case ACTION_MUTE:
+                        if (mChannelToUpdate.getImportance() == IMPORTANCE_UNSPECIFIED
+                                || mChannelToUpdate.getImportance() >= IMPORTANCE_DEFAULT) {
+                            mChannelToUpdate.setImportance(IMPORTANCE_LOW);
+                        } else {
+                            mChannelToUpdate.setImportance(Math.max(
+                                    mChannelToUpdate.getOriginalImportance(), IMPORTANCE_DEFAULT));
+                        }
+                        break;
+                    case ACTION_DEMOTE:
+                        // TODO: when demotion status field exists on notificationchannel
+                        break;
+
+                }
+
+                if (mAction != ACTION_HOME && mAction != ACTION_SNOOZE) {
+                    mINotificationManager.updateNotificationChannelForPackage(
+                            mAppPkg, mAppUid, mChannelToUpdate);
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG, "Unable to update notification channel", e);
+            }
+        }
+    }
+
+    @Retention(SOURCE)
+    @IntDef({BEHAVIOR_ALERTING, BEHAVIOR_SILENT, BEHAVIOR_BUBBLE})
+    private @interface AlertingBehavior {}
+    private static final int BEHAVIOR_ALERTING = 0;
+    private static final int BEHAVIOR_SILENT = 1;
+    private static final int BEHAVIOR_BUBBLE = 2;
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
index 779a224..6789c81 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
@@ -23,7 +23,9 @@
 import android.app.NotificationChannel;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.LauncherApps;
 import android.content.pm.PackageManager;
+import android.content.pm.ShortcutManager;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
@@ -228,6 +230,9 @@
                 initializeAppOpsInfo(row, (AppOpsInfo) gutsView);
             } else if (gutsView instanceof NotificationInfo) {
                 initializeNotificationInfo(row, (NotificationInfo) gutsView);
+            } else if (gutsView instanceof NotificationConversationInfo) {
+                initializeConversationNotificationInfo(
+                        row, (NotificationConversationInfo) gutsView);
             }
             return true;
         } catch (Exception e) {
@@ -339,6 +344,66 @@
     }
 
     /**
+     * Sets up the {@link NotificationConversationInfo} inside the notification row's guts.
+     * @param row view to set up the guts for
+     * @param notificationInfoView view to set up/bind within {@code row}
+     */
+    @VisibleForTesting
+    void initializeConversationNotificationInfo(
+            final ExpandableNotificationRow row,
+            NotificationConversationInfo notificationInfoView) throws Exception {
+        NotificationGuts guts = row.getGuts();
+        StatusBarNotification sbn = row.getEntry().getSbn();
+        String packageName = sbn.getPackageName();
+        // Settings link is only valid for notifications that specify a non-system user
+        NotificationConversationInfo.OnSettingsClickListener onSettingsClick = null;
+        UserHandle userHandle = sbn.getUser();
+        PackageManager pmUser = StatusBar.getPackageManagerForUser(
+                mContext, userHandle.getIdentifier());
+        LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
+        ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
+        INotificationManager iNotificationManager = INotificationManager.Stub.asInterface(
+                ServiceManager.getService(Context.NOTIFICATION_SERVICE));
+        final NotificationConversationInfo.OnAppSettingsClickListener onAppSettingsClick =
+                (View v, Intent intent) -> {
+                    mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_APP_NOTE_SETTINGS);
+                    guts.resetFalsingCheck();
+                    mNotificationActivityStarter.startNotificationGutsIntent(intent, sbn.getUid(),
+                            row);
+                };
+
+        final NotificationConversationInfo.OnSnoozeClickListener onSnoozeClickListener =
+                (View v, int hours) -> {
+                    mListContainer.getSwipeActionHelper().snooze(sbn, hours);
+                };
+
+        if (!userHandle.equals(UserHandle.ALL)
+                || mLockscreenUserManager.getCurrentUserId() == UserHandle.USER_SYSTEM) {
+            onSettingsClick = (View v, NotificationChannel channel, int appUid) -> {
+                mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_NOTE_INFO);
+                guts.resetFalsingCheck();
+                mOnSettingsClickListener.onSettingsClick(sbn.getKey());
+                startAppNotificationSettingsActivity(packageName, appUid, channel, row);
+            };
+        }
+
+        notificationInfoView.bindNotification(
+                shortcutManager,
+                launcherApps,
+                pmUser,
+                iNotificationManager,
+                mVisualStabilityManager,
+                packageName,
+                row.getEntry().getChannel(),
+                row.getEntry(),
+                onSettingsClick,
+                onAppSettingsClick,
+                onSnoozeClickListener,
+                mDeviceProvisionedController.isDeviceProvisioned());
+
+    }
+
+    /**
      * Closes guts or notification menus that might be visible and saves any changes.
      *
      * @param removeLeavebehinds true if leavebehinds (e.g. snooze) should be closed.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
index edfd1b4..212cba6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
@@ -83,7 +83,6 @@
     private OnMenuEventListener mMenuListener;
     private boolean mDismissRtl;
     private boolean mIsForeground;
-    private final boolean mIsUsingBidirectionalSwipe;
 
     private ValueAnimator mFadeAnimator;
     private boolean mAnimating;
@@ -116,19 +115,11 @@
     private boolean mIsUserTouching;
 
     public NotificationMenuRow(Context context) {
-        //TODO: (b/131242807) not using bidirectional swipe for now
-        this(context, false);
-    }
-
-    // Only needed for testing until we want to turn bidirectional swipe back on
-    @VisibleForTesting
-    NotificationMenuRow(Context context, boolean isUsingBidirectionalSwipe) {
         mContext = context;
         mShouldShowMenu = context.getResources().getBoolean(R.bool.config_showNotificationGear);
         mHandler = new Handler(Looper.getMainLooper());
         mLeftMenuItems = new ArrayList<>();
         mRightMenuItems = new ArrayList<>();
-        mIsUsingBidirectionalSwipe = isUsingBidirectionalSwipe;
     }
 
     @Override
@@ -269,24 +260,18 @@
             mSnoozeItem = createSnoozeItem(mContext);
         }
         mAppOpsItem = createAppOpsItem(mContext);
-        if (mIsUsingBidirectionalSwipe) {
-            mInfoItem = createInfoItem(mContext,
-                    mParent.getEntry().getBucket() == NotificationSectionsManager.BUCKET_SILENT);
+        if (mParent.getEntry().getBucket() == NotificationSectionsManager.BUCKET_PEOPLE) {
+            mInfoItem = createConversationItem(mContext);
         } else {
             mInfoItem = createInfoItem(mContext);
         }
 
-        if (!mIsUsingBidirectionalSwipe) {
-            if (!isForeground && showSnooze) {
-                mRightMenuItems.add(mSnoozeItem);
-            }
-            mRightMenuItems.add(mInfoItem);
-            mRightMenuItems.add(mAppOpsItem);
-            mLeftMenuItems.addAll(mRightMenuItems);
-        } else {
-            ArrayList<MenuItem> menuItems = mDismissRtl ? mLeftMenuItems : mRightMenuItems;
-            menuItems.add(mInfoItem);
+        if (!isForeground && showSnooze) {
+            mRightMenuItems.add(mSnoozeItem);
         }
+        mRightMenuItems.add(mInfoItem);
+        mRightMenuItems.add(mAppOpsItem);
+        mLeftMenuItems.addAll(mRightMenuItems);
 
         populateMenuViews();
         if (resetState) {
@@ -633,12 +618,12 @@
 
     @Override
     public boolean shouldShowGutsOnSnapOpen() {
-        return mIsUsingBidirectionalSwipe;
+        return false;
     }
 
     @Override
     public MenuItem menuItemToExposeOnSnap() {
-        return mIsUsingBidirectionalSwipe ? mInfoItem : null;
+        return null;
     }
 
     @Override
@@ -664,6 +649,16 @@
         return snooze;
     }
 
+    static NotificationMenuItem createConversationItem(Context context) {
+        Resources res = context.getResources();
+        String infoDescription = res.getString(R.string.notification_menu_gear_description);
+        NotificationConversationInfo infoContent =
+                (NotificationConversationInfo) LayoutInflater.from(context).inflate(
+                        R.layout.notification_conversation_info, null, false);
+        return new NotificationMenuItem(context, infoDescription, infoContent,
+                R.drawable.ic_settings);
+    }
+
     static NotificationMenuItem createInfoItem(Context context) {
         Resources res = context.getResources();
         String infoDescription = res.getString(R.string.notification_menu_gear_description);
@@ -673,17 +668,6 @@
                 R.drawable.ic_settings);
     }
 
-    static NotificationMenuItem createInfoItem(Context context, boolean isCurrentlySilent) {
-        Resources res = context.getResources();
-        String infoDescription = res.getString(R.string.notification_menu_gear_description);
-        NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate(
-                R.layout.notification_info, null, false);
-        int iconResId = isCurrentlySilent
-                ? R.drawable.ic_notifications_silence
-                : R.drawable.ic_notifications_alert;
-        return new NotificationMenuItem(context, infoDescription, infoContent, iconResId);
-    }
-
     static MenuItem createAppOpsItem(Context context) {
         AppOpsInfo appOpsContent = (AppOpsInfo) LayoutInflater.from(context).inflate(
                 R.layout.app_ops_info, null, false);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 823dd5a..dc2d99c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -6207,6 +6207,11 @@
         }
 
         @Override
+        public void onSnooze(StatusBarNotification sbn, int hours) {
+            mStatusBar.setNotificationSnoozed(sbn, hours);
+        }
+
+        @Override
         public boolean shouldDismissQuickly() {
             return NotificationStackScrollLayout.this.isExpanded() && mAmbientState.isFullyAwake();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
index 4845ea1..6c0655e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
@@ -282,6 +282,11 @@
         mCallback.onSnooze(sbn, snoozeOption);
     }
 
+    @Override
+    public void snooze(StatusBarNotification sbn, int hours) {
+        mCallback.onSnooze(sbn, hours);
+    }
+
     @VisibleForTesting
     protected void handleMenuCoveredOrDismissed() {
         View exposedMenuView = getExposedMenuView();
@@ -447,6 +452,8 @@
 
         void onSnooze(StatusBarNotification sbn, SnoozeOption snoozeOption);
 
+        void onSnooze(StatusBarNotification sbn, int hours);
+
         void onDismiss();
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index ba70cf4..dc9cf77 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -4026,6 +4026,11 @@
         }
     }
 
+    public void setNotificationSnoozed(StatusBarNotification sbn, int hoursToSnooze) {
+        mNotificationListener.snoozeNotification(sbn.getKey(),
+                hoursToSnooze * 60 * 60 * 1000);
+    }
+
     @Override
     public void toggleSplitScreen() {
         toggleSplitScreenMode(-1 /* metricsDockAction */, -1 /* metricsUndockAction */);
diff --git a/packages/SystemUI/src/com/android/systemui/util/LifecycleActivity.kt b/packages/SystemUI/src/com/android/systemui/util/LifecycleActivity.kt
new file mode 100644
index 0000000..e4b7a20
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/LifecycleActivity.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2019 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.util
+
+import android.app.Activity
+import android.os.Bundle
+import android.os.PersistableBundle
+import androidx.lifecycle.LifecycleOwner
+import com.android.settingslib.core.lifecycle.Lifecycle
+
+open class LifecycleActivity : Activity(), LifecycleOwner {
+
+    private val lifecycle = Lifecycle(this)
+
+    override fun getLifecycle() = lifecycle
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        lifecycle.onAttach(this)
+        lifecycle.onCreate(savedInstanceState)
+        lifecycle.handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_CREATE)
+        super.onCreate(savedInstanceState)
+    }
+
+    override fun onCreate(
+        savedInstanceState: Bundle?,
+        persistentState: PersistableBundle?
+    ) {
+        lifecycle.onAttach(this)
+        lifecycle.onCreate(savedInstanceState)
+        lifecycle.handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_CREATE)
+        super.onCreate(savedInstanceState, persistentState)
+    }
+
+    override fun onStart() {
+        lifecycle.handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_START)
+        super.onStart()
+    }
+
+    override fun onResume() {
+        lifecycle.handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_RESUME)
+        super.onResume()
+    }
+
+    override fun onPause() {
+        lifecycle.handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_PAUSE)
+        super.onPause()
+    }
+
+    override fun onStop() {
+        lifecycle.handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_STOP)
+        super.onStop()
+    }
+
+    override fun onDestroy() {
+        lifecycle.handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_DESTROY)
+        super.onDestroy()
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt
index cca76bd..8a1759d 100644
--- a/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt
@@ -27,6 +27,9 @@
 import androidx.dynamicanimation.animation.SpringForce
 import com.android.systemui.util.animation.PhysicsAnimator.Companion.getInstance
 import java.util.WeakHashMap
+import kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.min
 
 /**
  * Extension function for all objects which will return a PhysicsAnimator instance for that object.
@@ -35,6 +38,15 @@
 
 private const val TAG = "PhysicsAnimator"
 
+private val UNSET = -Float.MAX_VALUE
+
+/**
+ * [FlingAnimation] multiplies the friction set via [FlingAnimation.setFriction] by 4.2f, which is
+ * where this number comes from. We use it in [PhysicsAnimator.flingThenSpring] to calculate the
+ * minimum velocity for a fling to reach a certain value, given the fling's friction.
+ */
+private const val FLING_FRICTION_SCALAR_MULTIPLIER = 4.2f
+
 typealias EndAction = () -> Unit
 
 /** A map of Property -> AnimationUpdate, which is provided to update listeners on each frame. */
@@ -236,6 +248,71 @@
     }
 
     /**
+     * Flings a property using the given start velocity. If the fling animation reaches the min/max
+     * bounds (from the [flingConfig]) with velocity remaining, it'll overshoot it and spring back.
+     *
+     * If the object is already out of the fling bounds, it will immediately spring back within
+     * bounds.
+     *
+     * This is useful for animating objects that are bounded by constraints such as screen edges,
+     * since otherwise the fling animation would end abruptly upon reaching the min/max bounds.
+     *
+     * @param property The property to animate.
+     * @param startVelocity The velocity, in pixels/second, with which to start the fling. If the
+     * object is already outside the fling bounds, this velocity will be used as the start velocity
+     * of the spring that will spring it back within bounds.
+     * @param flingMustReachMinOrMax If true, the fling animation is guaranteed to reach either its
+     * minimum bound (if [startVelocity] is negative) or maximum bound (if it's positive). The
+     * animator will use startVelocity if it's sufficient, or add more velocity if necessary. This
+     * is useful when fling's deceleration-based physics are preferable to the acceleration-based
+     * forces used by springs - typically, when you're allowing the user to move an object somewhere
+     * on the screen, but it needs to be along an edge.
+     * @param flingConfig The configuration to use for the fling portion of the animation.
+     * @param springConfig The configuration to use for the spring portion of the animation.
+     */
+    @JvmOverloads
+    fun flingThenSpring(
+        property: FloatPropertyCompat<in T>,
+        startVelocity: Float,
+        flingConfig: FlingConfig,
+        springConfig: SpringConfig,
+        flingMustReachMinOrMax: Boolean = false
+    ): PhysicsAnimator<T> {
+        val flingConfigCopy = flingConfig.copy()
+        val springConfigCopy = springConfig.copy()
+        val toAtLeast = if (startVelocity < 0) flingConfig.min else flingConfig.max
+
+        // If the fling needs to reach min/max, calculate the velocity required to do so and use
+        // that if the provided start velocity is not sufficient.
+        if (flingMustReachMinOrMax &&
+                toAtLeast != -Float.MAX_VALUE && toAtLeast != Float.MAX_VALUE) {
+            val distanceToDestination = toAtLeast - property.getValue(target)
+
+            // The minimum velocity required for the fling to end up at the given destination,
+            // taking the provided fling friction value.
+            val velocityToReachDestination = distanceToDestination *
+                    (flingConfig.friction * FLING_FRICTION_SCALAR_MULTIPLIER)
+
+            // Try to use the provided start velocity, but use the required velocity to reach the
+            // destination if the provided velocity is insufficient.
+            val sufficientVelocity =
+                    if (distanceToDestination < 0)
+                        min(velocityToReachDestination, startVelocity)
+                    else
+                        max(velocityToReachDestination, startVelocity)
+
+            flingConfigCopy.startVelocity = sufficientVelocity
+            springConfigCopy.finalPosition = toAtLeast
+        } else {
+            flingConfigCopy.startVelocity = startVelocity
+        }
+
+        flingConfigs[property] = flingConfigCopy
+        springConfigs[property] = springConfigCopy
+        return this
+    }
+
+    /**
      * Adds a listener that will be called whenever any property on the animated object is updated.
      * This will be called on every animation frame, with the current value of the animated object
      * and the new property values.
@@ -246,7 +323,7 @@
     }
 
     /**
-     * Adds a listener that will be called whenever a property's animation ends. This is useful if
+     * Adds a listener that will be called when a property stops animating. This is useful if
      * you care about a specific property ending, or want to use the end value/end velocity from a
      * particular property's animation. If you just want to run an action when all property
      * animations have ended, use [withEndActions].
@@ -311,6 +388,114 @@
                     "your test setup.")
         }
 
+        // Functions that will actually start the animations. These are run after we build and add
+        // the InternalListener, since some animations might update/end immediately and we don't
+        // want to miss those updates.
+        val animationStartActions = ArrayList<() -> Unit>()
+
+        for (animatedProperty in getAnimatedProperties()) {
+            val flingConfig = flingConfigs[animatedProperty]
+            val springConfig = springConfigs[animatedProperty]
+
+            // The property's current value on the object.
+            val currentValue = animatedProperty.getValue(target)
+
+            // Start by checking for a fling configuration. If one is present, we're either flinging
+            // or flinging-then-springing. Either way, we'll want to start the fling first.
+            if (flingConfig != null) {
+                animationStartActions.add {
+                    // When the animation is starting, adjust the min/max bounds to include the
+                    // current value of the property, if necessary. This is required to allow a
+                    // fling to bring an out-of-bounds object back into bounds. For example, if an
+                    // object was dragged halfway off the left side of the screen, but then flung to
+                    // the right, we don't want the animation to end instantly just because the
+                    // object started out of bounds. If the fling is in the direction that would
+                    // take it farther out of bounds, it will end instantly as expected.
+                    flingConfig.apply {
+                        min = min(currentValue, this.min)
+                        max = max(currentValue, this.max)
+                    }
+
+                    // Apply the configuration and start the animation.
+                    getFlingAnimation(animatedProperty)
+                            .also { flingConfig.applyToAnimation(it) }
+                            .start()
+                }
+            }
+
+            // Check for a spring configuration. If one is present, we're either springing, or
+            // flinging-then-springing.
+            if (springConfig != null) {
+
+                // If there is no corresponding fling config, we're only springing.
+                if (flingConfig == null) {
+                    // Apply the configuration and start the animation.
+                    val springAnim = getSpringAnimation(animatedProperty)
+                    springConfig.applyToAnimation(springAnim)
+                    animationStartActions.add(springAnim::start)
+                } else {
+                    // If there's a corresponding fling config, we're flinging-then-springing. Save
+                    // the fling's original bounds so we can spring to them when the fling ends.
+                    val flingMin = flingConfig.min
+                    val flingMax = flingConfig.max
+
+                    // Add an end listener that will start the spring when the fling ends.
+                    endListeners.add(0, object : EndListener<T> {
+                        override fun onAnimationEnd(
+                            target: T,
+                            property: FloatPropertyCompat<in T>,
+                            wasFling: Boolean,
+                            canceled: Boolean,
+                            finalValue: Float,
+                            finalVelocity: Float,
+                            allRelevantPropertyAnimsEnded: Boolean
+                        ) {
+                            // If this isn't the relevant property, it wasn't a fling, or the fling
+                            // was explicitly cancelled, don't spring.
+                            if (property != animatedProperty || !wasFling || canceled) {
+                                return
+                            }
+
+                            val endedWithVelocity = abs(finalVelocity) > 0
+
+                            // If the object was out of bounds when the fling animation started, it
+                            // will immediately end. In that case, we'll spring it back in bounds.
+                            val endedOutOfBounds = finalValue !in flingMin..flingMax
+
+                            // If the fling ended either out of bounds or with remaining velocity,
+                            // it's time to spring.
+                            if (endedWithVelocity || endedOutOfBounds) {
+                                springConfig.startVelocity = finalVelocity
+
+                                // If the spring's final position isn't set, this is a
+                                // flingThenSpring where flingMustReachMinOrMax was false. We'll
+                                // need to set the spring's final position here.
+                                if (springConfig.finalPosition == UNSET) {
+                                    if (endedWithVelocity) {
+                                        // If the fling ended with negative velocity, that means it
+                                        // hit the min bound, so spring to that bound (and vice
+                                        // versa).
+                                        springConfig.finalPosition =
+                                                if (finalVelocity < 0) flingMin else flingMax
+                                    } else if (endedOutOfBounds) {
+                                        // If the fling ended out of bounds, spring it to the
+                                        // nearest bound.
+                                        springConfig.finalPosition =
+                                                if (finalValue < flingMin) flingMin else flingMax
+                                    }
+                                }
+
+                                // Apply the configuration and start the spring animation.
+                                getSpringAnimation(animatedProperty)
+                                        .also { springConfig.applyToAnimation(it) }
+                                        .start()
+                            }
+                        }
+                    })
+                }
+            }
+        }
+
         // Add an internal listener that will dispatch animation events to the provided listeners.
         internalListeners.add(InternalListener(
                 getAnimatedProperties(),
@@ -318,24 +503,10 @@
                 ArrayList(endListeners),
                 ArrayList(endActions)))
 
-        for ((property, config) in flingConfigs) {
-            val currentValue = property.getValue(target)
-
-            // If the fling is already out of bounds, don't start it.
-            if (currentValue <= config.min || currentValue >= config.max) {
-                continue
-            }
-
-            val flingAnim = getFlingAnimation(property)
-            config.applyToAnimation(flingAnim)
-            flingAnim.start()
-        }
-
-        for ((property, config) in springConfigs) {
-            val springAnim = getSpringAnimation(property)
-            config.applyToAnimation(springAnim)
-            springAnim.start()
-        }
+        // Actually start the DynamicAnimations. This is delayed until after the InternalListener is
+        // constructed and added so that we don't miss the end listener firing for any animations
+        // that immediately end.
+        animationStartActions.forEach { it.invoke() }
 
         clearAnimator()
     }
@@ -381,7 +552,10 @@
         }
         anim.addEndListener { _, canceled, value, velocity ->
             internalListeners.removeAll {
-                it.onInternalAnimationEnd(property, canceled, value, velocity) } }
+                it.onInternalAnimationEnd(
+                        property, canceled, value, velocity, anim is FlingAnimation)
+            }
+        }
         return anim
     }
 
@@ -434,7 +608,8 @@
             property: FloatPropertyCompat<in T>,
             canceled: Boolean,
             finalValue: Float,
-            finalVelocity: Float
+            finalVelocity: Float,
+            isFling: Boolean
         ): Boolean {
 
             // If this property animation isn't relevant to this listener, ignore it.
@@ -461,7 +636,15 @@
 
             val allEnded = !arePropertiesAnimating(properties)
             endListeners.forEach {
-                it.onAnimationEnd(target, property, canceled, finalValue, finalVelocity, allEnded) }
+                it.onAnimationEnd(
+                        target, property, isFling, canceled, finalValue, finalVelocity,
+                        allEnded)
+
+                // Check that the end listener didn't restart this property's animation.
+                if (isPropertyAnimating(property)) {
+                    return false
+                }
+            }
 
             // If all of the animations that this listener cares about have ended, run the end
             // actions unless the animation was canceled.
@@ -495,7 +678,8 @@
 
     /** Returns whether the given property is animating.  */
     fun isPropertyAnimating(property: FloatPropertyCompat<in T>): Boolean {
-        return springAnimations[property]?.isRunning ?: false
+        return springAnimations[property]?.isRunning ?: false ||
+                flingAnimations[property]?.isRunning ?: false
     }
 
     /** Returns whether any of the given properties are animating.  */
@@ -523,15 +707,15 @@
     data class SpringConfig internal constructor(
         internal var stiffness: Float,
         internal var dampingRatio: Float,
-        internal var startVel: Float = 0f,
-        internal var finalPosition: Float = -Float.MAX_VALUE
+        internal var startVelocity: Float = 0f,
+        internal var finalPosition: Float = UNSET
     ) {
 
         constructor() :
                 this(defaultSpring.stiffness, defaultSpring.dampingRatio)
 
         constructor(stiffness: Float, dampingRatio: Float) :
-                this(stiffness = stiffness, dampingRatio = dampingRatio, startVel = 0f)
+                this(stiffness = stiffness, dampingRatio = dampingRatio, startVelocity = 0f)
 
         /** Apply these configuration settings to the given SpringAnimation. */
         internal fun applyToAnimation(anim: SpringAnimation) {
@@ -542,7 +726,7 @@
                 finalPosition = this@SpringConfig.finalPosition
             }
 
-            if (startVel != 0f) anim.setStartVelocity(startVel)
+            if (startVelocity != 0f) anim.setStartVelocity(startVelocity)
         }
     }
 
@@ -555,7 +739,7 @@
         internal var friction: Float,
         internal var min: Float,
         internal var max: Float,
-        internal var startVel: Float
+        internal var startVelocity: Float
     ) {
 
         constructor() : this(defaultFling.friction)
@@ -564,7 +748,7 @@
                 this(friction, defaultFling.min, defaultFling.max)
 
         constructor(friction: Float, min: Float, max: Float) :
-                this(friction, min, max, startVel = 0f)
+                this(friction, min, max, startVelocity = 0f)
 
         /** Apply these configuration settings to the given FlingAnimation. */
         internal fun applyToAnimation(anim: FlingAnimation) {
@@ -572,7 +756,7 @@
                 friction = this@FlingConfig.friction
                 setMinValue(min)
                 setMaxValue(max)
-                setStartVelocity(startVel)
+                setStartVelocity(startVelocity)
             }
         }
     }
@@ -625,6 +809,10 @@
          *
          * @param target The animated object itself.
          * @param property The property whose animation has just ended.
+         * @param wasFling Whether this property ended after a fling animation (as opposed to a
+         * spring animation). If this property was animated via [flingThenSpring], this will be true
+         * if the fling animation did not reach the min/max bounds, decelerating to a stop
+         * naturally. It will be false if it hit the bounds and was sprung back.
          * @param canceled Whether the animation was explicitly canceled before it naturally ended.
          * @param finalValue The final value of the animated property.
          * @param finalVelocity The final velocity (in pixels per second) of the ended animation.
@@ -662,6 +850,7 @@
         fun onAnimationEnd(
             target: T,
             property: FloatPropertyCompat<in T>,
+            wasFling: Boolean,
             canceled: Boolean,
             finalValue: Float,
             finalVelocity: Float,
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimatorTestUtils.kt b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimatorTestUtils.kt
index e86970c..965decd 100644
--- a/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimatorTestUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimatorTestUtils.kt
@@ -19,6 +19,7 @@
 import android.os.Looper
 import android.util.ArrayMap
 import androidx.dynamicanimation.animation.FloatPropertyCompat
+import com.android.systemui.util.animation.PhysicsAnimatorTestUtils.prepareForTest
 import java.util.ArrayDeque
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
@@ -119,6 +120,7 @@
                 override fun onAnimationEnd(
                     target: T,
                     property: FloatPropertyCompat<in T>,
+                    wasFling: Boolean,
                     canceled: Boolean,
                     finalValue: Float,
                     finalVelocity: Float,
@@ -389,8 +391,6 @@
             val unblockLatch = CountDownLatch(if (startBlocksUntilAnimationsEnd) 2 else 1)
 
             animationThreadHandler.post {
-                val animatedProperties = animator.getAnimatedProperties()
-
                 // Add an update listener that dispatches to any test update listeners added by
                 // tests.
                 animator.addUpdateListener(object : PhysicsAnimator.UpdateListener<T> {
@@ -398,6 +398,10 @@
                         target: T,
                         values: ArrayMap<FloatPropertyCompat<in T>, PhysicsAnimator.AnimationUpdate>
                     ) {
+                        values.forEach { (property, value) ->
+                            allUpdates.getOrPut(property, { ArrayList() }).add(value)
+                        }
+
                         for (listener in testUpdateListeners) {
                             listener.onAnimationUpdateForProperty(target, values)
                         }
@@ -410,6 +414,7 @@
                     override fun onAnimationEnd(
                         target: T,
                         property: FloatPropertyCompat<in T>,
+                        wasFling: Boolean,
                         canceled: Boolean,
                         finalValue: Float,
                         finalVelocity: Float,
@@ -417,7 +422,7 @@
                     ) {
                         for (listener in testEndListeners) {
                             listener.onAnimationEnd(
-                                    target, property, canceled, finalValue, finalVelocity,
+                                    target, property, wasFling, canceled, finalValue, finalVelocity,
                                     allRelevantPropertyAnimsEnded)
                         }
 
@@ -432,31 +437,6 @@
                     }
                 })
 
-                val updateListeners = ArrayList<PhysicsAnimator.UpdateListener<T>>().also {
-                    it.add(object : PhysicsAnimator.UpdateListener<T> {
-                        override fun onAnimationUpdateForProperty(
-                            target: T,
-                            values: ArrayMap<FloatPropertyCompat<in T>,
-                                             PhysicsAnimator.AnimationUpdate>
-                        ) {
-                            values.forEach { (property, value) ->
-                                allUpdates.getOrPut(property, { ArrayList() }).add(value)
-                            }
-                        }
-                    })
-                }
-
-                /**
-                 * Add an internal listener at the head of the list that captures update values
-                 * directly from DynamicAnimation. We use this to build a list of all updates so we
-                 * can verify that InternalListener dispatches to the real listeners properly.
-                 */
-                animator.internalListeners.add(0, animator.InternalListener(
-                        animatedProperties,
-                        updateListeners,
-                        ArrayList(),
-                        ArrayList()))
-
                 animator.startInternal()
                 unblockLatch.countDown()
             }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt
new file mode 100644
index 0000000..7c8c7c8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2020 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.controls.controller
+
+import android.content.ComponentName
+import android.content.Context
+import android.os.Binder
+import android.service.controls.Control
+import android.service.controls.DeviceTypes
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import dagger.Lazy
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ControlsBindingControllerTest : SysuiTestCase() {
+
+    companion object {
+        fun <T> any(): T = Mockito.any<T>()
+        private val TEST_COMPONENT_NAME_1 = ComponentName("TEST_PKG", "TEST_CLS_1")
+        private val TEST_COMPONENT_NAME_2 = ComponentName("TEST_PKG", "TEST_CLS_2")
+        private val TEST_COMPONENT_NAME_3 = ComponentName("TEST_PKG", "TEST_CLS_3")
+    }
+
+    @Mock
+    private lateinit var mockControlsController: ControlsController
+
+    private val executor = FakeExecutor(FakeSystemClock())
+    private lateinit var controller: ControlsBindingController
+    private val providers = TestableControlsBindingControllerImpl.providers
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        controller = TestableControlsBindingControllerImpl(
+                mContext, executor, Lazy { mockControlsController })
+    }
+
+    @After
+    fun tearDown() {
+        executor.advanceClockToLast()
+        executor.runAllReady()
+        providers.clear()
+    }
+
+    @Test
+    fun testBindAndLoad() {
+        val callback: (List<Control>) -> Unit = {}
+        controller.bindAndLoad(TEST_COMPONENT_NAME_1, callback)
+
+        assertEquals(1, providers.size)
+        val provider = providers.first()
+        verify(provider).maybeBindAndLoad(callback)
+    }
+
+    @Test
+    fun testBindServices() {
+        controller.bindServices(listOf(TEST_COMPONENT_NAME_1, TEST_COMPONENT_NAME_2))
+        executor.runAllReady()
+
+        assertEquals(2, providers.size)
+        assertEquals(setOf(TEST_COMPONENT_NAME_1, TEST_COMPONENT_NAME_2),
+                providers.map { it.componentName }.toSet())
+        providers.forEach {
+            verify(it).bindPermanently()
+        }
+    }
+
+    @Test
+    fun testSubscribe() {
+        val controlInfo1 = ControlInfo(TEST_COMPONENT_NAME_1, "id_1", "", DeviceTypes.TYPE_UNKNOWN)
+        val controlInfo2 = ControlInfo(TEST_COMPONENT_NAME_2, "id_2", "", DeviceTypes.TYPE_UNKNOWN)
+        controller.bindServices(listOf(TEST_COMPONENT_NAME_3))
+
+        controller.subscribe(listOf(controlInfo1, controlInfo2))
+
+        executor.runAllReady()
+
+        assertEquals(3, providers.size)
+        val provider1 = providers.first { it.componentName == TEST_COMPONENT_NAME_1 }
+        val provider2 = providers.first { it.componentName == TEST_COMPONENT_NAME_2 }
+        val provider3 = providers.first { it.componentName == TEST_COMPONENT_NAME_3 }
+
+        verify(provider1).maybeBindAndSubscribe(listOf(controlInfo1.controlId))
+        verify(provider2).maybeBindAndSubscribe(listOf(controlInfo2.controlId))
+        verify(provider3, never()).maybeBindAndSubscribe(any())
+        verify(provider3).unbindService() // Not needed services will be unbound
+    }
+
+    @Test
+    fun testUnsubscribe_notRefreshing() {
+        controller.bindServices(listOf(TEST_COMPONENT_NAME_1, TEST_COMPONENT_NAME_2))
+        controller.unsubscribe()
+
+        executor.runAllReady()
+
+        providers.forEach {
+            verify(it, never()).unsubscribe()
+        }
+    }
+
+    @Test
+    fun testUnsubscribe_refreshing() {
+        val controlInfo1 = ControlInfo(TEST_COMPONENT_NAME_1, "id_1", "", DeviceTypes.TYPE_UNKNOWN)
+        val controlInfo2 = ControlInfo(TEST_COMPONENT_NAME_2, "id_2", "", DeviceTypes.TYPE_UNKNOWN)
+
+        controller.subscribe(listOf(controlInfo1, controlInfo2))
+
+        controller.unsubscribe()
+
+        executor.runAllReady()
+
+        providers.forEach {
+            verify(it).unsubscribe()
+        }
+    }
+}
+
+class TestableControlsBindingControllerImpl(
+    context: Context,
+    executor: DelayableExecutor,
+    lazyController: Lazy<ControlsController>
+) : ControlsBindingControllerImpl(context, executor, lazyController) {
+
+    companion object {
+        val providers = mutableSetOf<ControlsProviderLifecycleManager>()
+    }
+
+    override fun createProviderManager(component: ComponentName):
+            ControlsProviderLifecycleManager {
+        val provider = mock(ControlsProviderLifecycleManager::class.java)
+        val token = Binder()
+        `when`(provider.componentName).thenReturn(component)
+        `when`(provider.token).thenReturn(token)
+        providers.add(provider)
+        return provider
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
new file mode 100644
index 0000000..a19c299
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2020 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.controls.controller
+
+import android.app.PendingIntent
+import android.content.ComponentName
+import android.provider.Settings
+import android.service.controls.Control
+import android.service.controls.DeviceTypes
+import android.service.controls.actions.ControlAction
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.DumpController
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.ControlStatus
+import com.android.systemui.controls.ui.ControlsUiController
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import java.util.Optional
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ControlsControllerImplTest : SysuiTestCase() {
+
+    @Mock
+    private lateinit var uiController: ControlsUiController
+    @Mock
+    private lateinit var bindingController: ControlsBindingController
+    @Mock
+    private lateinit var dumpController: DumpController
+    @Mock
+    private lateinit var pendingIntent: PendingIntent
+    @Mock
+    private lateinit var persistenceWrapper: ControlsFavoritePersistenceWrapper
+
+    @Captor
+    private lateinit var controlInfoListCaptor: ArgumentCaptor<List<ControlInfo>>
+    @Captor
+    private lateinit var controlLoadCallbackCaptor: ArgumentCaptor<(List<Control>) -> Unit>
+
+    private lateinit var delayableExecutor: FakeExecutor
+    private lateinit var controller: ControlsController
+
+    companion object {
+        fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+        fun <T : Any> safeEq(value: T): T = eq(value) ?: value
+
+        private val TEST_COMPONENT = ComponentName("test.pkg", "test.class")
+        private const val TEST_CONTROL_ID = "control1"
+        private const val TEST_CONTROL_TITLE = "Test"
+        private const val TEST_DEVICE_TYPE = DeviceTypes.TYPE_AC_HEATER
+        private val TEST_CONTROL_INFO = ControlInfo(
+                TEST_COMPONENT, TEST_CONTROL_ID, TEST_CONTROL_TITLE, TEST_DEVICE_TYPE)
+
+        private val TEST_COMPONENT_2 = ComponentName("test.pkg", "test.class.2")
+        private const val TEST_CONTROL_ID_2 = "control2"
+        private const val TEST_CONTROL_TITLE_2 = "Test 2"
+        private const val TEST_DEVICE_TYPE_2 = DeviceTypes.TYPE_CAMERA
+        private val TEST_CONTROL_INFO_2 = ControlInfo(
+                TEST_COMPONENT_2, TEST_CONTROL_ID_2, TEST_CONTROL_TITLE_2, TEST_DEVICE_TYPE_2)
+    }
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        Settings.Secure.putInt(mContext.contentResolver,
+                ControlsControllerImpl.CONTROLS_AVAILABLE, 1)
+
+        delayableExecutor = FakeExecutor(FakeSystemClock())
+
+        controller = ControlsControllerImpl(
+                mContext,
+                delayableExecutor,
+                uiController,
+                bindingController,
+                Optional.of(persistenceWrapper),
+                dumpController
+        )
+        assertTrue(controller.available)
+    }
+
+    private fun builderFromInfo(controlInfo: ControlInfo): Control.StatelessBuilder {
+        return Control.StatelessBuilder(controlInfo.controlId, pendingIntent)
+                .setDeviceType(controlInfo.deviceType).setTitle(controlInfo.controlTitle)
+    }
+
+    @Test
+    fun testStartWithoutFavorites() {
+        assertTrue(controller.getFavoriteControls().isEmpty())
+    }
+
+    @Test
+    fun testStartWithSavedFavorites() {
+        `when`(persistenceWrapper.readFavorites()).thenReturn(listOf(TEST_CONTROL_INFO))
+        val controller_other = ControlsControllerImpl(
+                mContext,
+                delayableExecutor,
+                uiController,
+                bindingController,
+                Optional.of(persistenceWrapper),
+                dumpController
+        )
+        assertEquals(listOf(TEST_CONTROL_INFO), controller_other.getFavoriteControls())
+    }
+
+    @Test
+    fun testAddFavorite() {
+        controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
+
+        val favorites = controller.getFavoriteControls()
+        assertTrue(TEST_CONTROL_INFO in favorites)
+        assertEquals(1, favorites.size)
+    }
+
+    @Test
+    fun testAddMultipleFavorites() {
+        controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
+        controller.changeFavoriteStatus(TEST_CONTROL_INFO_2, true)
+
+        val favorites = controller.getFavoriteControls()
+        assertTrue(TEST_CONTROL_INFO in favorites)
+        assertTrue(TEST_CONTROL_INFO_2 in favorites)
+        assertEquals(2, favorites.size)
+    }
+
+    @Test
+    fun testAddAndRemoveFavorite() {
+        controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
+        controller.changeFavoriteStatus(TEST_CONTROL_INFO_2, true)
+        controller.changeFavoriteStatus(TEST_CONTROL_INFO, false)
+
+        val favorites = controller.getFavoriteControls()
+        assertTrue(TEST_CONTROL_INFO !in favorites)
+        assertTrue(TEST_CONTROL_INFO_2 in favorites)
+        assertEquals(1, favorites.size)
+    }
+
+    @Test
+    fun testFavoritesSavedOnAdd() {
+        controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
+
+        verify(persistenceWrapper).storeFavorites(listOf(TEST_CONTROL_INFO))
+    }
+
+    @Test
+    fun testFavoritesSavedOnRemove() {
+        controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
+        reset(persistenceWrapper)
+
+        controller.changeFavoriteStatus(TEST_CONTROL_INFO, false)
+        verify(persistenceWrapper).storeFavorites(emptyList())
+    }
+
+    @Test
+    fun testFavoritesSavedOnChange() {
+        controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
+        val newControlInfo = TEST_CONTROL_INFO.copy(controlTitle = TEST_CONTROL_TITLE_2)
+        val control = builderFromInfo(newControlInfo).build()
+
+        controller.loadForComponent(TEST_COMPONENT) {}
+
+        reset(persistenceWrapper)
+        verify(bindingController).bindAndLoad(safeEq(TEST_COMPONENT),
+                capture(controlLoadCallbackCaptor))
+
+        controlLoadCallbackCaptor.value.invoke(listOf(control))
+
+        verify(persistenceWrapper).storeFavorites(listOf(newControlInfo))
+    }
+
+    @Test
+    fun testFavoritesNotSavedOnRedundantAdd() {
+        controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
+
+        reset(persistenceWrapper)
+
+        controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
+        verify(persistenceWrapper, never()).storeFavorites(ArgumentMatchers.anyList())
+    }
+
+    @Test
+    fun testFavoritesNotSavedOnNotRemove() {
+        controller.changeFavoriteStatus(TEST_CONTROL_INFO, false)
+        verify(persistenceWrapper, never()).storeFavorites(ArgumentMatchers.anyList())
+    }
+
+    @Test
+    fun testOnActionResponse() {
+        controller.onActionResponse(TEST_COMPONENT, TEST_CONTROL_ID, ControlAction.RESPONSE_OK)
+
+        verify(uiController).onActionResponse(TEST_COMPONENT, TEST_CONTROL_ID,
+                ControlAction.RESPONSE_OK)
+    }
+
+    @Test
+    fun testRefreshStatus() {
+        val list = listOf(Control.StatefulBuilder(TEST_CONTROL_ID, pendingIntent).build())
+        controller.refreshStatus(TEST_COMPONENT, list)
+
+        verify(uiController).onRefreshState(TEST_COMPONENT, list)
+    }
+
+    @Test
+    fun testUnsubscribe() {
+        controller.unsubscribe()
+        verify(bindingController).unsubscribe()
+    }
+
+    @Test
+    fun testSubscribeFavorites() {
+        controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
+        controller.changeFavoriteStatus(TEST_CONTROL_INFO_2, true)
+
+        controller.subscribeToFavorites()
+
+        verify(bindingController).subscribe(capture(controlInfoListCaptor))
+
+        assertTrue(TEST_CONTROL_INFO in controlInfoListCaptor.value)
+        assertTrue(TEST_CONTROL_INFO_2 in controlInfoListCaptor.value)
+    }
+
+    @Test
+    fun testLoadForComponent_noFavorites() {
+        var loaded = false
+        val control = builderFromInfo(TEST_CONTROL_INFO).build()
+
+        controller.loadForComponent(TEST_COMPONENT) {
+            loaded = true
+            assertEquals(1, it.size)
+            val controlStatus = it[0]
+            assertEquals(ControlStatus(control, false), controlStatus)
+        }
+
+        verify(bindingController).bindAndLoad(safeEq(TEST_COMPONENT),
+                capture(controlLoadCallbackCaptor))
+
+        controlLoadCallbackCaptor.value.invoke(listOf(control))
+
+        assertTrue(loaded)
+    }
+
+    @Test
+    fun testLoadForComponent_favorites() {
+        var loaded = false
+        val control = builderFromInfo(TEST_CONTROL_INFO).build()
+        val control2 = builderFromInfo(TEST_CONTROL_INFO_2).build()
+        controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
+
+        controller.loadForComponent(TEST_COMPONENT) {
+            loaded = true
+            assertEquals(2, it.size)
+            val controlStatus = it.first { it.control.controlId == TEST_CONTROL_ID }
+            assertEquals(ControlStatus(control, true), controlStatus)
+
+            val controlStatus2 = it.first { it.control.controlId == TEST_CONTROL_ID_2 }
+            assertEquals(ControlStatus(control2, false), controlStatus2)
+        }
+
+        verify(bindingController).bindAndLoad(safeEq(TEST_COMPONENT),
+                capture(controlLoadCallbackCaptor))
+
+        controlLoadCallbackCaptor.value.invoke(listOf(control, control2))
+
+        assertTrue(loaded)
+    }
+
+    @Test
+    fun testLoadForComponent_removed() {
+        var loaded = false
+        controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
+
+        controller.loadForComponent(TEST_COMPONENT) {
+            loaded = true
+            assertEquals(1, it.size)
+            val controlStatus = it[0]
+            assertEquals(TEST_CONTROL_ID, controlStatus.control.controlId)
+            assertTrue(controlStatus.favorite)
+            assertTrue(controlStatus.removed)
+        }
+
+        verify(bindingController).bindAndLoad(safeEq(TEST_COMPONENT),
+                capture(controlLoadCallbackCaptor))
+
+        controlLoadCallbackCaptor.value.invoke(emptyList())
+
+        assertTrue(loaded)
+    }
+
+    @Test
+    fun testFavoriteInformationModifiedOnLoad() {
+        controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
+        val newControlInfo = TEST_CONTROL_INFO.copy(controlTitle = TEST_CONTROL_TITLE_2)
+        val control = builderFromInfo(newControlInfo).build()
+
+        controller.loadForComponent(TEST_COMPONENT) {}
+
+        verify(bindingController).bindAndLoad(safeEq(TEST_COMPONENT),
+                capture(controlLoadCallbackCaptor))
+
+        controlLoadCallbackCaptor.value.invoke(listOf(control))
+
+        val favorites = controller.getFavoriteControls()
+        assertEquals(1, favorites.size)
+        assertEquals(newControlInfo, favorites[0])
+    }
+
+    @Test
+    fun testFavoriteInformationModifiedOnRefresh() {
+        controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
+        val newControlInfo = TEST_CONTROL_INFO.copy(controlTitle = TEST_CONTROL_TITLE_2)
+        val control = builderFromInfo(newControlInfo).build()
+
+        controller.refreshStatus(TEST_COMPONENT, listOf(control))
+
+        delayableExecutor.runAllReady()
+
+        val favorites = controller.getFavoriteControls()
+        assertEquals(1, favorites.size)
+        assertEquals(newControlInfo, favorites[0])
+    }
+
+    @Test
+    fun testClearFavorites() {
+        controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
+        assertEquals(1, controller.getFavoriteControls().size)
+
+        controller.clearFavorites()
+        assertTrue(controller.getFavoriteControls().isEmpty())
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapperTest.kt
new file mode 100644
index 0000000..c145c1f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapperTest.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2020 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.controls.controller
+
+import android.content.ComponentName
+import android.service.controls.DeviceTypes
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.File
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ControlsFavoritePersistenceWrapperTest : SysuiTestCase() {
+
+    private lateinit var file: File
+
+    private val executor = FakeExecutor(FakeSystemClock())
+
+    private lateinit var wrapper: ControlsFavoritePersistenceWrapper
+
+    @Before
+    fun setUp() {
+        file = File.createTempFile("controls_favorites", ".temp")
+        wrapper = ControlsFavoritePersistenceWrapper(file, executor)
+    }
+
+    @After
+    fun tearDown() {
+        if (file.exists() ?: false) {
+            file.delete()
+        }
+    }
+
+    @Test
+    fun testSaveAndRestore() {
+        val controlInfo1 = ControlInfo(
+                ComponentName.unflattenFromString("TEST_PKG/.TEST_CLS_1")!!,
+                "id1", "name_1", DeviceTypes.TYPE_UNKNOWN)
+        val controlInfo2 = ControlInfo(
+                ComponentName.unflattenFromString("TEST_PKG/.TEST_CLS_2")!!,
+                "id2", "name_2", DeviceTypes.TYPE_GENERIC_ON_OFF)
+        val list = listOf(controlInfo1, controlInfo2)
+
+        wrapper.storeFavorites(list)
+
+        executor.runAllReady()
+
+        assertEquals(list, wrapper.readFavorites())
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt
new file mode 100644
index 0000000..556bb40
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2020 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.controls.controller
+
+import android.content.ComponentName
+import android.service.controls.Control
+import android.service.controls.IControlsProvider
+import android.service.controls.IControlsProviderCallback
+import android.service.controls.actions.ControlAction
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ControlsProviderLifecycleManagerTest : SysuiTestCase() {
+
+    @Mock
+    private lateinit var serviceCallback: IControlsProviderCallback.Stub
+    @Mock
+    private lateinit var service: IControlsProvider.Stub
+
+    private val componentName = ComponentName("test.pkg", "test.cls")
+    private lateinit var manager: ControlsProviderLifecycleManager
+    private lateinit var executor: DelayableExecutor
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        mContext.addMockService(componentName, service)
+        executor = FakeExecutor(FakeSystemClock())
+        `when`(service.asBinder()).thenCallRealMethod()
+        `when`(service.queryLocalInterface(ArgumentMatchers.anyString())).thenReturn(service)
+
+        manager = ControlsProviderLifecycleManager(
+                context,
+                executor,
+                serviceCallback,
+                componentName
+        )
+    }
+
+    @After
+    fun tearDown() {
+        manager.unbindService()
+    }
+
+    @Test
+    fun testBindService() {
+        manager.bindPermanently()
+        assertTrue(mContext.isBound(componentName))
+    }
+
+    @Test
+    fun testUnbindService() {
+        manager.bindPermanently()
+        manager.unbindService()
+        assertFalse(mContext.isBound(componentName))
+    }
+
+    @Test
+    fun testMaybeBindAndLoad() {
+        val callback: (List<Control>) -> Unit = {}
+        manager.maybeBindAndLoad(callback)
+
+        verify(service).load()
+
+        assertTrue(mContext.isBound(componentName))
+        assertEquals(callback, manager.lastLoadCallback)
+    }
+
+    @Test
+    fun testMaybeUnbind_bindingAndCallback() {
+        manager.maybeBindAndLoad {}
+
+        manager.maybeUnbindAndRemoveCallback()
+        assertFalse(mContext.isBound(componentName))
+        assertNull(manager.lastLoadCallback)
+    }
+
+    @Test
+    fun testUnsubscribe() {
+        manager.bindPermanently()
+        manager.unsubscribe()
+
+        verify(service).unsubscribe()
+    }
+
+    @Test
+    fun testMaybeBindAndSubscribe() {
+        val list = listOf("TEST_ID")
+        manager.maybeBindAndSubscribe(list)
+
+        assertTrue(mContext.isBound(componentName))
+        verify(service).subscribe(list)
+    }
+
+    @Test
+    fun testMaybeBindAndAction() {
+        val controlId = "TEST_ID"
+        val action = ControlAction.UNKNOWN_ACTION
+        manager.maybeBindAndSendAction(controlId, action)
+
+        assertTrue(mContext.isBound(componentName))
+        verify(service).onAction(controlId, action)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderServiceWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderServiceWrapperTest.kt
new file mode 100644
index 0000000..d6993c0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderServiceWrapperTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2020 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.controls.controller
+
+import android.os.RemoteException
+import android.service.controls.IControlsProvider
+import android.service.controls.actions.ControlAction
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.any
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ControlsProviderServiceWrapperTest : SysuiTestCase() {
+
+    @Mock
+    private lateinit var service: IControlsProvider
+
+    private val exception = RemoteException()
+
+    private lateinit var wrapper: ControlsProviderServiceWrapper
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        wrapper = ControlsProviderServiceWrapper(service)
+    }
+
+    @Test
+    fun testLoad_happyPath() {
+        val result = wrapper.load()
+
+        assertTrue(result)
+        verify(service).load()
+    }
+
+    @Test
+    fun testLoad_error() {
+        `when`(service.load()).thenThrow(exception)
+        val result = wrapper.load()
+
+        assertFalse(result)
+    }
+
+    @Test
+    fun testSubscribe_happyPath() {
+        val list = listOf("TEST_ID")
+        val result = wrapper.subscribe(list)
+
+        assertTrue(result)
+        verify(service).subscribe(list)
+    }
+
+    @Test
+    fun testSubscribe_error() {
+        `when`(service.subscribe(any())).thenThrow(exception)
+
+        val list = listOf("TEST_ID")
+        val result = wrapper.subscribe(list)
+
+        assertFalse(result)
+    }
+
+    @Test
+    fun testUnsubscribe_happyPath() {
+        val result = wrapper.unsubscribe()
+
+        assertTrue(result)
+        verify(service).unsubscribe()
+    }
+
+    @Test
+    fun testUnsubscribe_error() {
+        `when`(service.unsubscribe()).thenThrow(exception)
+        val result = wrapper.unsubscribe()
+
+        assertFalse(result)
+    }
+
+    @Test
+    fun testOnAction_happyPath() {
+        val id = "TEST_ID"
+        val action = ControlAction.UNKNOWN_ACTION
+
+        val result = wrapper.onAction(id, action)
+
+        assertTrue(result)
+        verify(service).onAction(id, action)
+    }
+
+    @Test
+    fun testOnAction_error() {
+        `when`(service.onAction(any(), any())).thenThrow(exception)
+
+        val id = "TEST_ID"
+        val action = ControlAction.UNKNOWN_ACTION
+
+        val result = wrapper.onAction(id, action)
+
+        assertFalse(result)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt
new file mode 100644
index 0000000..f09aab9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2020 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.controls.management
+
+import android.content.ComponentName
+import android.content.pm.ServiceInfo
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.settingslib.applications.ServiceListing
+import com.android.settingslib.widget.CandidateInfo
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ControlsListingControllerImplTest : SysuiTestCase() {
+
+    companion object {
+        private const val TEST_LABEL = "TEST_LABEL"
+        private const val TEST_PERMISSION = "permission"
+        fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+        fun <T> any(): T = Mockito.any<T>()
+    }
+
+    @Mock
+    private lateinit var mockSL: ServiceListing
+    @Mock
+    private lateinit var mockCallback: ControlsListingController.ControlsListingCallback
+    @Mock
+    private lateinit var mockCallbackOther: ControlsListingController.ControlsListingCallback
+    @Mock
+    private lateinit var serviceInfo: ServiceInfo
+    @Mock
+    private lateinit var componentName: ComponentName
+
+    private val executor = FakeExecutor(FakeSystemClock())
+
+    private lateinit var controller: ControlsListingControllerImpl
+
+    private var serviceListingCallbackCaptor =
+            ArgumentCaptor.forClass(ServiceListing.Callback::class.java)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        `when`(serviceInfo.componentName).thenReturn(componentName)
+
+        controller = ControlsListingControllerImpl(mContext, executor, mockSL)
+        verify(mockSL).addCallback(capture(serviceListingCallbackCaptor))
+    }
+
+    @After
+    fun tearDown() {
+        executor.advanceClockToLast()
+        executor.runAllReady()
+    }
+
+    @Test
+    fun testNoServices_notListening() {
+        assertTrue(controller.getCurrentServices().isEmpty())
+    }
+
+    @Test
+    fun testStartListening_onFirstCallback() {
+        controller.addCallback(mockCallback)
+        executor.runAllReady()
+
+        verify(mockSL).setListening(true)
+    }
+
+    @Test
+    fun testStartListening_onlyOnce() {
+        controller.addCallback(mockCallback)
+        controller.addCallback(mockCallbackOther)
+
+        executor.runAllReady()
+
+        verify(mockSL).setListening(true)
+    }
+
+    @Test
+    fun testStopListening_callbackRemoved() {
+        controller.addCallback(mockCallback)
+
+        executor.runAllReady()
+
+        controller.removeCallback(mockCallback)
+
+        executor.runAllReady()
+
+        verify(mockSL).setListening(false)
+    }
+
+    @Test
+    fun testStopListening_notWhileRemainingCallbacks() {
+        controller.addCallback(mockCallback)
+        controller.addCallback(mockCallbackOther)
+
+        executor.runAllReady()
+
+        controller.removeCallback(mockCallback)
+
+        executor.runAllReady()
+
+        verify(mockSL, never()).setListening(false)
+    }
+
+    @Test
+    fun testReloadOnFirstCallbackAdded() {
+        controller.addCallback(mockCallback)
+        executor.runAllReady()
+
+        verify(mockSL).reload()
+    }
+
+    @Test
+    fun testCallbackCalledWhenAdded() {
+        `when`(mockSL.reload()).then {
+            serviceListingCallbackCaptor.value.onServicesReloaded(emptyList())
+        }
+
+        controller.addCallback(mockCallback)
+        executor.runAllReady()
+        verify(mockCallback).onServicesUpdated(any())
+        reset(mockCallback)
+
+        controller.addCallback(mockCallbackOther)
+        executor.runAllReady()
+        verify(mockCallbackOther).onServicesUpdated(any())
+        verify(mockCallback, never()).onServicesUpdated(any())
+    }
+
+    @Test
+    fun testCallbackGetsList() {
+        val list = listOf(serviceInfo)
+        controller.addCallback(mockCallback)
+        controller.addCallback(mockCallbackOther)
+
+        @Suppress("unchecked_cast")
+        val captor: ArgumentCaptor<List<CandidateInfo>> =
+                ArgumentCaptor.forClass(List::class.java) as ArgumentCaptor<List<CandidateInfo>>
+
+        executor.runAllReady()
+        reset(mockCallback)
+        reset(mockCallbackOther)
+
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+        verify(mockCallback).onServicesUpdated(capture(captor))
+        assertEquals(1, captor.value.size)
+        assertEquals(componentName.flattenToString(), captor.value[0].key)
+
+        verify(mockCallbackOther).onServicesUpdated(capture(captor))
+        assertEquals(1, captor.value.size)
+        assertEquals(componentName.flattenToString(), captor.value[0].key)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
index 09cc5ba..fe8d769 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
@@ -29,6 +29,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -48,6 +49,7 @@
 
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.statusbar.NotificationVisibility;
+import com.android.systemui.DumpController;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.statusbar.RankingBuilder;
 import com.android.systemui.statusbar.notification.collection.NoManSimulator.NotifEvent;
@@ -103,7 +105,7 @@
         MockitoAnnotations.initMocks(this);
         Assert.sMainLooper = TestableLooper.get(this).getLooper();
 
-        mCollection = new NotifCollection(mStatusBarService);
+        mCollection = new NotifCollection(mStatusBarService, mock(DumpController.class));
         mCollection.attach(mGroupCoalescer);
         mCollection.addCollectionListener(mCollectionListener);
         mCollection.setBuildListener(mBuildListener);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
index be06748..e915be3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
@@ -16,9 +16,10 @@
 
 package com.android.systemui.statusbar.notification.collection;
 
-import static com.android.systemui.statusbar.notification.collection.ListDumper.dumpList;
+import static com.android.systemui.statusbar.notification.collection.ListDumper.dumpTree;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyList;
@@ -27,6 +28,7 @@
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
@@ -37,6 +39,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.systemui.DumpController;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.statusbar.notification.collection.ShadeListBuilder.OnRenderListListener;
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener;
@@ -45,7 +48,7 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.SectionsProvider;
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSection;
 import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener;
 import com.android.systemui.statusbar.notification.logging.NotifLog;
 import com.android.systemui.util.Assert;
@@ -100,7 +103,7 @@
         MockitoAnnotations.initMocks(this);
         Assert.sMainLooper = TestableLooper.get(this).getLooper();
 
-        mListBuilder = new ShadeListBuilder(mSystemClock, mNotifLog);
+        mListBuilder = new ShadeListBuilder(mSystemClock, mNotifLog, mock(DumpController.class));
         mListBuilder.setOnRenderListListener(mOnRenderListListener);
 
         mListBuilder.attach(mNotifCollection);
@@ -448,6 +451,29 @@
     }
 
     @Test
+    public void testPreRenderNotifsFilteredBreakupGroups() {
+        final String filterTag = "FILTER_ME";
+        // GIVEN a NotifFilter that filters out notifications with a tag
+        NotifFilter filter1 = spy(new NotifFilterWithTag(filterTag));
+        mListBuilder.addPreRenderFilter(filter1);
+
+        // WHEN the pipeline is kicked off on a list of notifs
+        addGroupChildWithTag(0, PACKAGE_2, GROUP_1, filterTag);
+        addGroupChild(1, PACKAGE_2, GROUP_1);
+        addGroupSummary(2, PACKAGE_2, GROUP_1);
+        dispatchBuild();
+
+        // THEN the final list doesn't contain any filtered-out notifs
+        // and groups that are too small are broken up
+        verifyBuiltList(
+                notif(1)
+        );
+
+        // THEN each filtered notif records the filter that did it
+        assertEquals(filter1, mEntrySet.get(0).mExcludingFilter);
+    }
+
+    @Test
     public void testNotifFiltersCanBePreempted() {
         // GIVEN two notif filters
         NotifFilter filter1 = spy(new PackageFilter(PACKAGE_2));
@@ -550,12 +576,17 @@
     }
 
     @Test
-    public void testNotifsAreSectioned() {
-        // GIVEN a filter that removes all PACKAGE_4 notifs and a SectionsProvider that divides
+    public void testNotifSections() {
+        // GIVEN a filter that removes all PACKAGE_4 notifs and sections that divide
         // notifs based on package name
         mListBuilder.addPreGroupFilter(new PackageFilter(PACKAGE_4));
-        final SectionsProvider sectionsProvider = spy(new PackageSectioner());
-        mListBuilder.setSectionsProvider(sectionsProvider);
+        final NotifSection pkg1Section = spy(new PackageSection(PACKAGE_1));
+        final NotifSection pkg2Section = spy(new PackageSection(PACKAGE_2));
+        // NOTE: no package 3 section explicitly added, so notifs with package 3 will get set by
+        // ShadeListBuilder's sDefaultSection which will demote it to the last section
+        final NotifSection pkg4Section = spy(new PackageSection(PACKAGE_4));
+        final NotifSection pkg5Section = spy(new PackageSection(PACKAGE_5));
+        mListBuilder.setSections(Arrays.asList(pkg1Section, pkg2Section, pkg4Section, pkg5Section));
 
         // WHEN we build a list with different packages
         addNotif(0, PACKAGE_4);
@@ -582,19 +613,93 @@
                         child(6)
                 ),
                 notif(8),
-                notif(3),
-                notif(9)
+                notif(9),
+                notif(3)
         );
 
-        // THEN the sections provider is called on all top level elements (but no children and no
-        // entries that were filtered out)
-        verify(sectionsProvider).getSection(mEntrySet.get(1));
-        verify(sectionsProvider).getSection(mEntrySet.get(2));
-        verify(sectionsProvider).getSection(mEntrySet.get(3));
-        verify(sectionsProvider).getSection(mEntrySet.get(7));
-        verify(sectionsProvider).getSection(mEntrySet.get(8));
-        verify(sectionsProvider).getSection(mEntrySet.get(9));
-        verify(sectionsProvider).getSection(mBuiltList.get(3));
+        // THEN the first section (pkg1Section) is called on all top level elements (but
+        // no children and no entries that were filtered out)
+        verify(pkg1Section).isInSection(mEntrySet.get(1));
+        verify(pkg1Section).isInSection(mEntrySet.get(2));
+        verify(pkg1Section).isInSection(mEntrySet.get(3));
+        verify(pkg1Section).isInSection(mEntrySet.get(7));
+        verify(pkg1Section).isInSection(mEntrySet.get(8));
+        verify(pkg1Section).isInSection(mEntrySet.get(9));
+        verify(pkg1Section).isInSection(mBuiltList.get(3));
+
+        verify(pkg1Section, never()).isInSection(mEntrySet.get(0));
+        verify(pkg1Section, never()).isInSection(mEntrySet.get(4));
+        verify(pkg1Section, never()).isInSection(mEntrySet.get(5));
+        verify(pkg1Section, never()).isInSection(mEntrySet.get(6));
+        verify(pkg1Section, never()).isInSection(mEntrySet.get(10));
+
+        // THEN the last section (pkg5Section) is not called on any of the entries that were
+        // filtered or already in a section
+        verify(pkg5Section, never()).isInSection(mEntrySet.get(0));
+        verify(pkg5Section, never()).isInSection(mEntrySet.get(1));
+        verify(pkg5Section, never()).isInSection(mEntrySet.get(2));
+        verify(pkg5Section, never()).isInSection(mEntrySet.get(4));
+        verify(pkg5Section, never()).isInSection(mEntrySet.get(5));
+        verify(pkg5Section, never()).isInSection(mEntrySet.get(6));
+        verify(pkg5Section, never()).isInSection(mEntrySet.get(7));
+        verify(pkg5Section, never()).isInSection(mEntrySet.get(8));
+        verify(pkg5Section, never()).isInSection(mEntrySet.get(10));
+
+        verify(pkg5Section).isInSection(mEntrySet.get(3));
+        verify(pkg5Section).isInSection(mEntrySet.get(9));
+
+        // THEN the correct section is assigned for entries in pkg1Section
+        assertEquals(pkg1Section, mEntrySet.get(2).mNotifSection);
+        assertEquals(0, mEntrySet.get(2).getSection());
+        assertEquals(pkg1Section, mEntrySet.get(7).mNotifSection);
+        assertEquals(0, mEntrySet.get(7).getSection());
+
+        // THEN the correct section is assigned for entries in pkg2Section
+        assertEquals(pkg2Section, mEntrySet.get(1).mNotifSection);
+        assertEquals(1, mEntrySet.get(1).getSection());
+        assertEquals(pkg2Section, mEntrySet.get(8).mNotifSection);
+        assertEquals(1, mEntrySet.get(8).getSection());
+        assertEquals(pkg2Section, mBuiltList.get(3).mNotifSection);
+        assertEquals(1, mBuiltList.get(3).getSection());
+
+        // THEN no section was assigned to entries in pkg4Section (since they were filtered)
+        assertEquals(null, mEntrySet.get(0).mNotifSection);
+        assertEquals(-1, mEntrySet.get(0).getSection());
+        assertEquals(null, mEntrySet.get(10).mNotifSection);
+        assertEquals(-1, mEntrySet.get(10).getSection());
+
+
+        // THEN the correct section is assigned for entries in pkg5Section
+        assertEquals(pkg5Section, mEntrySet.get(9).mNotifSection);
+        assertEquals(3, mEntrySet.get(9).getSection());
+
+        // THEN the children entries are assigned the same section as its parent
+        assertEquals(mBuiltList.get(3).mNotifSection, child(5).entry.mNotifSection);
+        assertEquals(mBuiltList.get(3).getSection(), child(5).entry.getSection());
+        assertEquals(mBuiltList.get(3).mNotifSection, child(6).entry.mNotifSection);
+        assertEquals(mBuiltList.get(3).getSection(), child(6).entry.getSection());
+    }
+
+    @Test
+    public void testNotifUsesDefaultSection() {
+        // GIVEN a Section for Package2
+        final NotifSection pkg2Section = spy(new PackageSection(PACKAGE_2));
+        mListBuilder.setSections(Arrays.asList(pkg2Section));
+
+        // WHEN we build a list with pkg1 and pkg2 packages
+        addNotif(0, PACKAGE_1);
+        addNotif(1, PACKAGE_2);
+        dispatchBuild();
+
+        // THEN the list is sorted according to section
+        verifyBuiltList(
+                notif(1),
+                notif(0)
+        );
+
+        // THEN the entry that didn't have an explicit section gets assigned the DefaultSection
+        assertEquals(1, notif(0).entry.getSection());
+        assertNotNull(notif(0).entry.mNotifSection);
     }
 
     @Test
@@ -629,7 +734,7 @@
         // GIVEN a bunch of registered listeners and pluggables
         NotifFilter preGroupFilter = spy(new PackageFilter(PACKAGE_1));
         NotifPromoter promoter = spy(new IdPromoter(3));
-        PackageSectioner sectioner = spy(new PackageSectioner());
+        NotifSection section = spy(new PackageSection(PACKAGE_1));
         NotifComparator comparator = spy(new HypeComparator(PACKAGE_4));
         NotifFilter preRenderFilter = spy(new PackageFilter(PACKAGE_5));
         mListBuilder.addPreGroupFilter(preGroupFilter);
@@ -637,7 +742,7 @@
         mListBuilder.addPromoter(promoter);
         mListBuilder.addOnBeforeSortListener(mOnBeforeSortListener);
         mListBuilder.setComparators(Collections.singletonList(comparator));
-        mListBuilder.setSectionsProvider(sectioner);
+        mListBuilder.setSections(Arrays.asList(section));
         mListBuilder.addOnBeforeRenderListListener(mOnBeforeRenderListListener);
         mListBuilder.addPreRenderFilter(preRenderFilter);
 
@@ -657,7 +762,7 @@
                 mOnBeforeTransformGroupsListener,
                 promoter,
                 mOnBeforeSortListener,
-                sectioner,
+                section,
                 comparator,
                 preRenderFilter,
                 mOnBeforeRenderListListener,
@@ -670,7 +775,7 @@
         inOrder.verify(promoter, atLeastOnce())
                 .shouldPromoteToTopLevel(any(NotificationEntry.class));
         inOrder.verify(mOnBeforeSortListener).onBeforeSort(anyList());
-        inOrder.verify(sectioner, atLeastOnce()).getSection(any(ListEntry.class));
+        inOrder.verify(section, atLeastOnce()).isInSection(any(ListEntry.class));
         inOrder.verify(comparator, atLeastOnce())
                 .compare(any(ListEntry.class), any(ListEntry.class));
         inOrder.verify(preRenderFilter, atLeastOnce())
@@ -684,12 +789,12 @@
         // GIVEN a variety of pluggables
         NotifFilter packageFilter = new PackageFilter(PACKAGE_1);
         NotifPromoter idPromoter = new IdPromoter(4);
-        SectionsProvider sectionsProvider = new PackageSectioner();
+        NotifSection section = new PackageSection(PACKAGE_1);
         NotifComparator hypeComparator = new HypeComparator(PACKAGE_2);
 
         mListBuilder.addPreGroupFilter(packageFilter);
         mListBuilder.addPromoter(idPromoter);
-        mListBuilder.setSectionsProvider(sectionsProvider);
+        mListBuilder.setSections(Arrays.asList(section));
         mListBuilder.setComparators(Collections.singletonList(hypeComparator));
 
         // GIVEN a set of random notifs
@@ -709,7 +814,7 @@
         verify(mOnRenderListListener).onRenderList(anyList());
 
         clearInvocations(mOnRenderListListener);
-        sectionsProvider.invalidateList();
+        section.invalidateList();
         verify(mOnRenderListListener).onRenderList(anyList());
 
         clearInvocations(mOnRenderListListener);
@@ -982,9 +1087,10 @@
         return builder;
     }
 
-    /** Same behavior as {@link #addNotif(int, String)}. */
-    private NotificationEntryBuilder addGroupChild(int index, String packageId, String groupId) {
+    private NotificationEntryBuilder addGroupChildWithTag(int index, String packageId,
+            String groupId, String tag) {
         final NotificationEntryBuilder builder = new NotificationEntryBuilder()
+                .setTag(tag)
                 .setPkg(packageId)
                 .setId(nextId(packageId))
                 .setRank(nextRank());
@@ -999,6 +1105,11 @@
         return builder;
     }
 
+    /** Same behavior as {@link #addNotif(int, String)}. */
+    private NotificationEntryBuilder addGroupChild(int index, String packageId, String groupId) {
+        return addGroupChildWithTag(index, packageId, groupId, null);
+    }
+
     private int nextId(String packageName) {
         Integer nextId = mNextIdMap.get(packageName);
         if (nextId == null) {
@@ -1081,7 +1192,8 @@
             }
         } catch (AssertionError err) {
             throw new AssertionError(
-                    "List under test failed verification:\n" + dumpList(mBuiltList), err);
+                    "List under test failed verification:\n" + dumpTree(mBuiltList,
+                            true, ""), err);
         }
     }
 
@@ -1166,6 +1278,21 @@
         }
     }
 
+    /** Filters out notifications with a particular tag */
+    private static class NotifFilterWithTag extends NotifFilter {
+        private final String mTag;
+
+        NotifFilterWithTag(String tag) {
+            super("NotifFilterWithTag_" + tag);
+            mTag = tag;
+        }
+
+        @Override
+        public boolean shouldFilterOut(NotificationEntry entry, long now) {
+            return Objects.equals(entry.getSbn().getTag(), mTag);
+        }
+    }
+
     /** Promotes notifs with particular IDs */
     private static class IdPromoter extends NotifPromoter {
         private final List<Integer> mIds;
@@ -1202,25 +1329,18 @@
         }
     }
 
-    /** Sorts notifs into sections based on their package name */
-    private static class PackageSectioner extends SectionsProvider {
+    /** Represents a section for the passed pkg */
+    private static class PackageSection extends NotifSection {
+        private final String mPackage;
 
-        PackageSectioner() {
-            super("PackageSectioner");
+        PackageSection(String pkg) {
+            super("PackageSection_" + pkg);
+            mPackage = pkg;
         }
 
         @Override
-        public int getSection(ListEntry entry) {
-            switch (entry.getRepresentativeEntry().getSbn().getPackageName()) {
-                case PACKAGE_1:
-                    return 1;
-                case PACKAGE_2:
-                    return 2;
-                case PACKAGE_3:
-                    return 3;
-                default:
-                    return 4;
-            }
+        public boolean isInSection(ListEntry entry) {
+            return entry.getRepresentativeEntry().getSbn().getPackageName().equals(mPackage);
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java
new file mode 100644
index 0000000..0bf458c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java
@@ -0,0 +1,816 @@
+/*
+ * Copyright (C) 2020 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.notification.row;
+
+import static android.app.Notification.FLAG_BUBBLE;
+import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
+import static android.app.NotificationManager.IMPORTANCE_HIGH;
+import static android.app.NotificationManager.IMPORTANCE_LOW;
+import static android.print.PrintManager.PRINT_SPOOLER_PACKAGE_NAME;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.INotificationManager;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
+import android.app.PendingIntent;
+import android.app.Person;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.util.Slog;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.bubbles.BubbleController;
+import com.android.systemui.bubbles.BubblesTestActivity;
+import com.android.systemui.statusbar.SbnBuilder;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.mockito.stubbing.Answer;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class NotificationConversationInfoTest extends SysuiTestCase {
+    private static final String TEST_PACKAGE_NAME = "test_package";
+    private static final String TEST_SYSTEM_PACKAGE_NAME = PRINT_SPOOLER_PACKAGE_NAME;
+    private static final int TEST_UID = 1;
+    private static final String TEST_CHANNEL = "test_channel";
+    private static final String TEST_CHANNEL_NAME = "TEST CHANNEL NAME";
+    private static final String CONVERSATION_ID = "convo";
+
+    private TestableLooper mTestableLooper;
+    private NotificationConversationInfo mNotificationInfo;
+    private NotificationChannel mNotificationChannel;
+    private NotificationChannel mConversationChannel;
+    private StatusBarNotification mSbn;
+    private NotificationEntry mEntry;
+    private StatusBarNotification mBubbleSbn;
+    private NotificationEntry mBubbleEntry;
+    @Mock
+    private ShortcutInfo mShortcutInfo;
+    private Drawable mImage;
+
+    @Rule
+    public MockitoRule mockito = MockitoJUnit.rule();
+    @Mock
+    private MetricsLogger mMetricsLogger;
+    @Mock
+    private INotificationManager mMockINotificationManager;
+    @Mock
+    private PackageManager mMockPackageManager;
+    @Mock
+    private VisualStabilityManager mVisualStabilityManager;
+    @Mock
+    private BubbleController mBubbleController;
+    @Mock
+    private LauncherApps mLauncherApps;
+    @Mock
+    private ShortcutManager mShortcutManager;
+    @Mock
+    private NotificationGuts mNotificationGuts;
+
+    @Before
+    public void setUp() throws Exception {
+        mTestableLooper = TestableLooper.get(this);
+
+        mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper());
+        mDependency.injectTestDependency(MetricsLogger.class, mMetricsLogger);
+        mDependency.injectTestDependency(BubbleController.class, mBubbleController);
+        // Inflate the layout
+        final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
+        mNotificationInfo = (NotificationConversationInfo) layoutInflater.inflate(
+                R.layout.notification_conversation_info,
+                null);
+        mNotificationInfo.mShowHomeScreen = true;
+        mNotificationInfo.setGutsParent(mNotificationGuts);
+        doAnswer((Answer<Object>) invocation -> {
+            mNotificationInfo.handleCloseControls(true, false);
+            return null;
+        }).when(mNotificationGuts).closeControls(anyInt(), anyInt(), eq(true), eq(false));
+        // Our view is never attached to a window so the View#post methods in NotificationInfo never
+        // get called. Setting this will skip the post and do the action immediately.
+        mNotificationInfo.mSkipPost = true;
+
+        // PackageManager must return a packageInfo and applicationInfo.
+        final PackageInfo packageInfo = new PackageInfo();
+        packageInfo.packageName = TEST_PACKAGE_NAME;
+        when(mMockPackageManager.getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt()))
+                .thenReturn(packageInfo);
+        final ApplicationInfo applicationInfo = new ApplicationInfo();
+        applicationInfo.uid = TEST_UID;  // non-zero
+        when(mMockPackageManager.getApplicationInfo(eq(TEST_PACKAGE_NAME), anyInt())).thenReturn(
+                applicationInfo);
+        final PackageInfo systemPackageInfo = new PackageInfo();
+        systemPackageInfo.packageName = TEST_SYSTEM_PACKAGE_NAME;
+        when(mMockPackageManager.getPackageInfo(eq(TEST_SYSTEM_PACKAGE_NAME), anyInt()))
+                .thenReturn(systemPackageInfo);
+        when(mMockPackageManager.getPackageInfo(eq("android"), anyInt()))
+                .thenReturn(packageInfo);
+
+        when(mShortcutInfo.getShortLabel()).thenReturn("Convo name");
+        List<ShortcutInfo> shortcuts = Arrays.asList(mShortcutInfo);
+        when(mLauncherApps.getShortcuts(any(), any())).thenReturn(shortcuts);
+        mImage = mContext.getDrawable(R.drawable.ic_star);
+        when(mLauncherApps.getShortcutBadgedIconDrawable(eq(mShortcutInfo),
+                anyInt())).thenReturn(mImage);
+
+        mNotificationChannel = new NotificationChannel(
+                TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_LOW);
+
+        Notification notification = new Notification.Builder(mContext, mNotificationChannel.getId())
+                .setShortcutId(CONVERSATION_ID)
+                .setStyle(new Notification.MessagingStyle(new Person.Builder().setName("m").build())
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "hello!", 1000, new Person.Builder().setName("other").build())))
+                .build();
+        mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0,
+                notification, UserHandle.CURRENT, null, 0);
+        mEntry = new NotificationEntryBuilder().setSbn(mSbn).build();
+
+        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0,
+                new Intent(mContext, BubblesTestActivity.class), 0);
+        mBubbleSbn = new SbnBuilder(mSbn).setBubbleMetadata(
+                new Notification.BubbleMetadata.Builder()
+                        .setIntent(bubbleIntent)
+                        .setIcon(Icon.createWithResource(mContext, R.drawable.android)).build())
+                .build();
+        mBubbleEntry = new NotificationEntryBuilder().setSbn(mBubbleSbn).build();
+
+        mConversationChannel = new NotificationChannel(
+                TEST_CHANNEL + " : " + CONVERSATION_ID, TEST_CHANNEL_NAME, IMPORTANCE_LOW);
+        mConversationChannel.setConversationId(TEST_CHANNEL, CONVERSATION_ID);
+        when(mMockINotificationManager.getConversationNotificationChannel(anyString(), anyInt(),
+                anyString(), eq(TEST_CHANNEL), eq(false), eq(CONVERSATION_ID)))
+                .thenReturn(mConversationChannel);
+    }
+
+    @Test
+    public void testBindNotification_SetsTextShortcutName() {
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mEntry,
+                null,
+                null,
+                null,
+                true);
+        final TextView textView = mNotificationInfo.findViewById(R.id.name);
+        assertEquals(mShortcutInfo.getShortLabel(), textView.getText().toString());
+        assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.header).getVisibility());
+    }
+
+    @Test
+    public void testBindNotification_SetsShortcutIcon() {
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mEntry,
+                null,
+                null,
+                null,
+                true);
+        final ImageView view = mNotificationInfo.findViewById(R.id.conversation_icon);
+        assertEquals(mImage, view.getDrawable());
+    }
+
+    @Test
+    public void testBindNotification_SetsTextApplicationName() {
+        when(mMockPackageManager.getApplicationLabel(any())).thenReturn("App Name");
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mEntry,
+                null,
+                null,
+                null,
+                true);
+        final TextView textView = mNotificationInfo.findViewById(R.id.pkg_name);
+        assertTrue(textView.getText().toString().contains("App Name"));
+        assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.header).getVisibility());
+    }
+
+    @Test
+    public void testBindNotification_SetsTextChannelName() {
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mEntry,
+                null,
+                null,
+                null,
+                true);
+        final TextView textView = mNotificationInfo.findViewById(R.id.parent_channel_name);
+        assertTrue(textView.getText().toString().contains(mNotificationChannel.getName()));
+        assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.header).getVisibility());
+    }
+
+    @Test
+    public void testBindNotification_SetsTextGroupName() throws Exception {
+        NotificationChannelGroup group = new NotificationChannelGroup("id", "name");
+        when(mMockINotificationManager.getNotificationChannelGroupForPackage(
+               anyString(), anyString(), anyInt())).thenReturn(group);
+        mNotificationChannel.setGroup(group.getId());
+        mConversationChannel.setGroup(group.getId());
+
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mEntry,
+                null,
+                null,
+                null,
+                true);
+        final TextView textView = mNotificationInfo.findViewById(R.id.group_name);
+        assertTrue(textView.getText().toString().contains(group.getName()));
+        assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.header).getVisibility());
+        assertEquals(VISIBLE, textView.getVisibility());
+        assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.group_divider).getVisibility());
+    }
+
+    @Test
+    public void testBindNotification_GroupNameHiddenIfNoGroup() {
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mEntry,
+                null,
+                null,
+                null,
+                true);
+        final TextView textView = mNotificationInfo.findViewById(R.id.group_name);
+        assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.header).getVisibility());
+        assertEquals(GONE, textView.getVisibility());
+        assertEquals(GONE, mNotificationInfo.findViewById(R.id.group_divider).getVisibility());
+    }
+
+    @Test
+    public void testBindNotification_noDelegate() {
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mEntry,
+                null,
+                null,
+                null,
+                true);
+        final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
+        assertEquals(GONE, nameView.getVisibility());
+        final TextView dividerView = mNotificationInfo.findViewById(R.id.pkg_divider);
+        assertEquals(GONE, dividerView.getVisibility());
+    }
+
+    @Test
+    public void testBindNotification_delegate() throws Exception {
+        mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, "other", 0, null, TEST_UID, 0,
+                mSbn.getNotification(), UserHandle.CURRENT, null, 0);
+        final ApplicationInfo applicationInfo = new ApplicationInfo();
+        applicationInfo.uid = 7;  // non-zero
+        when(mMockPackageManager.getApplicationInfo(eq("other"), anyInt())).thenReturn(
+                applicationInfo);
+        when(mMockPackageManager.getApplicationLabel(any())).thenReturn("Other");
+
+        NotificationEntry entry = new NotificationEntryBuilder().setSbn(mSbn).build();
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                entry,
+                null,
+                null,
+                null,
+                true);
+        final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
+        assertEquals(VISIBLE, nameView.getVisibility());
+        assertTrue(nameView.getText().toString().contains("Proxied"));
+        final TextView dividerView = mNotificationInfo.findViewById(R.id.pkg_divider);
+        assertEquals(VISIBLE, dividerView.getVisibility());
+    }
+
+    @Test
+    public void testBindNotification_SetsOnClickListenerForSettings() {
+        final CountDownLatch latch = new CountDownLatch(1);
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mEntry,
+                (View v, NotificationChannel c, int appUid) -> {
+                    assertEquals(mConversationChannel, c);
+                    latch.countDown();
+                },
+                null,
+                null,
+                true);
+
+        final View settingsButton = mNotificationInfo.findViewById(R.id.info);
+        settingsButton.performClick();
+        // Verify that listener was triggered.
+        assertEquals(0, latch.getCount());
+    }
+
+    @Test
+    public void testBindNotification_SettingsButtonInvisibleWhenNoClickListener() {
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mEntry,
+                null,
+                null,
+                null,
+                true);
+        final View settingsButton = mNotificationInfo.findViewById(R.id.info);
+        assertTrue(settingsButton.getVisibility() != View.VISIBLE);
+    }
+
+    @Test
+    public void testBindNotification_SettingsButtonInvisibleWhenDeviceUnprovisioned() {
+        final CountDownLatch latch = new CountDownLatch(1);
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mEntry,
+                (View v, NotificationChannel c, int appUid) -> {
+                    assertEquals(mNotificationChannel, c);
+                    latch.countDown();
+                },
+                null,
+                null,
+                false);
+        final View settingsButton = mNotificationInfo.findViewById(R.id.info);
+        assertTrue(settingsButton.getVisibility() != View.VISIBLE);
+    }
+
+    @Test
+    public void testBindNotification_bubbleActionVisibleWhenCanBubble()  {
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mBubbleEntry,
+                null,
+                null,
+                null,
+                true);
+
+        View bubbleView = mNotificationInfo.findViewById(R.id.bubble);
+        assertEquals(View.VISIBLE, bubbleView.getVisibility());
+    }
+
+    @Test
+    public void testBindNotification_bubbleActionVisibleWhenCannotBubble()  {
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mEntry,
+                null,
+                null,
+                null,
+                true);
+
+        View bubbleView = mNotificationInfo.findViewById(R.id.bubble);
+        assertEquals(View.GONE, bubbleView.getVisibility());
+    }
+
+    @Test
+    public void testAddToHome() throws Exception {
+        when(mShortcutManager.isRequestPinShortcutSupported()).thenReturn(true);
+
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mBubbleEntry,
+                null,
+                null,
+                null,
+                true);
+
+
+        // Promote it
+        mNotificationInfo.findViewById(R.id.home).performClick();
+        mTestableLooper.processAllMessages();
+
+        verify(mShortcutManager, times(1)).requestPinShortcut(mShortcutInfo, null);
+        verify(mMockINotificationManager, never()).updateNotificationChannelForPackage(
+                anyString(), anyInt(), any());
+    }
+
+    @Test
+    public void testSnooze() throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mBubbleEntry,
+                null,
+                null,
+                (View v, int hours) -> {
+                    latch.countDown();
+                },
+                true);
+
+
+        // Promote it
+        mNotificationInfo.findViewById(R.id.snooze).performClick();
+        mTestableLooper.processAllMessages();
+
+        assertEquals(0, latch.getCount());
+        verify(mMockINotificationManager, never()).updateNotificationChannelForPackage(
+                anyString(), anyInt(), any());
+    }
+
+    @Test
+    public void testBubble_promotesBubble() throws Exception {
+        mNotificationChannel.setAllowBubbles(false);
+        mConversationChannel.setAllowBubbles(false);
+
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mBubbleEntry,
+                null,
+                null,
+                null,
+                true);
+
+        assertFalse(mBubbleEntry.isBubble());
+
+        // Promote it
+        mNotificationInfo.findViewById(R.id.bubble).performClick();
+        mTestableLooper.processAllMessages();
+
+        verify(mBubbleController, times(1)).onUserCreatedBubbleFromNotification(mBubbleEntry);
+        ArgumentCaptor<NotificationChannel> captor =
+                ArgumentCaptor.forClass(NotificationChannel.class);
+        verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
+                anyString(), anyInt(), captor.capture());
+        assertTrue(captor.getValue().canBubble());
+    }
+
+    @Test
+    public void testBubble_demotesBubble() throws Exception {
+        mBubbleEntry.getSbn().getNotification().flags |= FLAG_BUBBLE;
+
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mBubbleEntry,
+                null,
+                null,
+                null,
+                true);
+
+        assertTrue(mBubbleEntry.isBubble());
+
+        // Demote it
+        mNotificationInfo.findViewById(R.id.bubble).performClick();
+        mTestableLooper.processAllMessages();
+
+        verify(mBubbleController, times(1)).onUserDemotedBubbleFromNotification(mBubbleEntry);
+        ArgumentCaptor<NotificationChannel> captor =
+                ArgumentCaptor.forClass(NotificationChannel.class);
+        verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
+                anyString(), anyInt(), captor.capture());
+        assertFalse(captor.getValue().canBubble());
+    }
+
+    @Test
+    public void testFavorite_favorite() throws Exception {
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mEntry,
+                null,
+                null,
+                null,
+                true);
+
+
+        Button fave = mNotificationInfo.findViewById(R.id.fave);
+        assertEquals(mContext.getString(R.string.notification_conversation_favorite),
+                fave.getText().toString());
+
+        fave.performClick();
+        mTestableLooper.processAllMessages();
+
+        ArgumentCaptor<NotificationChannel> captor =
+                ArgumentCaptor.forClass(NotificationChannel.class);
+        verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
+                anyString(), anyInt(), captor.capture());
+        assertTrue(captor.getValue().canBypassDnd());
+    }
+
+    @Test
+    public void testFavorite_unfavorite() throws Exception {
+        mNotificationChannel.setBypassDnd(true);
+        mConversationChannel.setBypassDnd(true);
+
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mEntry,
+                null,
+                null,
+                null,
+                true);
+
+        Button fave = mNotificationInfo.findViewById(R.id.fave);
+        assertEquals(mContext.getString(R.string.notification_conversation_unfavorite),
+                fave.getText().toString());
+
+        fave.performClick();
+        mTestableLooper.processAllMessages();
+
+        ArgumentCaptor<NotificationChannel> captor =
+                ArgumentCaptor.forClass(NotificationChannel.class);
+        verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
+                anyString(), anyInt(), captor.capture());
+        assertFalse(captor.getValue().canBypassDnd());
+    }
+
+    @Test
+    public void testMute_mute() throws Exception {
+        mNotificationChannel.setImportance(IMPORTANCE_DEFAULT);
+        mConversationChannel.setImportance(IMPORTANCE_DEFAULT);
+
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mEntry,
+                null,
+                null,
+                null,
+                true);
+
+        Button mute = mNotificationInfo.findViewById(R.id.mute);
+        assertEquals(mContext.getString(R.string.notification_conversation_mute),
+                mute.getText().toString());
+
+        mute.performClick();
+        mTestableLooper.processAllMessages();
+
+        ArgumentCaptor<NotificationChannel> captor =
+                ArgumentCaptor.forClass(NotificationChannel.class);
+        verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
+                anyString(), anyInt(), captor.capture());
+        assertEquals(IMPORTANCE_LOW, captor.getValue().getImportance());
+    }
+
+    @Test
+    public void testMute_unmute() throws Exception {
+        mNotificationChannel.setImportance(IMPORTANCE_LOW);
+        mNotificationChannel.setOriginalImportance(IMPORTANCE_HIGH);
+        mConversationChannel.setImportance(IMPORTANCE_LOW);
+        mConversationChannel.setOriginalImportance(IMPORTANCE_HIGH);
+
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mEntry,
+                null,
+                null,
+                null,
+                true);
+
+
+        Button mute = mNotificationInfo.findViewById(R.id.mute);
+        assertEquals(mContext.getString(R.string.notification_conversation_unmute),
+                mute.getText().toString());
+
+        mute.performClick();
+        mTestableLooper.processAllMessages();
+
+        ArgumentCaptor<NotificationChannel> captor =
+                ArgumentCaptor.forClass(NotificationChannel.class);
+        verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
+                anyString(), anyInt(), captor.capture());
+        assertEquals(IMPORTANCE_HIGH, captor.getValue().getImportance());
+    }
+
+    @Test
+    public void testBindNotification_createsNewChannel() throws Exception {
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mEntry,
+                null,
+                null,
+                null,
+                true);
+        verify(mMockINotificationManager, times(1)).createConversationNotificationChannelForPackage(
+                anyString(), anyInt(), any(), eq(CONVERSATION_ID));
+    }
+
+    @Test
+    public void testBindNotification_doesNotCreateNewChannelIfExists() throws Exception {
+        mNotificationChannel.setConversationId("", CONVERSATION_ID);
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mEntry,
+                null,
+                null,
+                null,
+                true);
+        verify(mMockINotificationManager, never()).createConversationNotificationChannelForPackage(
+                anyString(), anyInt(), any(), eq(CONVERSATION_ID));
+    }
+
+    @Test
+    public void testAdjustImportanceTemporarilyAllowsReordering() {
+        mNotificationChannel.setImportance(IMPORTANCE_DEFAULT);
+        mConversationChannel.setImportance(IMPORTANCE_DEFAULT);
+        mNotificationInfo.bindNotification(
+                mShortcutManager,
+                mLauncherApps,
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mEntry,
+                null,
+                null,
+                null,
+                true);
+
+        mNotificationInfo.findViewById(R.id.mute).performClick();
+
+        verify(mVisualStabilityManager).temporarilyAllowReordering();
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
index f48c40c..b33d26f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
@@ -132,16 +132,6 @@
     }
 
     @Test
-    public void testNoAppOpsInSlowSwipe_biDirectionalSwipe() {
-        NotificationMenuRow row = new NotificationMenuRow(mContext, true);
-        row.createMenu(mRow, null);
-
-        ViewGroup container = (ViewGroup) row.getMenuView();
-        // in the new interruption model there is only the blocking item
-        assertEquals(1, container.getChildCount());
-    }
-
-    @Test
     public void testIsSnappedAndOnSameSide() {
         NotificationMenuRow row = Mockito.spy(new NotificationMenuRow((mContext)));
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/animation/PhysicsAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/animation/PhysicsAnimatorTest.kt
index 709a1a8..a785d4b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/animation/PhysicsAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/animation/PhysicsAnimatorTest.kt
@@ -19,6 +19,7 @@
 import org.junit.After
 import org.junit.Assert
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNotEquals
 import org.junit.Assert.assertTrue
 import org.junit.Before
@@ -154,6 +155,7 @@
         verify(mockEndListener).onAnimationEnd(
                 testView,
                 DynamicAnimation.TRANSLATION_X,
+                wasFling = false,
                 canceled = false,
                 finalValue = 10f,
                 finalVelocity = 0f,
@@ -175,6 +177,7 @@
         verify(mockEndListener).onAnimationEnd(
                 testView,
                 DynamicAnimation.TRANSLATION_Y,
+                wasFling = false,
                 canceled = false,
                 finalValue = 500f,
                 finalVelocity = 0f,
@@ -214,8 +217,8 @@
 
         verifyUpdateListenerCalls(animator, mockUpdateListener)
         verify(mockEndListener, times(1)).onAnimationEnd(
-                eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(false), anyFloat(), anyFloat(),
-                eq(true))
+                eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(false), eq(false), anyFloat(),
+                anyFloat(), eq(true))
         verify(mockEndAction, times(1)).run()
 
         animator
@@ -329,13 +332,24 @@
         // The original end listener should also have been called, with allEnded = true since it was
         // provided to an animator that animated only TRANSLATION_X.
         verify(mockEndListener, times(1))
-                .onAnimationEnd(testView, DynamicAnimation.TRANSLATION_X, false, 200f, 0f, true)
+                .onAnimationEnd(
+                        testView, DynamicAnimation.TRANSLATION_X,
+                        wasFling = false,
+                        canceled = false,
+                        finalValue = 200f,
+                        finalVelocity = 0f,
+                        allRelevantPropertyAnimsEnded = true)
         verifyNoMoreInteractions(mockEndListener)
 
         // The second end listener should have been called, but with allEnded = false since it was
         // provided to an animator that animated both TRANSLATION_X and TRANSLATION_Y.
         verify(secondEndListener, times(1))
-                .onAnimationEnd(testView, DynamicAnimation.TRANSLATION_X, false, 200f, 0f, false)
+                .onAnimationEnd(testView, DynamicAnimation.TRANSLATION_X,
+                        wasFling = false,
+                        canceled = false,
+                        finalValue = 200f,
+                        finalVelocity = 0f,
+                        allRelevantPropertyAnimsEnded = false)
         verifyNoMoreInteractions(secondEndListener)
 
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_Y)
@@ -345,7 +359,12 @@
         verifyNoMoreInteractions(mockEndListener)
 
         verify(secondEndListener, times(1))
-                .onAnimationEnd(testView, DynamicAnimation.TRANSLATION_Y, false, 4000f, 0f, true)
+                .onAnimationEnd(testView, DynamicAnimation.TRANSLATION_Y,
+                        wasFling = false,
+                        canceled = false,
+                        finalValue = 4000f,
+                        finalVelocity = 0f,
+                        allRelevantPropertyAnimsEnded = true)
         verifyNoMoreInteractions(secondEndListener)
     }
 
@@ -364,8 +383,8 @@
         assertEquals(10f, testView.translationX, 1f)
         verify(mockEndListener, times(1))
                 .onAnimationEnd(
-                        eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(false), eq(10f),
-                        anyFloat(), eq(true))
+                        eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(true), eq(false),
+                        eq(10f), anyFloat(), eq(true))
 
         animator
                 .fling(
@@ -381,8 +400,39 @@
         assertEquals(-5f, testView.translationX, 1f)
         verify(mockEndListener, times(1))
                 .onAnimationEnd(
-                        eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(false), eq(-5f),
-                        anyFloat(), eq(true))
+                        eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(true), eq(false),
+                        eq(-5f), anyFloat(), eq(true))
+    }
+
+    @Test
+    fun testIsPropertyAnimating() {
+        PhysicsAnimatorTestUtils.setAllAnimationsBlock(false)
+
+        testView.physicsAnimator
+                .spring(DynamicAnimation.TRANSLATION_X, 500f, springConfig)
+                .fling(DynamicAnimation.TRANSLATION_Y, 10f, flingConfig)
+                .spring(DynamicAnimation.TRANSLATION_Z, 1000f, springConfig)
+                .start()
+
+        // All of the properties we just started should be animating.
+        assertTrue(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_X))
+        assertTrue(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Y))
+        assertTrue(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Z))
+
+        // Block until x and y end.
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(testView.physicsAnimator,
+                DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)
+
+        // Verify that x and y are no longer animating, but that Z is (it's springing to 1000f).
+        assertFalse(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_X))
+        assertFalse(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Y))
+        assertTrue(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Z))
+
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Z)
+
+        assertFalse(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_X))
+        assertFalse(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Y))
+        assertFalse(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Z))
     }
 
     @Test
@@ -395,6 +445,118 @@
         assertEquals(200f, testView.translationX, 1f)
     }
 
+    @Test
+    fun testFlingThenSpring() {
+        PhysicsAnimatorTestUtils.setAllAnimationsBlock(false)
+
+        // Start at 500f and fling hard to the left. We should quickly reach the 250f minimum, fly
+        // past it since there's so much velocity remaining, then spring back to 250f.
+        testView.translationX = 500f
+        animator
+                .flingThenSpring(
+                        DynamicAnimation.TRANSLATION_X,
+                        -5000f,
+                        flingConfig.apply { min = 250f },
+                        springConfig)
+                .addUpdateListener(mockUpdateListener)
+                .addEndListener(mockEndListener)
+                .withEndActions(mockEndAction::run)
+                .start()
+
+        // Block until we pass the minimum.
+        PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(
+                animator) { v -> v.translationX <= 250f }
+
+        // Double check that the view is there.
+        assertTrue(testView.translationX <= 250f)
+
+        // The update listener should have been called with a value < 500f, and then a value less
+        // than or equal to the 250f minimum.
+        verifyAnimationUpdateFrames(animator, DynamicAnimation.TRANSLATION_X,
+                { u -> u.value < 500f },
+                { u -> u.value <= 250f })
+
+        // Despite the fact that the fling has ended, the end listener shouldn't have been called
+        // since we're about to begin springing the same property.
+        verifyNoMoreInteractions(mockEndListener)
+        verifyNoMoreInteractions(mockEndAction)
+
+        // Wait for the spring to finish.
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_X)
+
+        // Make sure we continued past 250f since the spring should have been started with some
+        // remaining negative velocity from the fling.
+        verifyAnimationUpdateFrames(animator, DynamicAnimation.TRANSLATION_X,
+                { u -> u.value < 250f })
+
+        // At this point, the animation end listener should have been called once, and only once,
+        // when the spring ended at 250f.
+        verify(mockEndListener).onAnimationEnd(testView, DynamicAnimation.TRANSLATION_X,
+                wasFling = false,
+                canceled = false,
+                finalValue = 250f,
+                finalVelocity = 0f,
+                allRelevantPropertyAnimsEnded = true)
+        verifyNoMoreInteractions(mockEndListener)
+
+        // The end action should also have been called once.
+        verify(mockEndAction, times(1)).run()
+        verifyNoMoreInteractions(mockEndAction)
+
+        assertEquals(250f, testView.translationX)
+    }
+
+    @Test
+    fun testFlingThenSpring_objectOutsideFlingBounds() {
+        // Start the view at x = -500, well outside the fling bounds of min = 0f, with strong
+        // negative velocity.
+        testView.translationX = -500f
+        animator
+                .flingThenSpring(
+                        DynamicAnimation.TRANSLATION_X,
+                        -5000f,
+                        flingConfig.apply { min = 0f },
+                        springConfig)
+                .addUpdateListener(mockUpdateListener)
+                .addEndListener(mockEndListener)
+                .withEndActions(mockEndAction::run)
+                .start()
+
+        // The initial -5000f velocity should result in frames to the left of -500f before the view
+        // springs back towards 0f.
+        verifyAnimationUpdateFrames(
+                animator, DynamicAnimation.TRANSLATION_X,
+                { u -> u.value < -500f },
+                { u -> u.value > -500f })
+
+        // We should end up at the fling min.
+        assertEquals(0f, testView.translationX, 1f)
+    }
+
+    @Test
+    fun testFlingToMinMaxThenSpring() {
+        // Start at x = 500f.
+        testView.translationX = 500f
+
+        // Fling to the left at the very sad rate of -1 pixels per second. That won't get us much of
+        // anywhere, and certainly not to the 0f min.
+        animator
+                // Good thing we have flingToMinMaxThenSpring!
+                .flingThenSpring(
+                        DynamicAnimation.TRANSLATION_X,
+                        -10000f,
+                        flingConfig.apply { min = 0f },
+                        springConfig,
+                        flingMustReachMinOrMax = true)
+                .addUpdateListener(mockUpdateListener)
+                .addEndListener(mockEndListener)
+                .withEndActions(mockEndAction::run)
+                .start()
+
+        // Thanks, flingToMinMaxThenSpring, for adding enough velocity to get us here.
+        assertEquals(0f, testView.translationX, 1f)
+    }
+
     /**
      * Verifies that the calls to the mock update listener match the animation update frames
      * reported by the test internal listener, in order.
diff --git a/packages/Tethering/Android.bp b/packages/Tethering/Android.bp
index 5bad24a..e441fb5 100644
--- a/packages/Tethering/Android.bp
+++ b/packages/Tethering/Android.bp
@@ -16,7 +16,7 @@
 
 java_defaults {
     name: "TetheringAndroidLibraryDefaults",
-    platform_apis: true,
+    sdk_version: "system_current",
     srcs: [
         "src/**/*.java",
         ":framework-tethering-shared-srcs",
@@ -81,7 +81,7 @@
 // Common defaults for compiling the actual APK.
 java_defaults {
     name: "TetheringAppDefaults",
-    platform_apis: true,
+    sdk_version: "system_current",
     privileged: true,
     // Build system doesn't track transitive dependeicies for jni_libs, list all the dependencies
     // explicitly.
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/MultiTap.java b/services/accessibility/java/com/android/server/accessibility/gestures/MultiTap.java
index 2891c6c..4c9e590 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/MultiTap.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/MultiTap.java
@@ -82,7 +82,7 @@
             mCurrentTaps++;
             if (mCurrentTaps == mTargetTaps) {
                 // Done.
-                completeAfterTapTimeout(event, rawEvent, policyFlags);
+                completeGesture(event, rawEvent, policyFlags);
                 return;
             }
             // Needs more taps.
diff --git a/services/api/current.txt b/services/api/current.txt
index d802177..18e38b1 100644
--- a/services/api/current.txt
+++ b/services/api/current.txt
@@ -1 +1,31 @@
 // Signature format: 2.0
+package com.android.server {
+
+  public abstract class SystemService {
+    ctor public SystemService(@NonNull android.content.Context);
+    method @NonNull public final android.content.Context getContext();
+    method public boolean isSupportedUser(@NonNull com.android.server.SystemService.TargetUser);
+    method public void onBootPhase(int);
+    method public void onCleanupUser(@NonNull com.android.server.SystemService.TargetUser);
+    method public abstract void onStart();
+    method public void onStartUser(@NonNull com.android.server.SystemService.TargetUser);
+    method public void onStopUser(@NonNull com.android.server.SystemService.TargetUser);
+    method public void onSwitchUser(@Nullable com.android.server.SystemService.TargetUser, @NonNull com.android.server.SystemService.TargetUser);
+    method public void onUnlockUser(@NonNull com.android.server.SystemService.TargetUser);
+    method protected final void publishBinderService(@NonNull String, @NonNull android.os.IBinder);
+    method protected final void publishBinderService(@NonNull String, @NonNull android.os.IBinder, boolean);
+    field public static final int PHASE_ACTIVITY_MANAGER_READY = 550; // 0x226
+    field public static final int PHASE_BOOT_COMPLETED = 1000; // 0x3e8
+    field public static final int PHASE_DEVICE_SPECIFIC_SERVICES_READY = 520; // 0x208
+    field public static final int PHASE_LOCK_SETTINGS_READY = 480; // 0x1e0
+    field public static final int PHASE_SYSTEM_SERVICES_READY = 500; // 0x1f4
+    field public static final int PHASE_THIRD_PARTY_APPS_CAN_START = 600; // 0x258
+    field public static final int PHASE_WAIT_FOR_DEFAULT_DISPLAY = 100; // 0x64
+  }
+
+  public static final class SystemService.TargetUser {
+    method @NonNull public android.os.UserHandle getUserHandle();
+  }
+
+}
+
diff --git a/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionUi.java b/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionUi.java
index 17cb739..1e3ee88 100644
--- a/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionUi.java
+++ b/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionUi.java
@@ -16,24 +16,36 @@
 
 package com.android.server.autofill.ui;
 
+import static android.app.slice.SliceItem.FORMAT_IMAGE;
+import static android.app.slice.SliceItem.FORMAT_TEXT;
+
 import android.annotation.MainThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.slice.Slice;
+import android.app.slice.SliceItem;
 import android.content.Context;
-import android.graphics.Color;
 import android.graphics.PixelFormat;
+import android.graphics.drawable.Icon;
 import android.os.IBinder;
 import android.service.autofill.Dataset;
 import android.util.Log;
 import android.util.Slog;
+import android.view.LayoutInflater;
 import android.view.SurfaceControl;
 import android.view.SurfaceControlViewHost;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.WindowManager;
 import android.view.autofill.AutofillId;
 import android.view.autofill.AutofillValue;
+import android.widget.ImageView;
 import android.widget.TextView;
 
+import com.android.internal.R;
+
+import java.util.List;
+
 /**
  * This is a temporary inline suggestion UI inflater which will be replaced by the ExtServices
  * implementation.
@@ -72,18 +84,66 @@
                 mContext.getDisplay(), (IBinder) null);
         final SurfaceControl sc = wvr.getSurfacePackage().getSurfaceControl();
 
-        TextView textView = new TextView(mContext);
-        textView.setText(datasetValue.getTextValue());
-        textView.setBackgroundColor(Color.WHITE);
-        textView.setTextColor(Color.BLACK);
-        if (onClickListener != null) {
-            textView.setOnClickListener(onClickListener);
-        }
+        final ViewGroup suggestionView =
+                (ViewGroup) renderSlice(dataset.getFieldInlinePresentation(index).getSlice());
 
         WindowManager.LayoutParams lp =
                 new WindowManager.LayoutParams(width, height,
                         WindowManager.LayoutParams.TYPE_APPLICATION, 0, PixelFormat.TRANSPARENT);
-        wvr.addView(textView, lp);
+        wvr.addView(suggestionView, lp);
         return sc;
     }
+
+    private View renderSlice(Slice slice) {
+        final LayoutInflater inflater = LayoutInflater.from(mContext);
+        final ViewGroup suggestionView =
+                (ViewGroup) inflater.inflate(R.layout.autofill_inline_suggestion, null);
+
+        final ImageView startIconView =
+                suggestionView.findViewById(R.id.autofill_inline_suggestion_start_icon);
+        final TextView titleView =
+                suggestionView.findViewById(R.id.autofill_inline_suggestion_title);
+        final TextView subtitleView =
+                suggestionView.findViewById(R.id.autofill_inline_suggestion_subtitle);
+        final ImageView endIconView =
+                suggestionView.findViewById(R.id.autofill_inline_suggestion_end_icon);
+
+        boolean hasStartIcon = false;
+        boolean hasEndIcon = false;
+        boolean hasSubtitle = false;
+        final List<SliceItem> sliceItems = slice.getItems();
+        for (int i = 0; i < sliceItems.size(); i++) {
+            final SliceItem sliceItem = sliceItems.get(i);
+            if (sliceItem.getFormat().equals(FORMAT_IMAGE)) {
+                final Icon sliceIcon = sliceItem.getIcon();
+                if (i == 0) { // start icon
+                    startIconView.setImageIcon(sliceIcon);
+                    hasStartIcon = true;
+                } else { // end icon
+                    endIconView.setImageIcon(sliceIcon);
+                    hasEndIcon = true;
+                }
+            } else if (sliceItem.getFormat().equals(FORMAT_TEXT)) {
+                final List<String> sliceHints = sliceItem.getHints();
+                final String sliceText = sliceItem.getText().toString();
+                if (sliceHints.contains("inline_title")) { // title
+                    titleView.setText(sliceText);
+                } else { // subtitle
+                    subtitleView.setText(sliceText);
+                    hasSubtitle = true;
+                }
+            }
+        }
+        if (!hasStartIcon) {
+            startIconView.setVisibility(View.GONE);
+        }
+        if (!hasEndIcon) {
+            endIconView.setVisibility(View.GONE);
+        }
+        if (!hasSubtitle) {
+            subtitleView.setVisibility(View.GONE);
+        }
+
+        return suggestionView;
+    }
 }
diff --git a/services/core/java/android/app/usage/UsageStatsManagerInternal.java b/services/core/java/android/app/usage/UsageStatsManagerInternal.java
index 2f8c506..f3647602 100644
--- a/services/core/java/android/app/usage/UsageStatsManagerInternal.java
+++ b/services/core/java/android/app/usage/UsageStatsManagerInternal.java
@@ -23,8 +23,6 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 
-import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener;
-
 import java.util.List;
 import java.util.Set;
 
@@ -198,6 +196,12 @@
             long beginTime, long endTime, boolean obfuscateInstantApps);
 
     /**
+     * Returns the events for the user in the given time period.
+     */
+    public abstract UsageEvents queryEventsForUser(@UserIdInt int userId, long beginTime,
+            long endTime, boolean shouldObfuscateInstantApps);
+
+    /**
      * Used to persist the last time a job was run for this app, in order to make decisions later
      * whether a job should be deferred until later. The time passed in should be in elapsed
      * realtime since boot.
diff --git a/services/core/java/com/android/server/SystemService.java b/services/core/java/com/android/server/SystemService.java
index f46b9ae..b1584fe 100644
--- a/services/core/java/com/android/server/SystemService.java
+++ b/services/core/java/com/android/server/SystemService.java
@@ -21,6 +21,9 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.SystemApi.Client;
+import android.annotation.SystemApi.Process;
 import android.annotation.UserIdInt;
 import android.app.ActivityThread;
 import android.content.Context;
@@ -62,7 +65,7 @@
  *
  * {@hide}
  */
-//@SystemApi(client = Client.MODULE_LIBRARIES, process = Process.SYSTEM_SERVER)
+@SystemApi(client = Client.MODULE_LIBRARIES, process = Process.SYSTEM_SERVER)
 public abstract class SystemService {
 
     /** @hide */
@@ -129,7 +132,7 @@
      * Class representing user in question in the lifecycle callbacks.
      * @hide
      */
-    //@SystemApi(client = Client.MODULE_LIBRARIES, process = Process.SYSTEM_SERVER)
+    @SystemApi(client = Client.MODULE_LIBRARIES, process = Process.SYSTEM_SERVER)
     public static final class TargetUser {
         @NonNull
         private final UserInfo mUserInfo;
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 2b7745b..4f03a8e 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -46,6 +46,7 @@
 import android.telephony.Annotation.DataFailureCause;
 import android.telephony.Annotation.RadioPowerState;
 import android.telephony.Annotation.SrvccState;
+import android.telephony.BarringInfo;
 import android.telephony.CallAttributes;
 import android.telephony.CallQuality;
 import android.telephony.CellIdentity;
@@ -254,6 +255,8 @@
 
     private int[] mCallPreciseDisconnectCause;
 
+    private List<BarringInfo> mBarringInfo = null;
+
     private boolean mCarrierNetworkChangeState = false;
 
     private PhoneCapability mPhoneCapability = null;
@@ -436,6 +439,7 @@
             cutListToSize(mCellInfo, mNumPhones);
             cutListToSize(mImsReasonInfo, mNumPhones);
             cutListToSize(mPreciseDataConnectionStates, mNumPhones);
+            cutListToSize(mBarringInfo, mNumPhones);
             return;
         }
 
@@ -467,6 +471,7 @@
             mForegroundCallState[i] = PreciseCallState.PRECISE_CALL_STATE_IDLE;
             mBackgroundCallState[i] = PreciseCallState.PRECISE_CALL_STATE_IDLE;
             mPreciseDataConnectionStates.add(new HashMap<Integer, PreciseDataConnectionState>());
+            mBarringInfo.add(i, new BarringInfo());
         }
     }
 
@@ -524,6 +529,7 @@
         mEmergencyNumberList = new HashMap<>();
         mOutgoingCallEmergencyNumber = new EmergencyNumber[numPhones];
         mOutgoingSmsEmergencyNumber = new EmergencyNumber[numPhones];
+        mBarringInfo = new ArrayList<>();
         for (int i = 0; i < numPhones; i++) {
             mCallState[i] =  TelephonyManager.CALL_STATE_IDLE;
             mDataActivity[i] = TelephonyManager.DATA_ACTIVITY_NONE;
@@ -551,6 +557,7 @@
             mForegroundCallState[i] = PreciseCallState.PRECISE_CALL_STATE_IDLE;
             mBackgroundCallState[i] = PreciseCallState.PRECISE_CALL_STATE_IDLE;
             mPreciseDataConnectionStates.add(new HashMap<Integer, PreciseDataConnectionState>());
+            mBarringInfo.add(i, new BarringInfo());
         }
 
         mAppOps = mContext.getSystemService(AppOpsManager.class);
@@ -993,6 +1000,19 @@
                             remove(r.binder);
                         }
                     }
+                    if ((events & PhoneStateListener.LISTEN_BARRING_INFO) != 0) {
+                        BarringInfo barringInfo = mBarringInfo.get(phoneId);
+                        BarringInfo biNoLocation = barringInfo != null
+                                ? barringInfo.createLocationInfoSanitizedCopy() : null;
+                        if (VDBG) log("listen: call onBarringInfoChanged=" + barringInfo);
+                        try {
+                            r.callback.onBarringInfoChanged(
+                                    checkFineLocationAccess(r, Build.VERSION_CODES.R)
+                                            ? barringInfo : biNoLocation);
+                        } catch (RemoteException ex) {
+                            remove(r.binder);
+                        }
+                    }
                 }
             }
         } else {
@@ -2102,6 +2122,52 @@
         }
     }
 
+    /**
+     * Send a notification of changes to barring status to PhoneStateListener registrants.
+     *
+     * @param phoneId the phoneId
+     * @param subId the subId
+     * @param barringInfo a structure containing the complete updated barring info.
+     */
+    public void notifyBarringInfoChanged(int phoneId, int subId, @NonNull BarringInfo barringInfo) {
+        if (!checkNotifyPermission("notifyBarringInfo()")) {
+            return;
+        }
+        if (barringInfo == null) {
+            log("Received null BarringInfo for subId=" + subId + ", phoneId=" + phoneId);
+            mBarringInfo.set(phoneId, new BarringInfo());
+            return;
+        }
+
+        synchronized (mRecords) {
+            if (validatePhoneId(phoneId)) {
+                mBarringInfo.set(phoneId, barringInfo);
+                // Barring info is non-null
+                BarringInfo biNoLocation = barringInfo.createLocationInfoSanitizedCopy();
+                if (VDBG) log("listen: call onBarringInfoChanged=" + barringInfo);
+                for (Record r : mRecords) {
+                    if (r.matchPhoneStateListenerEvent(
+                            PhoneStateListener.LISTEN_BARRING_INFO)
+                            && idMatch(r.subId, subId, phoneId)) {
+                        try {
+                            if (DBG_LOC) {
+                                log("notifyBarringInfo: mBarringInfo="
+                                        + barringInfo + " r=" + r);
+                            }
+                            r.callback.onBarringInfoChanged(
+                                    checkFineLocationAccess(r, Build.VERSION_CODES.R)
+                                        ? barringInfo : biNoLocation);
+                        } catch (RemoteException ex) {
+                            mRemoveList.add(r.binder);
+                        }
+                    }
+                }
+            }
+            handleRemoveListLocked();
+        }
+    }
+
+
     @Override
     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
         final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
@@ -2142,6 +2208,7 @@
                 pw.println("mPreciseDataConnectionStates=" + mPreciseDataConnectionStates.get(i));
                 pw.println("mOutgoingCallEmergencyNumber=" + mOutgoingCallEmergencyNumber[i]);
                 pw.println("mOutgoingSmsEmergencyNumber=" + mOutgoingSmsEmergencyNumber[i]);
+                pw.println("mBarringInfo=" + mBarringInfo.get(i));
                 pw.decreaseIndent();
             }
             pw.println("mCarrierNetworkChangeState=" + mCarrierNetworkChangeState);
diff --git a/services/core/java/com/android/server/backup/PeopleBackupHelper.java b/services/core/java/com/android/server/backup/PeopleBackupHelper.java
new file mode 100644
index 0000000..e58a051
--- /dev/null
+++ b/services/core/java/com/android/server/backup/PeopleBackupHelper.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2020 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.backup;
+
+import android.app.backup.BlobBackupHelper;
+import android.util.Slog;
+
+import com.android.server.LocalServices;
+import com.android.server.people.PeopleServiceInternal;
+
+class PeopleBackupHelper extends BlobBackupHelper {
+
+    private static final String TAG = PeopleBackupHelper.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    // Current schema of the backup state blob.
+    private static final int STATE_VERSION = 1;
+
+    // Key under which conversation infos state blob is committed to backup.
+    private static final String KEY_CONVERSATIONS = "people_conversation_infos";
+
+    private final int mUserId;
+
+    PeopleBackupHelper(int userId) {
+        super(STATE_VERSION, KEY_CONVERSATIONS);
+        mUserId = userId;
+    }
+
+    @Override
+    protected byte[] getBackupPayload(String key) {
+        if (!KEY_CONVERSATIONS.equals(key)) {
+            Slog.w(TAG, "Unexpected backup key " + key);
+            return new byte[0];
+        }
+        PeopleServiceInternal ps = LocalServices.getService(PeopleServiceInternal.class);
+        if (DEBUG) {
+            Slog.d(TAG, "Handling backup of " + key);
+        }
+        return ps.backupConversationInfos(mUserId);
+    }
+
+    @Override
+    protected void applyRestoredPayload(String key, byte[] payload) {
+        if (!KEY_CONVERSATIONS.equals(key)) {
+            Slog.w(TAG, "Unexpected restore key " + key);
+            return;
+        }
+        PeopleServiceInternal ps = LocalServices.getService(PeopleServiceInternal.class);
+        if (DEBUG) {
+            Slog.d(TAG, "Handling restore of " + key);
+        }
+        ps.restoreConversationInfos(mUserId, key, payload);
+    }
+}
diff --git a/services/core/java/com/android/server/backup/SystemBackupAgent.java b/services/core/java/com/android/server/backup/SystemBackupAgent.java
index 35e8f56..1f4563b 100644
--- a/services/core/java/com/android/server/backup/SystemBackupAgent.java
+++ b/services/core/java/com/android/server/backup/SystemBackupAgent.java
@@ -55,6 +55,7 @@
     private static final String SHORTCUT_MANAGER_HELPER = "shortcut_manager";
     private static final String ACCOUNT_MANAGER_HELPER = "account_manager";
     private static final String SLICES_HELPER = "slices";
+    private static final String PEOPLE_HELPER = "people";
 
     // These paths must match what the WallpaperManagerService uses.  The leaf *_FILENAME
     // are also used in the full-backup file format, so must not change unless steps are
@@ -99,6 +100,7 @@
         addHelper(SHORTCUT_MANAGER_HELPER, new ShortcutBackupHelper());
         addHelper(ACCOUNT_MANAGER_HELPER, new AccountManagerBackupHelper());
         addHelper(SLICES_HELPER, new SliceBackupHelper(this));
+        addHelper(PEOPLE_HELPER, new PeopleBackupHelper(mUserId));
     }
 
     @Override
diff --git a/services/core/java/com/android/server/connectivity/MultipathPolicyTracker.java b/services/core/java/com/android/server/connectivity/MultipathPolicyTracker.java
index 6fa999c..04c792a 100644
--- a/services/core/java/com/android/server/connectivity/MultipathPolicyTracker.java
+++ b/services/core/java/com/android/server/connectivity/MultipathPolicyTracker.java
@@ -26,6 +26,7 @@
 import static android.net.NetworkPolicy.LIMIT_DISABLED;
 import static android.net.NetworkPolicy.WARNING_DISABLED;
 import static android.provider.Settings.Global.NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 
 import static com.android.server.net.NetworkPolicyManagerInternal.QUOTA_TYPE_MULTIPATH;
 import static com.android.server.net.NetworkPolicyManagerService.OPPORTUNISTIC_QUOTA_UNKNOWN;
@@ -46,19 +47,18 @@
 import android.net.NetworkPolicy;
 import android.net.NetworkPolicyManager;
 import android.net.NetworkRequest;
+import android.net.NetworkSpecifier;
 import android.net.NetworkStats;
 import android.net.NetworkTemplate;
-import android.net.StringNetworkSpecifier;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.Uri;
 import android.os.BestClock;
 import android.os.Handler;
 import android.os.SystemClock;
-import android.net.Uri;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.telephony.TelephonyManager;
-import android.util.DataUnit;
 import android.util.DebugUtils;
-import android.util.Pair;
 import android.util.Range;
 import android.util.Slog;
 
@@ -74,7 +74,6 @@
 import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
 import java.time.temporal.ChronoUnit;
-import java.util.Iterator;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 
@@ -185,7 +184,6 @@
     // Track information on mobile networks as they come and go.
     class MultipathTracker {
         final Network network;
-        final int subId;
         final String subscriberId;
 
         private long mQuota;
@@ -198,13 +196,14 @@
         public MultipathTracker(Network network, NetworkCapabilities nc) {
             this.network = network;
             this.mNetworkCapabilities = new NetworkCapabilities(nc);
-            try {
-                subId = Integer.parseInt(
-                        ((StringNetworkSpecifier) nc.getNetworkSpecifier()).toString());
-            } catch (ClassCastException | NullPointerException | NumberFormatException e) {
+            NetworkSpecifier specifier = nc.getNetworkSpecifier();
+            int subId = INVALID_SUBSCRIPTION_ID;
+            if (specifier instanceof TelephonyNetworkSpecifier) {
+                subId = ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
+            } else {
                 throw new IllegalStateException(String.format(
-                        "Can't get subId from mobile network %s (%s): %s",
-                        network, nc, e.getMessage()));
+                        "Can't get subId from mobile network %s (%s)",
+                        network, nc));
             }
 
             TelephonyManager tele = mContext.getSystemService(TelephonyManager.class);
diff --git a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
index 2179518..2c41557 100644
--- a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
+++ b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
@@ -28,7 +28,7 @@
 import android.content.Intent;
 import android.content.res.Resources;
 import android.net.NetworkSpecifier;
-import android.net.StringNetworkSpecifier;
+import android.net.TelephonyNetworkSpecifier;
 import android.net.wifi.WifiInfo;
 import android.os.UserHandle;
 import android.telephony.SubscriptionManager;
@@ -223,14 +223,8 @@
                     // name has been added to it
                     NetworkSpecifier specifier = nai.networkCapabilities.getNetworkSpecifier();
                     int subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID;
-                    if (specifier instanceof StringNetworkSpecifier) {
-                        try {
-                            subId = Integer.parseInt(
-                                    ((StringNetworkSpecifier) specifier).specifier);
-                        } catch (NumberFormatException e) {
-                            Slog.e(TAG, "NumberFormatException on "
-                                    + ((StringNetworkSpecifier) specifier).specifier);
-                        }
+                    if (specifier instanceof TelephonyNetworkSpecifier) {
+                        subId = ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
                     }
 
                     details = mTelephonyManager.createForSubscriptionId(subId)
diff --git a/services/core/java/com/android/server/integrity/model/IndexingFileConstants.java b/services/core/java/com/android/server/integrity/model/IndexingFileConstants.java
index 52df89870..d21febb 100644
--- a/services/core/java/com/android/server/integrity/model/IndexingFileConstants.java
+++ b/services/core/java/com/android/server/integrity/model/IndexingFileConstants.java
@@ -18,9 +18,9 @@
 
 /**  A helper class containing special indexing file constants. */
 public final class IndexingFileConstants {
-    // The parsing time seems acceptable for this block size based on the tests in
-    // go/ic-rule-file-format.
-    public static final int INDEXING_BLOCK_SIZE = 100;
+    // We empirically experimented with different block sizes and identified that 250 is in the
+    // optimal range of efficient computation.
+    public static final int INDEXING_BLOCK_SIZE = 250;
 
     public static final String START_INDEXING_KEY = "START_KEY";
     public static final String END_INDEXING_KEY = "END_KEY";
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index b291a2e..c518614 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -161,7 +161,7 @@
 import android.net.NetworkState;
 import android.net.NetworkStats;
 import android.net.NetworkTemplate;
-import android.net.StringNetworkSpecifier;
+import android.net.TelephonyNetworkSpecifier;
 import android.net.TrafficStats;
 import android.net.wifi.WifiConfiguration;
 import android.net.wifi.WifiManager;
@@ -5281,16 +5281,12 @@
     }
 
     private int parseSubId(NetworkState state) {
-        // TODO: moved to using a legitimate NetworkSpecifier instead of string parsing
         int subId = INVALID_SUBSCRIPTION_ID;
         if (state != null && state.networkCapabilities != null
                 && state.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
             NetworkSpecifier spec = state.networkCapabilities.getNetworkSpecifier();
-            if (spec instanceof StringNetworkSpecifier) {
-                try {
-                    subId = Integer.parseInt(((StringNetworkSpecifier) spec).specifier);
-                } catch (NumberFormatException e) {
-                }
+            if (spec instanceof TelephonyNetworkSpecifier) {
+                subId = ((TelephonyNetworkSpecifier) spec).getSubscriptionId();
             }
         }
         return subId;
diff --git a/services/core/java/com/android/server/notification/NotificationChannelExtractor.java b/services/core/java/com/android/server/notification/NotificationChannelExtractor.java
index da2ca9b..eaf120e 100644
--- a/services/core/java/com/android/server/notification/NotificationChannelExtractor.java
+++ b/services/core/java/com/android/server/notification/NotificationChannelExtractor.java
@@ -42,9 +42,10 @@
             return null;
         }
 
-        record.updateNotificationChannel(mConfig.getNotificationChannel(record.sbn.getPackageName(),
+        record.updateNotificationChannel(mConfig.getConversationNotificationChannel(
+                record.sbn.getPackageName(),
                 record.sbn.getUid(), record.getChannel().getId(),
-                record.getNotification().getShortcutId(), false));
+                record.getNotification().getShortcutId(), true, false));
 
         return null;
     }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 0e08033..1d49364 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -472,7 +472,8 @@
     private static final String LOCKSCREEN_ALLOW_SECURE_NOTIFICATIONS_VALUE = "value";
 
     private RankingHelper mRankingHelper;
-    private PreferencesHelper mPreferencesHelper;
+    @VisibleForTesting
+    PreferencesHelper mPreferencesHelper;
 
     private final UserProfiles mUserProfiles = new UserProfiles();
     private NotificationListeners mListeners;
@@ -3054,23 +3055,24 @@
         public NotificationChannel getNotificationChannel(String callingPkg, int userId,
                 String targetPkg, String channelId) {
             return getConversationNotificationChannel(
-                    callingPkg, userId, targetPkg, channelId, null);
+                    callingPkg, userId, targetPkg, channelId, true, null);
         }
 
         @Override
         public NotificationChannel getConversationNotificationChannel(String callingPkg, int userId,
-                String targetPkg, String channelId, String conversationId) {
+                String targetPkg, String channelId, boolean returnParentIfNoConversationChannel,
+                String conversationId) {
             if (canNotifyAsPackage(callingPkg, targetPkg, userId)
-                    || isCallingUidSystem()) {
+                    || isCallerIsSystemOrSystemUi()) {
                 int targetUid = -1;
                 try {
                     targetUid = mPackageManagerClient.getPackageUidAsUser(targetPkg, userId);
                 } catch (NameNotFoundException e) {
                     /* ignore */
                 }
-                return mPreferencesHelper.getNotificationChannel(
+                return mPreferencesHelper.getConversationNotificationChannel(
                         targetPkg, targetUid, channelId, conversationId,
-                        false /* includeDeleted */);
+                        returnParentIfNoConversationChannel, false /* includeDeleted */);
             }
             throw new SecurityException("Pkg " + callingPkg
                     + " cannot read channels for " + targetPkg + " in " + userId);
@@ -5255,9 +5257,15 @@
         if (mIsTelevision && (new Notification.TvExtender(notification)).getChannelId() != null) {
             channelId = (new Notification.TvExtender(notification)).getChannelId();
         }
-        final NotificationChannel channel = mPreferencesHelper.getNotificationChannel(pkg,
-                notificationUid, channelId, notification.getShortcutId(),
-                false /* includeDeleted */);
+        // TODO: flag this behavior
+        String shortcutId = notification.getShortcutId();
+        if (shortcutId == null
+            && notification.getNotificationStyle() == Notification.MessagingStyle.class) {
+            shortcutId = id + tag + NotificationChannel.PLACEHOLDER_CONVERSATION_ID;
+        }
+        final NotificationChannel channel = mPreferencesHelper.getConversationNotificationChannel(
+                pkg, notificationUid, channelId, shortcutId,
+                true /* parent ok */, false /* includeDeleted */);
         if (channel == null) {
             final String noChannelStr = "No Channel found for "
                     + "pkg=" + pkg
@@ -7893,6 +7901,14 @@
         return isUidSystemOrPhone(Binder.getCallingUid());
     }
 
+    private boolean isCallerIsSystemOrSystemUi() {
+        if (isCallerSystemOrPhone()) {
+            return true;
+        }
+        return getContext().checkCallingPermission(android.Manifest.permission.STATUS_BAR_SERVICE)
+                == PERMISSION_GRANTED;
+    }
+
     private void checkCallerIsSystemOrShell() {
         int callingUid = Binder.getCallingUid();
         if (callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID) {
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 92fcb7f..185d75c 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -858,12 +858,13 @@
     public NotificationChannel getNotificationChannel(String pkg, int uid, String channelId,
             boolean includeDeleted) {
         Objects.requireNonNull(pkg);
-        return getNotificationChannel(pkg, uid, channelId, null, includeDeleted);
+        return getConversationNotificationChannel(pkg, uid, channelId, null, true, includeDeleted);
     }
 
     @Override
-    public NotificationChannel getNotificationChannel(String pkg, int uid, String channelId,
-            String conversationId, boolean includeDeleted) {
+    public NotificationChannel getConversationNotificationChannel(String pkg, int uid,
+            String channelId, String conversationId, boolean returnParentIfNoConversationChannel,
+            boolean includeDeleted) {
         Preconditions.checkNotNull(pkg);
         synchronized (mPackagePreferences) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
@@ -873,16 +874,19 @@
             if (channelId == null) {
                 channelId = NotificationChannel.DEFAULT_CHANNEL_ID;
             }
-            if (conversationId == null) {
+            NotificationChannel channel = null;
+            if (conversationId != null) {
+                // look for an automatically created conversation specific channel
+                channel = findConversationChannel(r, channelId, conversationId, includeDeleted);
+            }
+            if (channel == null && returnParentIfNoConversationChannel) {
+                // look for it just based on its id
                 final NotificationChannel nc = r.channels.get(channelId);
                 if (nc != null && (includeDeleted || !nc.isDeleted())) {
                     return nc;
                 }
-            } else {
-                // look for an automatically created conversation specific channel
-                return findConversationChannel(r, channelId, conversationId, includeDeleted);
             }
-            return null;
+            return channel;
         }
     }
 
diff --git a/services/core/java/com/android/server/notification/RankingConfig.java b/services/core/java/com/android/server/notification/RankingConfig.java
index 4b044c1..7e98be7 100644
--- a/services/core/java/com/android/server/notification/RankingConfig.java
+++ b/services/core/java/com/android/server/notification/RankingConfig.java
@@ -45,8 +45,9 @@
             boolean fromUser);
     NotificationChannel getNotificationChannel(String pkg, int uid, String channelId,
             boolean includeDeleted);
-    NotificationChannel getNotificationChannel(String pkg, int uid, String channelId,
-            String conversationId, boolean includeDeleted);
+    NotificationChannel getConversationNotificationChannel(String pkg, int uid, String channelId,
+            String conversationId, boolean returnParentIfNoConversationChannel,
+            boolean includeDeleted);
     void deleteNotificationChannel(String pkg, int uid, String channelId);
     void permanentlyDeleteNotificationChannel(String pkg, int uid, String channelId);
     void permanentlyDeleteNotificationChannels(String pkg, int uid);
diff --git a/services/core/java/com/android/server/people/PeopleServiceInternal.java b/services/core/java/com/android/server/people/PeopleServiceInternal.java
index 31d30362..c5b868f 100644
--- a/services/core/java/com/android/server/people/PeopleServiceInternal.java
+++ b/services/core/java/com/android/server/people/PeopleServiceInternal.java
@@ -16,9 +16,25 @@
 
 package com.android.server.people;
 
+import android.annotation.NonNull;
 import android.service.appprediction.IPredictionService;
 
 /**
  * @hide Only for use within the system server.
  */
-public abstract class PeopleServiceInternal extends IPredictionService.Stub {}
+public abstract class PeopleServiceInternal extends IPredictionService.Stub {
+
+    /**
+     * The number conversation infos will be dynamic, based on the currently installed apps on the
+     * device. All of which should be combined into a single blob to be backed up.
+     */
+    public abstract byte[] backupConversationInfos(@NonNull int userId);
+
+    /**
+     * Multiple conversation infos may exist in the restore payload, child classes are required to
+     * manage the restoration based on how individual conversation infos were originally combined
+     * during backup.
+     */
+    public abstract void restoreConversationInfos(@NonNull int userId, @NonNull String key,
+            @NonNull byte[] payload);
+}
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index d468cd9..0411e29 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -55,6 +55,8 @@
 import android.app.ActivityManager;
 import android.app.ApplicationPackageManager;
 import android.app.IActivityManager;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
@@ -107,6 +109,7 @@
 import android.util.SparseBooleanArray;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.compat.IPlatformCompat;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.os.RoSystemProperties;
@@ -224,6 +227,8 @@
     private final Handler mHandler;
     private final Context mContext;
     private final MetricsLogger mMetricsLogger = new MetricsLogger();
+    private final IPlatformCompat mPlatformCompat = IPlatformCompat.Stub.asInterface(
+            ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
 
     /** Internal storage for permissions and related settings */
     @GuardedBy("mLock")
@@ -1824,6 +1829,14 @@
         return true;
     }
 
+    /**
+     * This change makes it so that apps are told to show rationale for asking for background
+     * location access every time they request.
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
+    private static final long BACKGROUND_RATIONALE_CHANGE_ID = 147316723L;
+
     @Override
     public boolean shouldShowRequestPermissionRationale(String permName,
             String packageName, int userId) {
@@ -1862,6 +1875,16 @@
             return false;
         }
 
+        try {
+            if (permName.equals(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
+                    && mPlatformCompat.isChangeEnabledByPackageName(BACKGROUND_RATIONALE_CHANGE_ID,
+                    packageName, userId)) {
+                return true;
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Unable to check if compatibility change is enabled.", e);
+        }
+
         return (flags & PackageManager.FLAG_PERMISSION_USER_SET) != 0;
     }
 
diff --git a/services/core/java/com/android/server/stats/StatsPullAtomService.java b/services/core/java/com/android/server/stats/StatsPullAtomService.java
index 91e39da..1f7ed8a 100644
--- a/services/core/java/com/android/server/stats/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/StatsPullAtomService.java
@@ -1154,11 +1154,30 @@
     }
 
     private void registerBuildInformation() {
-        // No op.
+        int tagId = StatsLog.BUILD_INFORMATION;
+        mStatsManager.registerPullAtomCallback(
+                tagId,
+                null, // use default PullAtomMetadata values
+                (atomTag, data) -> pullBuildInformation(atomTag, data),
+                BackgroundThread.getExecutor()
+        );
     }
 
-    private void pullBuildInformation() {
-        // No op.
+    private int pullBuildInformation(int atomTag, List<StatsEvent> pulledData) {
+        StatsEvent e = StatsEvent.newBuilder()
+                .setAtomId(atomTag)
+                .writeString(Build.FINGERPRINT)
+                .writeString(Build.BRAND)
+                .writeString(Build.PRODUCT)
+                .writeString(Build.DEVICE)
+                .writeString(Build.VERSION.RELEASE)
+                .writeString(Build.ID)
+                .writeString(Build.VERSION.INCREMENTAL)
+                .writeString(Build.TYPE)
+                .writeString(Build.TAGS)
+                .build();
+        pulledData.add(e);
+        return StatsManager.PULL_SUCCESS;
     }
 
     private void registerRoleHolder() {
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index c9a702f..19bc560 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2137,8 +2137,10 @@
                 (intent == null || (intent.getFlags() & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) == 0);
     }
 
+    @Override
     boolean isFocusable() {
-        return mRootWindowContainer.isFocusable(this, isAlwaysFocusable());
+        return super.isFocusable()
+                && (getWindowConfiguration().canReceiveKeys() || isAlwaysFocusable());
     }
 
     boolean isResizeable() {
@@ -2489,7 +2491,7 @@
             // We are finishing the top focused activity and its stack has nothing to be focused so
             // the next focusable stack should be focused.
             if (mayAdjustTop
-                    && (stack.topRunningActivity() == null || !stack.isFocusable())) {
+                    && (stack.topRunningActivity() == null || !stack.isTopActivityFocusable())) {
                 if (shouldAdjustGlobalFocus) {
                     // Move the entire hierarchy to top with updating global top resumed activity
                     // and focused application if needed.
diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java
index 9fd3ea4..f5fba8e 100644
--- a/services/core/java/com/android/server/wm/ActivityStack.java
+++ b/services/core/java/com/android/server/wm/ActivityStack.java
@@ -161,9 +161,9 @@
 import android.view.Display;
 import android.view.DisplayCutout;
 import android.view.DisplayInfo;
+import android.view.ITaskOrganizer;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
-import android.view.ITaskOrganizer;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -1248,13 +1248,20 @@
         }
     }
 
+    @Override
     boolean isFocusable() {
+        return super.isFocusable() && !(inSplitScreenPrimaryWindowingMode()
+                && mRootWindowContainer.mIsDockMinimized);
+    }
+
+    boolean isTopActivityFocusable() {
         final ActivityRecord r = topRunningActivity();
-        return mRootWindowContainer.isFocusable(this, r != null && r.isFocusable());
+        return r != null ? r.isFocusable()
+                : (isFocusable() && getWindowConfiguration().canReceiveKeys());
     }
 
     boolean isFocusableAndVisible() {
-        return isFocusable() && shouldBeVisible(null /* starting */);
+        return isTopActivityFocusable() && shouldBeVisible(null /* starting */);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 61ba15c..6587226 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -1569,7 +1569,7 @@
         if (mDoResume) {
             final ActivityRecord topTaskActivity =
                     mStartActivity.getTask().topRunningActivityLocked();
-            if (!mTargetStack.isFocusable()
+            if (!mTargetStack.isTopActivityFocusable()
                     || (topTaskActivity != null && topTaskActivity.isTaskOverlay()
                     && mStartActivity != topTaskActivity)) {
                 // If the activity is not focusable, we can't resume it, but still would like to
@@ -1588,7 +1588,7 @@
                 // will not update the focused stack.  If starting the new activity now allows the
                 // task stack to be focusable, then ensure that we now update the focused stack
                 // accordingly.
-                if (mTargetStack.isFocusable()
+                if (mTargetStack.isTopActivityFocusable()
                         && !mRootWindowContainer.isTopDisplayFocusedStack(mTargetStack)) {
                     mTargetStack.moveToFront("startActivityInner");
                 }
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 3092a08..e308f6b 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -248,7 +248,6 @@
 import com.android.internal.policy.KeyguardDismissCallback;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FastPrintWriter;
-import com.android.internal.util.Preconditions;
 import com.android.internal.util.function.pooled.PooledConsumer;
 import com.android.internal.util.function.pooled.PooledFunction;
 import com.android.internal.util.function.pooled.PooledLambda;
@@ -345,6 +344,10 @@
     /** This activity is being relaunched due to a free-resize operation. */
     public static final int RELAUNCH_REASON_FREE_RESIZE = 2;
 
+    /** Flag indicating that an applied transaction may have effected lifecycle */
+    private static final int TRANSACT_EFFECTS_CLIENT_CONFIG = 1;
+    private static final int TRANSACT_EFFECTS_LIFECYCLE = 1 << 1;
+
     Context mContext;
 
     /**
@@ -3300,7 +3303,7 @@
         }
     }
 
-    private void sanitizeAndApplyConfigChange(ConfigurationContainer container,
+    private int sanitizeAndApplyChange(ConfigurationContainer container,
             WindowContainerTransaction.Change change) {
         if (!(container instanceof Task || container instanceof ActivityStack)) {
             throw new RuntimeException("Invalid token in task transaction");
@@ -3312,12 +3315,22 @@
         configMask &= ActivityInfo.CONFIG_WINDOW_CONFIGURATION
                 | ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;
         windowMask &= WindowConfiguration.WINDOW_CONFIG_BOUNDS;
-        Configuration c = new Configuration(container.getRequestedOverrideConfiguration());
-        c.setTo(change.getConfiguration(), configMask, windowMask);
-        container.onRequestedOverrideConfigurationChanged(c);
-        // TODO(b/145675353): remove the following once we could apply new bounds to the
-        // pinned stack together with its children.
-        resizePinnedStackIfNeeded(container, configMask, windowMask, c);
+        int effects = 0;
+        if (configMask != 0) {
+            Configuration c = new Configuration(container.getRequestedOverrideConfiguration());
+            c.setTo(change.getConfiguration(), configMask, windowMask);
+            container.onRequestedOverrideConfigurationChanged(c);
+            // TODO(b/145675353): remove the following once we could apply new bounds to the
+            // pinned stack together with its children.
+            resizePinnedStackIfNeeded(container, configMask, windowMask, c);
+            effects |= TRANSACT_EFFECTS_CLIENT_CONFIG;
+        }
+        if ((change.getChangeMask() & WindowContainerTransaction.Change.CHANGE_FOCUSABLE) != 0) {
+            if (container.setFocusable(change.getFocusable())) {
+                effects |= TRANSACT_EFFECTS_LIFECYCLE;
+            }
+        }
+        return effects;
     }
 
     private void resizePinnedStackIfNeeded(ConfigurationContainer container, int configMask,
@@ -3334,9 +3347,9 @@
         }
     }
 
-    private void applyWindowContainerChange(ConfigurationContainer cc,
+    private int applyWindowContainerChange(ConfigurationContainer cc,
             WindowContainerTransaction.Change c) {
-        sanitizeAndApplyConfigChange(cc, c);
+        int effects = sanitizeAndApplyChange(cc, c);
 
         Rect enterPipBounds = c.getEnterPipBounds();
         if (enterPipBounds != null) {
@@ -3344,25 +3357,58 @@
             mStackSupervisor.updatePictureInPictureMode(tr,
                     enterPipBounds, true);
         }
+        return effects;
     }
 
     @Override
     public void applyContainerTransaction(WindowContainerTransaction t) {
         mAmInternal.enforceCallingPermission(MANAGE_ACTIVITY_STACKS, "applyContainerTransaction()");
+        if (t == null) {
+            return;
+        }
         long ident = Binder.clearCallingIdentity();
         try {
-            if (t == null) {
-                return;
-            }
             synchronized (mGlobalLock) {
-                Iterator<Map.Entry<IBinder, WindowContainerTransaction.Change>> entries =
-                        t.getChanges().entrySet().iterator();
-                while (entries.hasNext()) {
-                    final Map.Entry<IBinder, WindowContainerTransaction.Change> entry =
-                            entries.next();
-                    final ConfigurationContainer cc = ConfigurationContainer.RemoteToken.fromBinder(
-                            entry.getKey()).getContainer();
-                    applyWindowContainerChange(cc, entry.getValue());
+                int effects = 0;
+                deferWindowLayout();
+                try {
+                    ArraySet<WindowContainer> haveConfigChanges = new ArraySet<>();
+                    Iterator<Map.Entry<IBinder, WindowContainerTransaction.Change>> entries =
+                            t.getChanges().entrySet().iterator();
+                    while (entries.hasNext()) {
+                        final Map.Entry<IBinder, WindowContainerTransaction.Change> entry =
+                                entries.next();
+                        final ConfigurationContainer cc =
+                                ConfigurationContainer.RemoteToken.fromBinder(
+                                        entry.getKey()).getContainer();
+                        int containerEffect = applyWindowContainerChange(cc, entry.getValue());
+                        effects |= containerEffect;
+                        // Lifecycle changes will trigger ensureConfig for everything.
+                        if ((effects & TRANSACT_EFFECTS_LIFECYCLE) == 0
+                                && (containerEffect & TRANSACT_EFFECTS_CLIENT_CONFIG) != 0) {
+                            if (cc instanceof WindowContainer) {
+                                haveConfigChanges.add((WindowContainer) cc);
+                            }
+                        }
+                    }
+                    if ((effects & TRANSACT_EFFECTS_LIFECYCLE) != 0) {
+                        // Already calls ensureActivityConfig
+                        mRootWindowContainer.ensureActivitiesVisible(null, 0, PRESERVE_WINDOWS);
+                    } else if ((effects & TRANSACT_EFFECTS_CLIENT_CONFIG) != 0) {
+                        final PooledConsumer f = PooledLambda.obtainConsumer(
+                                ActivityRecord::ensureActivityConfiguration,
+                                PooledLambda.__(ActivityRecord.class), 0,
+                                false /* preserveWindow */);
+                        try {
+                            for (int i = haveConfigChanges.size() - 1; i >= 0; --i) {
+                                haveConfigChanges.valueAt(i).forAllActivities(f);
+                            }
+                        } finally {
+                            f.recycle();
+                        }
+                    }
+                } finally {
+                    continueWindowLayout();
                 }
             }
         } finally {
diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java
index d0310f1..1e60ce8 100644
--- a/services/core/java/com/android/server/wm/ConfigurationContainer.java
+++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java
@@ -624,6 +624,21 @@
         return mFullConfiguration.windowConfiguration.isAlwaysOnTop();
     }
 
+    /**
+     * Returns {@code true} if this container is focusable. Generally, if a parent is not focusable,
+     * this will not be focusable either.
+     */
+    boolean isFocusable() {
+        // TODO(split): Move this to WindowContainer once Split-screen is based on a WindowContainer
+        //              like DisplayArea vs. TaskTiles.
+        ConfigurationContainer parent = getParent();
+        return parent == null || parent.isFocusable();
+    }
+
+    boolean setFocusable(boolean focusable) {
+        return false;
+    }
+
     boolean hasChild() {
         return getChildCount() > 0;
     }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index ba9d757..908c4f1 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -5915,7 +5915,7 @@
             final ActivityRecord resumedActivity = stack.getResumedActivity();
             if (resumedActivity != null
                     && (stack.getVisibility(resuming) != STACK_VISIBILITY_VISIBLE
-                    || !stack.isFocusable())) {
+                    || !stack.isTopActivityFocusable())) {
                 if (DEBUG_STATES) Slog.d(TAG_STATES, "pauseBackStacks: stack=" + stack
                         + " mResumedActivity=" + resumedActivity);
                 someActivityPaused |= stack.startPausingLocked(userLeaving, false /* uiSleeping*/,
@@ -6238,7 +6238,7 @@
             for (int i = getStackCount() - 1; i >= 0; --i) {
                 final ActivityStack stack = getStackAt(i);
                 // Only consider focusable stacks other than the current focused one.
-                if (stack == focusedStack || !stack.isFocusable()) {
+                if (stack == focusedStack || !stack.isTopActivityFocusable()) {
                     continue;
                 }
                 topRunning = stack.topRunningActivity();
diff --git a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
index c09834f..9d985d7 100644
--- a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
+++ b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
@@ -75,7 +75,8 @@
         // activities are actually behind other fullscreen activities, but still required
         // to be visible (such as performing Recents animation).
         final boolean resumeTopActivity = mTop != null && !mTop.mLaunchTaskBehind
-                && mContiner.isFocusable() && mContiner.isInStackLocked(starting) == null;
+                && mContiner.isTopActivityFocusable()
+                && mContiner.isInStackLocked(starting) == null;
 
         final PooledConsumer f = PooledLambda.obtainConsumer(
                 EnsureActivitiesVisibleHelper::setActivityVisibilityState, this,
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index c3e815d..2f726e9 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -1860,14 +1860,6 @@
         return null;
     }
 
-    boolean isFocusable(ConfigurationContainer container, boolean alwaysFocusable) {
-        if (container.inSplitScreenPrimaryWindowingMode() && mIsDockMinimized) {
-            return false;
-        }
-
-        return container.getWindowConfiguration().canReceiveKeys() || alwaysFocusable;
-    }
-
     boolean isTopDisplayFocusedStack(ActivityStack stack) {
         return stack != null && stack == getTopDisplayFocusedStack();
     }
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotPersister.java b/services/core/java/com/android/server/wm/TaskSnapshotPersister.java
index d36a5d4..38a7000 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotPersister.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotPersister.java
@@ -53,7 +53,7 @@
     private static final String SNAPSHOTS_DIRNAME = "snapshots";
     private static final String REDUCED_POSTFIX = "_reduced";
     private static final float REDUCED_SCALE = .5f;
-    private static final float LOW_RAM_REDUCED_SCALE = .6f;
+    private static final float LOW_RAM_REDUCED_SCALE = .8f;
     static final boolean DISABLE_FULL_SIZED_BITMAPS = ActivityManager.isLowRamDeviceStatic();
     private static final long DELAY_MS = 100;
     private static final int QUALITY = 95;
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index c38868a..3b2d519 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -247,6 +247,8 @@
 
     private MagnificationSpec mLastMagnificationSpec;
 
+    private boolean mIsFocusable = true;
+
     WindowContainer(WindowManagerService wms) {
         mWmService = wms;
         mPendingTransaction = wms.mTransactionFactory.get();
@@ -851,6 +853,21 @@
         return false;
     }
 
+    @Override
+    boolean isFocusable() {
+        return super.isFocusable() && mIsFocusable;
+    }
+
+    /** Set whether this container or its children can be focusable */
+    @Override
+    boolean setFocusable(boolean focusable) {
+        if (mIsFocusable == focusable) {
+            return false;
+        }
+        mIsFocusable = focusable;
+        return true;
+    }
+
     /**
      * @return Whether this child is on top of the window hierarchy.
      */
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index ea2385f..0bd7392 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -71,11 +71,11 @@
 import android.util.DisplayMetrics;
 import android.util.EventLog;
 import android.util.Slog;
+import android.util.StatsLog;
 import android.view.WindowManager;
 import android.view.contentcapture.ContentCaptureManager;
 
 import com.android.internal.R;
-import com.android.internal.logging.MetricsLogger;
 import com.android.internal.notification.SystemNotificationChannels;
 import com.android.internal.os.BinderInternal;
 import com.android.internal.util.ConcurrentUtils;
@@ -443,10 +443,12 @@
 
             // Here we go!
             Slog.i(TAG, "Entered the Android system server!");
-            int uptimeMillis = (int) SystemClock.elapsedRealtime();
+            final long uptimeMillis = SystemClock.elapsedRealtime();
             EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_SYSTEM_RUN, uptimeMillis);
             if (!mRuntimeRestart) {
-                MetricsLogger.histogram(null, "boot_system_server_init", uptimeMillis);
+                StatsLog.write(StatsLog.BOOT_TIME_EVENT_ELAPSED_TIME_REPORTED,
+                        StatsLog.BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__SYSTEM_SERVER_INIT_START,
+                        uptimeMillis);
             }
 
             // In case the runtime switched since last boot (such as when
@@ -555,10 +557,12 @@
         StrictMode.initVmDefaults(null);
 
         if (!mRuntimeRestart && !isFirstBootOrUpgrade()) {
-            int uptimeMillis = (int) SystemClock.elapsedRealtime();
-            MetricsLogger.histogram(null, "boot_system_server_ready", uptimeMillis);
-            final int MAX_UPTIME_MILLIS = 60 * 1000;
-            if (uptimeMillis > MAX_UPTIME_MILLIS) {
+            final long uptimeMillis = SystemClock.elapsedRealtime();
+            StatsLog.write(StatsLog.BOOT_TIME_EVENT_ELAPSED_TIME_REPORTED,
+                    StatsLog.BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__SYSTEM_SERVER_READY,
+                    uptimeMillis);
+            final long maxUptimeMillis = 60 * 1000;
+            if (uptimeMillis > maxUptimeMillis) {
                 Slog.wtf(SYSTEM_SERVER_TIMING_TAG,
                         "SystemServer init took too long. uptimeMillis=" + uptimeMillis);
             }
@@ -791,8 +795,9 @@
 
         // Start the package manager.
         if (!mRuntimeRestart) {
-            MetricsLogger.histogram(null, "boot_package_manager_init_start",
-                    (int) SystemClock.elapsedRealtime());
+            StatsLog.write(StatsLog.BOOT_TIME_EVENT_ELAPSED_TIME_REPORTED,
+                    StatsLog.BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__PACKAGE_MANAGER_INIT_START,
+                    SystemClock.elapsedRealtime());
         }
 
         t.traceBegin("StartPackageManagerService");
@@ -808,8 +813,9 @@
         mPackageManager = mSystemContext.getPackageManager();
         t.traceEnd();
         if (!mRuntimeRestart && !isFirstBootOrUpgrade()) {
-            MetricsLogger.histogram(null, "boot_package_manager_init_ready",
-                    (int) SystemClock.elapsedRealtime());
+            StatsLog.write(StatsLog.BOOT_TIME_EVENT_ELAPSED_TIME_REPORTED,
+                    StatsLog.BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__PACKAGE_MANAGER_INIT_READY,
+                    SystemClock.elapsedRealtime());
         }
         // Manages A/B OTA dexopting. This is a bootstrap service as we need it to rename
         // A/B artifacts after boot, before anything else might touch/need them.
diff --git a/services/people/java/com/android/server/people/PeopleService.java b/services/people/java/com/android/server/people/PeopleService.java
index ef3da60..9569c6e 100644
--- a/services/people/java/com/android/server/people/PeopleService.java
+++ b/services/people/java/com/android/server/people/PeopleService.java
@@ -137,5 +137,14 @@
                 Slog.e(TAG, "Failed to calling callback" + e);
             }
         }
+
+        @Override
+        public byte[] backupConversationInfos(int userId) {
+            return new byte[0];
+        }
+
+        @Override
+        public void restoreConversationInfos(int userId, String key, byte[] payload) {
+        }
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
index 3d40637..355cada 100644
--- a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
@@ -112,7 +112,7 @@
 import android.net.NetworkStats;
 import android.net.NetworkStatsHistory;
 import android.net.NetworkTemplate;
-import android.net.StringNetworkSpecifier;
+import android.net.TelephonyNetworkSpecifier;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.INetworkManagementService;
@@ -1913,7 +1913,8 @@
         if (!roaming) {
             nc.addCapability(NET_CAPABILITY_NOT_ROAMING);
         }
-        nc.setNetworkSpecifier(new StringNetworkSpecifier(String.valueOf(subId)));
+        nc.setNetworkSpecifier(new TelephonyNetworkSpecifier.Builder()
+                .setSubscriptionId(subId).build());
         return nc;
     }
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java
index bd7d9ec..f5af3ec 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java
@@ -64,7 +64,8 @@
 
         NotificationChannel updatedChannel =
                 new NotificationChannel("a", "", IMPORTANCE_HIGH);
-        when(mConfig.getNotificationChannel(any(), anyInt(), eq("a"), eq(null), eq(false)))
+        when(mConfig.getConversationNotificationChannel(
+                any(), anyInt(), eq("a"), eq(null), eq(true), eq(false)))
                 .thenReturn(updatedChannel);
 
         assertNull(extractor.process(r));
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 62f5230..2ac4642 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -471,16 +471,17 @@
         mTestableLooper.processAllMessages();
     }
 
-    private void setUpPrefsForBubbles(boolean globalEnabled, boolean pkgEnabled,
-            boolean channelEnabled) {
-        mService.setPreferencesHelper(mPreferencesHelper);
-        when(mPreferencesHelper.bubblesEnabled()).thenReturn(globalEnabled);
-        when(mPreferencesHelper.areBubblesAllowed(anyString(), anyInt())).thenReturn(pkgEnabled);
-        when(mPreferencesHelper.getNotificationChannel(
-                anyString(), anyInt(), anyString(), eq(null), anyBoolean())).thenReturn(
-                mTestNotificationChannel);
-        when(mPreferencesHelper.getImportance(anyString(), anyInt())).thenReturn(
-                mTestNotificationChannel.getImportance());
+    private void setUpPrefsForBubbles(String pkg, int uid, boolean globalEnabled,
+            boolean pkgEnabled, boolean channelEnabled) {
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.NOTIFICATION_BUBBLES, globalEnabled ? 1 : 0);
+        mService.mPreferencesHelper.updateBubblesEnabled();
+        assertEquals(globalEnabled, mService.mPreferencesHelper.bubblesEnabled());
+        try {
+            mBinderService.setBubblesAllowed(pkg, uid, pkgEnabled);
+        } catch (RemoteException e) {
+            e.printStackTrace();
+        }
         mTestNotificationChannel.setAllowBubbles(channelEnabled);
     }
 
@@ -1763,8 +1764,8 @@
         Notification.TvExtender tv = new Notification.TvExtender().setChannelId("foo");
         mBinderService.enqueueNotificationWithTag(PKG, PKG, "testTvExtenderChannelOverride_onTv", 0,
                 generateNotificationRecord(null, tv).getNotification(), 0);
-        verify(mPreferencesHelper, times(1)).getNotificationChannel(
-                anyString(), anyInt(), eq("foo"), eq(null), anyBoolean());
+        verify(mPreferencesHelper, times(1)).getConversationNotificationChannel(
+                anyString(), anyInt(), eq("foo"), eq(null), anyBoolean(), anyBoolean());
     }
 
     @Test
@@ -1778,9 +1779,9 @@
         Notification.TvExtender tv = new Notification.TvExtender().setChannelId("foo");
         mBinderService.enqueueNotificationWithTag(PKG, PKG, "testTvExtenderChannelOverride_notOnTv",
                 0, generateNotificationRecord(null, tv).getNotification(), 0);
-        verify(mPreferencesHelper, times(1)).getNotificationChannel(
+        verify(mPreferencesHelper, times(1)).getConversationNotificationChannel(
                 anyString(), anyInt(), eq(mTestNotificationChannel.getId()), eq(null),
-                anyBoolean());
+                anyBoolean(), anyBoolean());
     }
 
     @Test
@@ -4759,7 +4760,7 @@
     @Test
     public void testFlagBubble() throws RemoteException {
         // Bubbles are allowed!
-        setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */);
 
         NotificationRecord nr =
                 generateMessageBubbleNotifRecord(mTestNotificationChannel, "testFlagBubble");
@@ -4778,7 +4779,7 @@
     @Test
     public void testFlagBubble_noFlag_appNotAllowed() throws RemoteException {
         // Bubbles are allowed!
-        setUpPrefsForBubbles(true /* global */, false /* app */, true /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, true /* global */, false /* app */, true /* channel */);
 
         NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel,
                         "testFlagBubble_noFlag_appNotAllowed");
@@ -4797,7 +4798,7 @@
     @Test
     public void testFlagBubbleNotifs_noFlag_whenAppForeground() throws RemoteException {
         // Bubbles are allowed!
-        setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */);
 
         // Notif with bubble metadata but not our other misc requirements
         Notification.Builder nb = new Notification.Builder(mContext,
@@ -4825,7 +4826,7 @@
     @Test
     public void testFlagBubbleNotifs_flag_messaging() throws RemoteException {
         // Bubbles are allowed!
-        setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */);
 
         NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel,
                 "testFlagBubbleNotifs_flag_messaging");
@@ -4842,7 +4843,7 @@
     @Test
     public void testFlagBubbleNotifs_flag_phonecall() throws RemoteException {
         // Bubbles are allowed!
-        setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */);
 
         // Give it bubble metadata
         Notification.BubbleMetadata data = getBubbleMetadataBuilder().build();
@@ -4878,7 +4879,7 @@
     @Test
     public void testFlagBubbleNotifs_noFlag_phonecall_noForegroundService() throws RemoteException {
         // Bubbles are allowed!
-        setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */);
 
         // Give it bubble metadata
         Notification.BubbleMetadata data = getBubbleMetadataBuilder().build();
@@ -4911,7 +4912,7 @@
     @Test
     public void testFlagBubbleNotifs_noFlag_phonecall_noPerson() throws RemoteException {
         // Bubbles are allowed!
-        setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */);
 
         // Give it bubble metadata
         Notification.BubbleMetadata data = getBubbleMetadataBuilder().build();
@@ -4942,7 +4943,7 @@
     @Test
     public void testFlagBubbleNotifs_noFlag_phonecall_noCategory() throws RemoteException {
         // Bubbles are allowed!
-        setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */);
 
         // Give it bubble metadata
         Notification.BubbleMetadata data = getBubbleMetadataBuilder().build();
@@ -4977,7 +4978,7 @@
     @Test
     public void testFlagBubbleNotifs_noFlag_messaging_appNotAllowed() throws RemoteException {
         // Bubbles are NOT allowed!
-        setUpPrefsForBubbles(false /* global */, true /* app */, true /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, true /* global */, false /* app */, true /* channel */);
 
         NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel,
                 "testFlagBubbleNotifs_noFlag_messaging_appNotAllowed");
@@ -4995,7 +4996,7 @@
     @Test
     public void testFlagBubbleNotifs_noFlag_notBubble() throws RemoteException {
         // Bubbles are allowed!
-        setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */);
 
         // Messaging notif WITHOUT bubble metadata
         Notification.Builder nb = getMessageStyleNotifBuilder(false /* addBubbleMetadata */,
@@ -5019,7 +5020,7 @@
     @Test
     public void testFlagBubbleNotifs_noFlag_messaging_channelNotAllowed() throws RemoteException {
         // Bubbles are allowed except on this channel
-        setUpPrefsForBubbles(true /* global */, true /* app */, false /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, false /* channel */);
 
         NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel,
                 "testFlagBubbleNotifs_noFlag_messaging_channelNotAllowed");
@@ -5037,7 +5038,7 @@
     @Test
     public void testFlagBubbleNotifs_noFlag_phonecall_notAllowed() throws RemoteException {
         // Bubbles are not allowed!
-        setUpPrefsForBubbles(false /* global */, true /* app */, true /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, false /* global */, true /* app */, true /* channel */);
 
         // Give it bubble metadata
         Notification.BubbleMetadata data = getBubbleMetadataBuilder().build();
@@ -5072,7 +5073,7 @@
     @Test
     public void testFlagBubbleNotifs_noFlag_phonecall_channelNotAllowed() throws RemoteException {
         // Bubbles are allowed, but not on channel.
-        setUpPrefsForBubbles(true /* global */, true /* app */, false /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, false /* channel */);
 
         // Give it bubble metadata
         Notification.BubbleMetadata data = getBubbleMetadataBuilder().build();
@@ -5280,7 +5281,7 @@
     @Test
     public void testNotificationBubbleChanged_false() throws Exception {
         // Bubbles are allowed!
-        setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */);
 
         // Notif with bubble metadata
         NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel,
@@ -5311,7 +5312,7 @@
     @Test
     public void testNotificationBubbleChanged_true() throws Exception {
         // Bubbles are allowed!
-        setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */);
 
         // Notif that is not a bubble
         NotificationRecord nr = generateNotificationRecord(mTestNotificationChannel,
@@ -5348,7 +5349,7 @@
     @Test
     public void testNotificationBubbleChanged_true_notAllowed() throws Exception {
         // Bubbles are allowed!
-        setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */);
 
         // Notif that is not a bubble
         NotificationRecord nr = generateNotificationRecord(mTestNotificationChannel);
@@ -5547,7 +5548,7 @@
     @Test
     public void testNotificationBubbles_disabled_lowRamDevice() throws Exception {
         // Bubbles are allowed!
-        setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */);
 
         // And we are low ram
         when(mActivityManager.isLowRamDevice()).thenReturn(true);
@@ -5631,7 +5632,7 @@
     public void testNotificationBubbles_flagAutoExpandForeground_fails_notForeground()
             throws Exception {
         // Bubbles are allowed!
-        setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */);
 
         NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel,
                 "testNotificationBubbles_flagAutoExpandForeground_fails_notForeground");
@@ -5661,7 +5662,7 @@
     public void testNotificationBubbles_flagAutoExpandForeground_succeeds_foreground()
             throws RemoteException {
         // Bubbles are allowed!
-        setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */);
 
         NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel,
                 "testNotificationBubbles_flagAutoExpandForeground_succeeds_foreground");
@@ -5691,7 +5692,7 @@
     public void testNotificationBubbles_bubbleChildrenStay_whenGroupSummaryDismissed()
             throws Exception {
         // Bubbles are allowed!
-        setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */);
 
         NotificationRecord nrSummary = addGroupWithBubblesAndValidateAdded(
                 true /* summaryAutoCancel */);
@@ -5714,7 +5715,7 @@
     public void testNotificationBubbles_bubbleChildrenStay_whenGroupSummaryClicked()
             throws Exception {
         // Bubbles are allowed!
-        setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */);
+        setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */);
 
         NotificationRecord nrSummary = addGroupWithBubblesAndValidateAdded(
                 true /* summaryAutoCancel */);
@@ -5836,7 +5837,7 @@
         mBinderService.createConversationNotificationChannelForPackage(PKG, mUid, orig, "friend");
 
         NotificationChannel friendChannel = mBinderService.getConversationNotificationChannel(
-                PKG, 0, PKG, original.getId(), "friend");
+                PKG, 0, PKG, original.getId(), false, "friend");
 
         assertEquals(original.getName(), friendChannel.getName());
         assertEquals(original.getId(), friendChannel.getParentChannelId());
@@ -5874,9 +5875,9 @@
                 conversationId);
 
         NotificationChannel messagesChild = mBinderService.getConversationNotificationChannel(
-                PKG, 0, PKG, messagesParent.getId(), conversationId);
+                PKG, 0, PKG, messagesParent.getId(), false, conversationId);
         NotificationChannel callsChild = mBinderService.getConversationNotificationChannel(
-                PKG, 0, PKG, callsParent.getId(), conversationId);
+                PKG, 0, PKG, callsParent.getId(), false, conversationId);
 
         assertEquals(messagesParent.getId(), messagesChild.getParentChannelId());
         assertEquals(conversationId, messagesChild.getConversationId());
@@ -5887,8 +5888,8 @@
         mBinderService.deleteConversationNotificationChannels(PKG, mUid, conversationId);
 
         assertNull(mBinderService.getConversationNotificationChannel(
-                PKG, 0, PKG, messagesParent.getId(), conversationId));
+                PKG, 0, PKG, messagesParent.getId(), false, conversationId));
         assertNull(mBinderService.getConversationNotificationChannel(
-                PKG, 0, PKG, callsParent.getId(), conversationId));
+                PKG, 0, PKG, callsParent.getId(), false, conversationId));
     }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 7173e0c..7ac45f0 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -354,6 +354,7 @@
         channel2.setGroup(ncg.getId());
         channel2.setVibrationPattern(new long[]{100, 67, 145, 156});
         channel2.setLightColor(Color.BLUE);
+        channel2.setConversationId("id1", "conversation");
 
         mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true);
         mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg2, true);
@@ -2822,8 +2823,20 @@
         friend.setConversationId(parent.getId(), conversationId);
         mHelper.createNotificationChannel(PKG_O, UID_O, friend, true, false);
 
-        compareChannelsParentChild(parent, mHelper.getNotificationChannel(
-                PKG_O, UID_O, parent.getId(), conversationId, false), conversationId);
+        compareChannelsParentChild(parent, mHelper.getConversationNotificationChannel(
+                PKG_O, UID_O, parent.getId(), conversationId, false, false), conversationId);
+    }
+
+    @Test
+    public void testGetNotificationChannel_conversationProvidedByNotCustomizedYet() {
+        String conversationId = "friend";
+
+        NotificationChannel parent =
+                new NotificationChannel("parent", "messages", IMPORTANCE_DEFAULT);
+        mHelper.createNotificationChannel(PKG_O, UID_O, parent, true, false);
+
+        compareChannels(parent, mHelper.getConversationNotificationChannel(
+                PKG_O, UID_O, parent.getId(), conversationId, true, false));
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
index 079c49f..d22502d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
@@ -27,6 +27,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -153,6 +154,19 @@
     }
 
     @Test
+    public void testContainerChanges() {
+        removeGlobalMinSizeRestriction();
+        final ActivityStack stack = new StackBuilder(mRootWindowContainer)
+                .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+        final Task task = stack.getTopMostTask();
+        WindowContainerTransaction t = new WindowContainerTransaction();
+        assertTrue(task.isFocusable());
+        t.setFocusable(stack.mRemoteToken, false);
+        mService.applyContainerTransaction(t);
+        assertFalse(task.isFocusable());
+    }
+
+    @Test
     public void testDisplayWindowListener() {
         final ArrayList<Integer> added = new ArrayList<>();
         final ArrayList<Integer> changed = new ArrayList<>();
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootActivityContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootActivityContainerTests.java
index 04d79ca..ea8d082 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootActivityContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootActivityContainerTests.java
@@ -253,12 +253,12 @@
 
         // Under split screen primary we should be focusable when not minimized
         mRootWindowContainer.setDockedStackMinimized(false);
-        assertTrue(stack.isFocusable());
+        assertTrue(stack.isTopActivityFocusable());
         assertTrue(activity.isFocusable());
 
         // Under split screen primary we should not be focusable when minimized
         mRootWindowContainer.setDockedStackMinimized(true);
-        assertFalse(stack.isFocusable());
+        assertFalse(stack.isTopActivityFocusable());
         assertFalse(activity.isFocusable());
 
         final ActivityStack pinnedStack = mRootWindowContainer.getDefaultDisplay().createStack(
@@ -267,19 +267,19 @@
                 .setStack(pinnedStack).build();
 
         // We should not be focusable when in pinned mode
-        assertFalse(pinnedStack.isFocusable());
+        assertFalse(pinnedStack.isTopActivityFocusable());
         assertFalse(pinnedActivity.isFocusable());
 
         // Add flag forcing focusability.
         pinnedActivity.info.flags |= FLAG_ALWAYS_FOCUSABLE;
 
         // We should not be focusable when in pinned mode
-        assertTrue(pinnedStack.isFocusable());
+        assertTrue(pinnedStack.isTopActivityFocusable());
         assertTrue(pinnedActivity.isFocusable());
 
         // Without the overridding activity, stack should not be focusable.
         pinnedStack.removeChild(pinnedActivity.getTask(), "testFocusability");
-        assertFalse(pinnedStack.isFocusable());
+        assertFalse(pinnedStack.isTopActivityFocusable());
     }
 
     /**
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 8397aa4..b8cd378 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -2069,6 +2069,13 @@
         }
 
         @Override
+        public UsageEvents queryEventsForUser(int userId, long beginTime, long endTime,
+                boolean shouldObfuscateInstantApps) {
+            return UsageStatsService.this.queryEvents(
+                    userId, beginTime, endTime, shouldObfuscateInstantApps);
+        }
+
+        @Override
         public void setLastJobRunTime(String packageName, int userId, long elapsedRealtime) {
             mAppStandby.setLastJobRunTime(packageName, userId, elapsedRealtime);
         }
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
index bde2cfd..7099c09 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
@@ -756,7 +756,9 @@
             return;
         }
 
-        model.setStopped();
+        if (event.status != SoundTrigger.RECOGNITION_STATUS_GET_STATE_RESPONSE) {
+            model.setStopped();
+        }
 
         try {
             callback.onGenericSoundTriggerDetected((GenericRecognitionEvent) event);
@@ -900,7 +902,9 @@
             return;
         }
 
-        modelData.setStopped();
+        if (event.status != SoundTrigger.RECOGNITION_STATUS_GET_STATE_RESPONSE) {
+            modelData.setStopped();
+        }
 
         try {
             modelData.getCallback().onKeyphraseDetected((KeyphraseRecognitionEvent) event);
diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java
index a8852a8..826a89e 100644
--- a/telecomm/java/android/telecom/Call.java
+++ b/telecomm/java/android/telecom/Call.java
@@ -568,6 +568,7 @@
         private final Bundle mExtras;
         private final Bundle mIntentExtras;
         private final long mCreationTimeMillis;
+        private final String mContactDisplayName;
         private final @CallDirection int mCallDirection;
         private final @Connection.VerificationStatus int mCallerNumberVerificationStatus;
 
@@ -872,6 +873,17 @@
         }
 
         /**
+         * Returns the name of the caller on the remote end, as derived from a
+         * {@link android.provider.ContactsContract} lookup of the call's handle.
+         * @return The name of the caller, or {@code null} if the lookup is not yet complete, if
+         *         there's no contacts entry for the caller, or if the {@link InCallService} does
+         *         not hold the {@link android.Manifest.permission#READ_CONTACTS} permission.
+         */
+        public @Nullable String getContactDisplayName() {
+            return mContactDisplayName;
+        }
+
+        /**
          * Indicates whether the call is an incoming or outgoing call.
          * @return The call's direction.
          */
@@ -909,6 +921,7 @@
                         areBundlesEqual(mExtras, d.mExtras) &&
                         areBundlesEqual(mIntentExtras, d.mIntentExtras) &&
                         Objects.equals(mCreationTimeMillis, d.mCreationTimeMillis) &&
+                        Objects.equals(mContactDisplayName, d.mContactDisplayName) &&
                         Objects.equals(mCallDirection, d.mCallDirection) &&
                         Objects.equals(mCallerNumberVerificationStatus,
                                 d.mCallerNumberVerificationStatus);
@@ -933,6 +946,7 @@
                             mExtras,
                             mIntentExtras,
                             mCreationTimeMillis,
+                            mContactDisplayName,
                             mCallDirection,
                             mCallerNumberVerificationStatus);
         }
@@ -955,6 +969,7 @@
                 Bundle extras,
                 Bundle intentExtras,
                 long creationTimeMillis,
+                String contactDisplayName,
                 int callDirection,
                 int callerNumberVerificationStatus) {
             mTelecomCallId = telecomCallId;
@@ -973,6 +988,7 @@
             mExtras = extras;
             mIntentExtras = intentExtras;
             mCreationTimeMillis = creationTimeMillis;
+            mContactDisplayName = contactDisplayName;
             mCallDirection = callDirection;
             mCallerNumberVerificationStatus = callerNumberVerificationStatus;
         }
@@ -996,6 +1012,7 @@
                     parcelableCall.getExtras(),
                     parcelableCall.getIntentExtras(),
                     parcelableCall.getCreationTimeMillis(),
+                    parcelableCall.getContactDisplayName(),
                     parcelableCall.getCallDirection(),
                     parcelableCall.getCallerNumberVerificationStatus());
         }
@@ -1445,6 +1462,7 @@
 
     private boolean mChildrenCached;
     private String mParentId = null;
+    private String mActiveGenericConferenceChild = null;
     private int mState;
     private List<String> mCannedTextResponses = null;
     private String mCallingPackage;
@@ -1943,6 +1961,20 @@
     }
 
     /**
+     * Returns the child {@link Call} in a generic conference that is currently active.
+     * For calls that are not generic conferences, or when the generic conference has more than
+     * 2 children, returns {@code null}.
+     * @see Details#PROPERTY_GENERIC_CONFERENCE
+     * @return The active child call.
+     */
+    public @Nullable Call getGenericConferenceActiveChildCall() {
+        if (mActiveGenericConferenceChild != null) {
+            return mPhone.internalGetCallByTelecomId(mActiveGenericConferenceChild);
+        }
+        return null;
+    }
+
+    /**
      * Obtains a list of canned, pre-configured message responses to present to the user as
      * ways of rejecting this {@code Call} using via a text message.
      *
@@ -2190,6 +2222,13 @@
             mChildrenCached = false;
         }
 
+        String activeChildCallId = parcelableCall.getActiveChildCallId();
+        boolean activeChildChanged = !Objects.equals(activeChildCallId,
+                mActiveGenericConferenceChild);
+        if (activeChildChanged) {
+            mActiveGenericConferenceChild = activeChildCallId;
+        }
+
         List<String> conferenceableCallIds = parcelableCall.getConferenceableCallIds();
         List<Call> conferenceableCalls = new ArrayList<Call>(conferenceableCallIds.size());
         for (String otherId : conferenceableCallIds) {
@@ -2249,7 +2288,7 @@
         if (parentChanged) {
             fireParentChanged(getParent());
         }
-        if (childrenChanged) {
+        if (childrenChanged || activeChildChanged) {
             fireChildrenChanged(getChildren());
         }
         if (isRttChanged) {
diff --git a/telecomm/java/android/telecom/ParcelableCall.java b/telecomm/java/android/telecom/ParcelableCall.java
index be4e2f4..415a817 100644
--- a/telecomm/java/android/telecom/ParcelableCall.java
+++ b/telecomm/java/android/telecom/ParcelableCall.java
@@ -16,6 +16,7 @@
 
 package android.telecom;
 
+import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.net.Uri;
 import android.os.Build;
@@ -36,6 +37,265 @@
  * @hide
  */
 public final class ParcelableCall implements Parcelable {
+
+    public static class ParcelableCallBuilder {
+        private String mId;
+        private int mState;
+        private DisconnectCause mDisconnectCause;
+        private List<String> mCannedSmsResponses;
+        private int mCapabilities;
+        private int mProperties;
+        private int mSupportedAudioRoutes;
+        private long mConnectTimeMillis;
+        private Uri mHandle;
+        private int mHandlePresentation;
+        private String mCallerDisplayName;
+        private int mCallerDisplayNamePresentation;
+        private GatewayInfo mGatewayInfo;
+        private PhoneAccountHandle mAccountHandle;
+        private boolean mIsVideoCallProviderChanged;
+        private IVideoProvider mVideoCallProvider;
+        private boolean mIsRttCallChanged;
+        private ParcelableRttCall mRttCall;
+        private String mParentCallId;
+        private List<String> mChildCallIds;
+        private StatusHints mStatusHints;
+        private int mVideoState;
+        private List<String> mConferenceableCallIds;
+        private Bundle mIntentExtras;
+        private Bundle mExtras;
+        private long mCreationTimeMillis;
+        private int mCallDirection;
+        private int mCallerNumberVerificationStatus;
+        private String mContactDisplayName;
+        private String mActiveChildCallId;
+
+        public ParcelableCallBuilder setId(String id) {
+            mId = id;
+            return this;
+        }
+
+        public ParcelableCallBuilder setState(int state) {
+            mState = state;
+            return this;
+        }
+
+        public ParcelableCallBuilder setDisconnectCause(DisconnectCause disconnectCause) {
+            mDisconnectCause = disconnectCause;
+            return this;
+        }
+
+        public ParcelableCallBuilder setCannedSmsResponses(List<String> cannedSmsResponses) {
+            mCannedSmsResponses = cannedSmsResponses;
+            return this;
+        }
+
+        public ParcelableCallBuilder setCapabilities(int capabilities) {
+            mCapabilities = capabilities;
+            return this;
+        }
+
+        public ParcelableCallBuilder setProperties(int properties) {
+            mProperties = properties;
+            return this;
+        }
+
+        public ParcelableCallBuilder setSupportedAudioRoutes(int supportedAudioRoutes) {
+            mSupportedAudioRoutes = supportedAudioRoutes;
+            return this;
+        }
+
+        public ParcelableCallBuilder setConnectTimeMillis(long connectTimeMillis) {
+            mConnectTimeMillis = connectTimeMillis;
+            return this;
+        }
+
+        public ParcelableCallBuilder setHandle(Uri handle) {
+            mHandle = handle;
+            return this;
+        }
+
+        public ParcelableCallBuilder setHandlePresentation(int handlePresentation) {
+            mHandlePresentation = handlePresentation;
+            return this;
+        }
+
+        public ParcelableCallBuilder setCallerDisplayName(String callerDisplayName) {
+            mCallerDisplayName = callerDisplayName;
+            return this;
+        }
+
+        public ParcelableCallBuilder setCallerDisplayNamePresentation(
+                int callerDisplayNamePresentation) {
+            mCallerDisplayNamePresentation = callerDisplayNamePresentation;
+            return this;
+        }
+
+        public ParcelableCallBuilder setGatewayInfo(GatewayInfo gatewayInfo) {
+            mGatewayInfo = gatewayInfo;
+            return this;
+        }
+
+        public ParcelableCallBuilder setAccountHandle(PhoneAccountHandle accountHandle) {
+            mAccountHandle = accountHandle;
+            return this;
+        }
+
+        public ParcelableCallBuilder setIsVideoCallProviderChanged(
+                boolean isVideoCallProviderChanged) {
+            mIsVideoCallProviderChanged = isVideoCallProviderChanged;
+            return this;
+        }
+
+        public ParcelableCallBuilder setVideoCallProvider(IVideoProvider videoCallProvider) {
+            mVideoCallProvider = videoCallProvider;
+            return this;
+        }
+
+        public ParcelableCallBuilder setIsRttCallChanged(boolean isRttCallChanged) {
+            mIsRttCallChanged = isRttCallChanged;
+            return this;
+        }
+
+        public ParcelableCallBuilder setRttCall(ParcelableRttCall rttCall) {
+            mRttCall = rttCall;
+            return this;
+        }
+
+        public ParcelableCallBuilder setParentCallId(String parentCallId) {
+            mParentCallId = parentCallId;
+            return this;
+        }
+
+        public ParcelableCallBuilder setChildCallIds(List<String> childCallIds) {
+            mChildCallIds = childCallIds;
+            return this;
+        }
+
+        public ParcelableCallBuilder setStatusHints(StatusHints statusHints) {
+            mStatusHints = statusHints;
+            return this;
+        }
+
+        public ParcelableCallBuilder setVideoState(int videoState) {
+            mVideoState = videoState;
+            return this;
+        }
+
+        public ParcelableCallBuilder setConferenceableCallIds(
+                List<String> conferenceableCallIds) {
+            mConferenceableCallIds = conferenceableCallIds;
+            return this;
+        }
+
+        public ParcelableCallBuilder setIntentExtras(Bundle intentExtras) {
+            mIntentExtras = intentExtras;
+            return this;
+        }
+
+        public ParcelableCallBuilder setExtras(Bundle extras) {
+            mExtras = extras;
+            return this;
+        }
+
+        public ParcelableCallBuilder setCreationTimeMillis(long creationTimeMillis) {
+            mCreationTimeMillis = creationTimeMillis;
+            return this;
+        }
+
+        public ParcelableCallBuilder setCallDirection(int callDirection) {
+            mCallDirection = callDirection;
+            return this;
+        }
+
+        public ParcelableCallBuilder setCallerNumberVerificationStatus(
+                int callerNumberVerificationStatus) {
+            mCallerNumberVerificationStatus = callerNumberVerificationStatus;
+            return this;
+        }
+
+        public ParcelableCallBuilder setContactDisplayName(String contactDisplayName) {
+            mContactDisplayName = contactDisplayName;
+            return this;
+        }
+
+        public ParcelableCallBuilder setActiveChildCallId(String activeChildCallId) {
+            mActiveChildCallId = activeChildCallId;
+            return this;
+        }
+
+        public ParcelableCall createParcelableCall() {
+            return new ParcelableCall(
+                    mId,
+                    mState,
+                    mDisconnectCause,
+                    mCannedSmsResponses,
+                    mCapabilities,
+                    mProperties,
+                    mSupportedAudioRoutes,
+                    mConnectTimeMillis,
+                    mHandle,
+                    mHandlePresentation,
+                    mCallerDisplayName,
+                    mCallerDisplayNamePresentation,
+                    mGatewayInfo,
+                    mAccountHandle,
+                    mIsVideoCallProviderChanged,
+                    mVideoCallProvider,
+                    mIsRttCallChanged,
+                    mRttCall,
+                    mParentCallId,
+                    mChildCallIds,
+                    mStatusHints,
+                    mVideoState,
+                    mConferenceableCallIds,
+                    mIntentExtras,
+                    mExtras,
+                    mCreationTimeMillis,
+                    mCallDirection,
+                    mCallerNumberVerificationStatus,
+                    mContactDisplayName,
+                    mActiveChildCallId);
+        }
+
+        public static ParcelableCallBuilder fromParcelableCall(ParcelableCall parcelableCall) {
+            ParcelableCallBuilder newBuilder = new ParcelableCallBuilder();
+            newBuilder.mId = parcelableCall.mId;
+            newBuilder.mState = parcelableCall.mState;
+            newBuilder.mDisconnectCause = parcelableCall.mDisconnectCause;
+            newBuilder.mCannedSmsResponses = parcelableCall.mCannedSmsResponses;
+            newBuilder.mCapabilities = parcelableCall.mCapabilities;
+            newBuilder.mProperties = parcelableCall.mProperties;
+            newBuilder.mSupportedAudioRoutes = parcelableCall.mSupportedAudioRoutes;
+            newBuilder.mConnectTimeMillis = parcelableCall.mConnectTimeMillis;
+            newBuilder.mHandle = parcelableCall.mHandle;
+            newBuilder.mHandlePresentation = parcelableCall.mHandlePresentation;
+            newBuilder.mCallerDisplayName = parcelableCall.mCallerDisplayName;
+            newBuilder.mCallerDisplayNamePresentation =
+                    parcelableCall.mCallerDisplayNamePresentation;
+            newBuilder.mGatewayInfo = parcelableCall.mGatewayInfo;
+            newBuilder.mAccountHandle = parcelableCall.mAccountHandle;
+            newBuilder.mIsVideoCallProviderChanged = parcelableCall.mIsVideoCallProviderChanged;
+            newBuilder.mVideoCallProvider = parcelableCall.mVideoCallProvider;
+            newBuilder.mIsRttCallChanged = parcelableCall.mIsRttCallChanged;
+            newBuilder.mRttCall = parcelableCall.mRttCall;
+            newBuilder.mParentCallId = parcelableCall.mParentCallId;
+            newBuilder.mChildCallIds = parcelableCall.mChildCallIds;
+            newBuilder.mStatusHints = parcelableCall.mStatusHints;
+            newBuilder.mVideoState = parcelableCall.mVideoState;
+            newBuilder.mConferenceableCallIds = parcelableCall.mConferenceableCallIds;
+            newBuilder.mIntentExtras = parcelableCall.mIntentExtras;
+            newBuilder.mExtras = parcelableCall.mExtras;
+            newBuilder.mCreationTimeMillis = parcelableCall.mCreationTimeMillis;
+            newBuilder.mCallDirection = parcelableCall.mCallDirection;
+            newBuilder.mCallerNumberVerificationStatus =
+                    parcelableCall.mCallerNumberVerificationStatus;
+            newBuilder.mContactDisplayName = parcelableCall.mContactDisplayName;
+            newBuilder.mActiveChildCallId = parcelableCall.mActiveChildCallId;
+            return newBuilder;
+        }
+    }
+
     private final String mId;
     private final int mState;
     private final DisconnectCause mDisconnectCause;
@@ -65,6 +325,8 @@
     private final long mCreationTimeMillis;
     private final int mCallDirection;
     private final int mCallerNumberVerificationStatus;
+    private final String mContactDisplayName;
+    private final String mActiveChildCallId; // Only valid for CDMA conferences
 
     public ParcelableCall(
             String id,
@@ -94,7 +356,10 @@
             Bundle extras,
             long creationTimeMillis,
             int callDirection,
-            int callerNumberVerificationStatus) {
+            int callerNumberVerificationStatus,
+            String contactDisplayName,
+            String activeChildCallId
+    ) {
         mId = id;
         mState = state;
         mDisconnectCause = disconnectCause;
@@ -123,6 +388,8 @@
         mCreationTimeMillis = creationTimeMillis;
         mCallDirection = callDirection;
         mCallerNumberVerificationStatus = callerNumberVerificationStatus;
+        mContactDisplayName = contactDisplayName;
+        mActiveChildCallId = activeChildCallId;
     }
 
     /** The unique ID of the call. */
@@ -332,6 +599,21 @@
         return mCallerNumberVerificationStatus;
     }
 
+    /**
+     * @return the name of the remote party as derived from a contacts DB lookup.
+     */
+    public @Nullable String getContactDisplayName() {
+        return mContactDisplayName;
+    }
+
+    /**
+     * @return On a CDMA conference with two participants, returns the ID of the child call that's
+     *         currently active.
+     */
+    public @Nullable String getActiveChildCallId() {
+        return mActiveChildCallId;
+    }
+
     /** Responsible for creating ParcelableCall objects for deserialized Parcels. */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     public static final @android.annotation.NonNull Parcelable.Creator<ParcelableCall> CREATOR =
@@ -371,35 +653,40 @@
             long creationTimeMillis = source.readLong();
             int callDirection = source.readInt();
             int callerNumberVerificationStatus = source.readInt();
-            return new ParcelableCall(
-                    id,
-                    state,
-                    disconnectCause,
-                    cannedSmsResponses,
-                    capabilities,
-                    properties,
-                    supportedAudioRoutes,
-                    connectTimeMillis,
-                    handle,
-                    handlePresentation,
-                    callerDisplayName,
-                    callerDisplayNamePresentation,
-                    gatewayInfo,
-                    accountHandle,
-                    isVideoCallProviderChanged,
-                    videoCallProvider,
-                    isRttCallChanged,
-                    rttCall,
-                    parentCallId,
-                    childCallIds,
-                    statusHints,
-                    videoState,
-                    conferenceableCallIds,
-                    intentExtras,
-                    extras,
-                    creationTimeMillis,
-                    callDirection,
-                    callerNumberVerificationStatus);
+            String contactDisplayName = source.readString();
+            String activeChildCallId = source.readString();
+            return new ParcelableCallBuilder()
+                    .setId(id)
+                    .setState(state)
+                    .setDisconnectCause(disconnectCause)
+                    .setCannedSmsResponses(cannedSmsResponses)
+                    .setCapabilities(capabilities)
+                    .setProperties(properties)
+                    .setSupportedAudioRoutes(supportedAudioRoutes)
+                    .setConnectTimeMillis(connectTimeMillis)
+                    .setHandle(handle)
+                    .setHandlePresentation(handlePresentation)
+                    .setCallerDisplayName(callerDisplayName)
+                    .setCallerDisplayNamePresentation(callerDisplayNamePresentation)
+                    .setGatewayInfo(gatewayInfo)
+                    .setAccountHandle(accountHandle)
+                    .setIsVideoCallProviderChanged(isVideoCallProviderChanged)
+                    .setVideoCallProvider(videoCallProvider)
+                    .setIsRttCallChanged(isRttCallChanged)
+                    .setRttCall(rttCall)
+                    .setParentCallId(parentCallId)
+                    .setChildCallIds(childCallIds)
+                    .setStatusHints(statusHints)
+                    .setVideoState(videoState)
+                    .setConferenceableCallIds(conferenceableCallIds)
+                    .setIntentExtras(intentExtras)
+                    .setExtras(extras)
+                    .setCreationTimeMillis(creationTimeMillis)
+                    .setCallDirection(callDirection)
+                    .setCallerNumberVerificationStatus(callerNumberVerificationStatus)
+                    .setContactDisplayName(contactDisplayName)
+                    .setActiveChildCallId(activeChildCallId)
+                    .createParcelableCall();
         }
 
         @Override
@@ -446,6 +733,8 @@
         destination.writeLong(mCreationTimeMillis);
         destination.writeInt(mCallDirection);
         destination.writeInt(mCallerNumberVerificationStatus);
+        destination.writeString(mContactDisplayName);
+        destination.writeString(mActiveChildCallId);
     }
 
     @Override
diff --git a/telephony/java/android/telephony/BarringInfo.aidl b/telephony/java/android/telephony/BarringInfo.aidl
new file mode 100644
index 0000000..50ddf6b
--- /dev/null
+++ b/telephony/java/android/telephony/BarringInfo.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2019 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.
+ */
+
+/** @hide */
+package android.telephony;
+
+parcelable BarringInfo;
diff --git a/telephony/java/android/telephony/BarringInfo.java b/telephony/java/android/telephony/BarringInfo.java
new file mode 100644
index 0000000..5419c3c
--- /dev/null
+++ b/telephony/java/android/telephony/BarringInfo.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright 2019 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.telephony;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Provides the barring configuration for a particular service type.
+ *
+ * Provides indication about the barring of a particular service for use. Certain barring types
+ * are only valid for certain technology families. Any service that does not have a barring
+ * configuration is unbarred by default.
+ */
+public final class BarringInfo implements Parcelable {
+
+    /**
+     * Barring Service Type
+     *
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = "BARRING_SERVICE_TYPE_", value = {
+            BARRING_SERVICE_TYPE_CS_SERVICE,
+            BARRING_SERVICE_TYPE_PS_SERVICE,
+            BARRING_SERVICE_TYPE_CS_VOICE,
+            BARRING_SERVICE_TYPE_MO_SIGNALLING,
+            BARRING_SERVICE_TYPE_MO_DATA,
+            BARRING_SERVICE_TYPE_CS_FALLBACK,
+            BARRING_SERVICE_TYPE_MMTEL_VOICE,
+            BARRING_SERVICE_TYPE_MMTEL_VIDEO,
+            BARRING_SERVICE_TYPE_EMERGENCY,
+            BARRING_SERVICE_TYPE_SMS})
+    public @interface BarringServiceType {}
+
+    /* Applicabe to UTRAN */
+    /** Barring indicator for circuit-switched service; applicable to UTRAN */
+    public static final int BARRING_SERVICE_TYPE_CS_SERVICE =
+            android.hardware.radio.V1_5.BarringServiceType.CS_SERVICE;
+    /** Barring indicator for packet-switched service; applicable to UTRAN */
+    public static final int BARRING_SERVICE_TYPE_PS_SERVICE =
+            android.hardware.radio.V1_5.BarringServiceType.PS_SERVICE;
+    /** Barring indicator for circuit-switched voice service; applicable to UTRAN */
+    public static final int BARRING_SERVICE_TYPE_CS_VOICE =
+            android.hardware.radio.V1_5.BarringServiceType.CS_VOICE;
+
+    /* Applicable to EUTRAN, NGRAN */
+    /** Barring indicator for mobile-originated signalling; applicable to EUTRAN and NGRAN */
+    public static final int BARRING_SERVICE_TYPE_MO_SIGNALLING =
+            android.hardware.radio.V1_5.BarringServiceType.MO_SIGNALLING;
+    /** Barring indicator for mobile-originated data traffic; applicable to EUTRAN and NGRAN */
+    public static final int BARRING_SERVICE_TYPE_MO_DATA =
+            android.hardware.radio.V1_5.BarringServiceType.MO_DATA;
+    /** Barring indicator for circuit-switched fallback for voice; applicable to EUTRAN and NGRAN */
+    public static final int BARRING_SERVICE_TYPE_CS_FALLBACK =
+            android.hardware.radio.V1_5.BarringServiceType.CS_FALLBACK;
+    /** Barring indicator for MMTEL (IMS) voice; applicable to EUTRAN and NGRAN */
+    public static final int BARRING_SERVICE_TYPE_MMTEL_VOICE =
+            android.hardware.radio.V1_5.BarringServiceType.MMTEL_VOICE;
+    /** Barring indicator for MMTEL (IMS) video; applicable to EUTRAN and NGRAN */
+    public static final int BARRING_SERVICE_TYPE_MMTEL_VIDEO =
+            android.hardware.radio.V1_5.BarringServiceType.MMTEL_VIDEO;
+
+    /* Applicable to UTRAN, EUTRAN, NGRAN */
+    /** Barring indicator for emergency services; applicable to UTRAN, EUTRAN, and NGRAN */
+    public static final int BARRING_SERVICE_TYPE_EMERGENCY =
+            android.hardware.radio.V1_5.BarringServiceType.EMERGENCY;
+    /** Barring indicator for SMS sending; applicable to UTRAN, EUTRAN, and NGRAN */
+    public static final int BARRING_SERVICE_TYPE_SMS =
+            android.hardware.radio.V1_5.BarringServiceType.SMS;
+
+    //TODO: add barring constants for Operator-Specific barring codes
+
+    /** Describe the current barring configuration of a cell */
+    public static final class BarringServiceInfo implements Parcelable {
+        /**
+         * Barring Type
+         * @hide
+         */
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(prefix = "BARRING_TYPE_", value =
+                    {BARRING_TYPE_NONE,
+                    BARRING_TYPE_UNCONDITIONAL,
+                    BARRING_TYPE_CONDITIONAL,
+                    BARRING_TYPE_UNKNOWN})
+        public @interface BarringType {}
+
+        /** Barring is inactive */
+        public static final int BARRING_TYPE_NONE = android.hardware.radio.V1_5.BarringType.NONE;
+        /** The service is barred */
+        public static final int BARRING_TYPE_UNCONDITIONAL =
+                android.hardware.radio.V1_5.BarringType.UNCONDITIONAL;
+        /** The service may be barred based on additional factors */
+        public static final int BARRING_TYPE_CONDITIONAL =
+                android.hardware.radio.V1_5.BarringType.CONDITIONAL;
+
+        /** If a modem does not report barring info, then the barring type will be UNKNOWN */
+        public static final int BARRING_TYPE_UNKNOWN = -1;
+
+        private final @BarringType int mBarringType;
+
+        private final boolean mIsConditionallyBarred;
+        private final int mConditionalBarringFactor;
+        private final int mConditionalBarringTimeSeconds;
+
+        /** @hide */
+        public BarringServiceInfo(@BarringType int type) {
+            this(type, false, 0, 0);
+        }
+
+        /** @hide */
+        @TestApi
+        public BarringServiceInfo(@BarringType int barringType, boolean isConditionallyBarred,
+                int conditionalBarringFactor, int conditionalBarringTimeSeconds) {
+            mBarringType = barringType;
+            mIsConditionallyBarred = isConditionallyBarred;
+            mConditionalBarringFactor = conditionalBarringFactor;
+            mConditionalBarringTimeSeconds = conditionalBarringTimeSeconds;
+        }
+
+        public @BarringType int getBarringType() {
+            return mBarringType;
+        }
+
+        /**
+         * @return true if the conditional barring parameters have resulted in the service being
+         *         barred; false if the service has either not been evaluated for conditional
+         *         barring or has been evaluated and isn't barred.
+         */
+        public boolean isConditionallyBarred() {
+            return mIsConditionallyBarred;
+        }
+
+        /**
+         * @return the conditional barring factor as a percentage 0-100, which is the probability of
+         *         a random device being barred for the service type.
+         */
+        public int getConditionalBarringFactor() {
+            return mConditionalBarringFactor;
+        }
+
+        /**
+         * @return the conditional barring time seconds, which is the interval between successive
+         *         evaluations for conditional barring based on the barring factor.
+         */
+        @SuppressLint("MethodNameUnits")
+        public int getConditionalBarringTimeSeconds() {
+            return mConditionalBarringTimeSeconds;
+        }
+
+        /**
+         * Return whether a service is currently barred based on the BarringInfo
+         *
+         * @return true if the service is currently being barred, otherwise false
+         */
+        public boolean isBarred() {
+            return mBarringType == BarringServiceInfo.BARRING_TYPE_UNCONDITIONAL
+                    || (mBarringType == BarringServiceInfo.BARRING_TYPE_CONDITIONAL
+                            && mIsConditionallyBarred);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mBarringType, mIsConditionallyBarred,
+                    mConditionalBarringFactor, mConditionalBarringTimeSeconds);
+        }
+
+        @Override
+        public boolean equals(Object rhs) {
+            if (!(rhs instanceof BarringServiceInfo)) return false;
+
+            BarringServiceInfo other = (BarringServiceInfo) rhs;
+            return mBarringType == other.mBarringType
+                    && mIsConditionallyBarred == other.mIsConditionallyBarred
+                    && mConditionalBarringFactor == other.mConditionalBarringFactor
+                    && mConditionalBarringTimeSeconds == other.mConditionalBarringTimeSeconds;
+        }
+
+        /** @hide */
+        public BarringServiceInfo(Parcel p) {
+            mBarringType = p.readInt();
+            mIsConditionallyBarred = p.readBoolean();
+            mConditionalBarringFactor = p.readInt();
+            mConditionalBarringTimeSeconds = p.readInt();
+        }
+
+        @Override
+        public void writeToParcel(@NonNull Parcel dest, int flags) {
+            dest.writeInt(mBarringType);
+            dest.writeBoolean(mIsConditionallyBarred);
+            dest.writeInt(mConditionalBarringFactor);
+            dest.writeInt(mConditionalBarringTimeSeconds);
+        }
+
+        /* @inheritDoc */
+        public static final @NonNull Parcelable.Creator<BarringServiceInfo> CREATOR =
+                new Parcelable.Creator<BarringServiceInfo>() {
+                    @Override
+                    public BarringServiceInfo createFromParcel(Parcel source) {
+                        return new BarringServiceInfo(source);
+                    }
+
+                    @Override
+                    public BarringServiceInfo[] newArray(int size) {
+                        return new BarringServiceInfo[size];
+                    }
+                };
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+    }
+
+    private CellIdentity mCellIdentity;
+
+    // A SparseArray potentially mapping each BarringService type to a BarringServiceInfo config
+    // that describes the current barring status of that particular service.
+    private SparseArray<BarringServiceInfo> mBarringServiceInfos;
+
+    /** @hide */
+    @TestApi
+    @SystemApi
+    public BarringInfo() {
+        mBarringServiceInfos = new SparseArray<>();
+    }
+
+    /**
+     * Constructor for new BarringInfo instances.
+     *
+     * @hide
+     */
+    @TestApi
+    public BarringInfo(@Nullable CellIdentity barringCellId,
+            @NonNull SparseArray<BarringServiceInfo> barringServiceInfos) {
+        mCellIdentity = barringCellId;
+        mBarringServiceInfos = barringServiceInfos;
+    }
+
+    /** @hide */
+    public static BarringInfo create(
+            @NonNull android.hardware.radio.V1_5.CellIdentity halBarringCellId,
+            @NonNull List<android.hardware.radio.V1_5.BarringInfo> halBarringInfos) {
+        CellIdentity ci = CellIdentity.create(halBarringCellId);
+        SparseArray<BarringServiceInfo> serviceInfos = new SparseArray<>();
+
+        for (android.hardware.radio.V1_5.BarringInfo halBarringInfo : halBarringInfos) {
+            if (halBarringInfo.type == android.hardware.radio.V1_5.BarringType.CONDITIONAL) {
+                if (halBarringInfo.typeSpecificInfo.getDiscriminator()
+                        != android.hardware.radio.V1_5.BarringTypeSpecificInfo
+                                .hidl_discriminator.conditionalBarringInfo) {
+                    // this is an error case where the barring info is conditional but the
+                    // conditional barring fields weren't included
+                    continue;
+                }
+                android.hardware.radio.V1_5.ConditionalBarringInfo conditionalInfo =
+                        halBarringInfo.typeSpecificInfo.conditionalBarringInfo();
+                serviceInfos.put(
+                        halBarringInfo.service, new BarringServiceInfo(
+                                halBarringInfo.type, // will always be CONDITIONAL here
+                                conditionalInfo.isBarred,
+                                conditionalInfo.barringFactor,
+                                conditionalInfo.barringTimeSeconds));
+            } else {
+                // Barring type is either NONE or UNCONDITIONAL
+                serviceInfos.put(
+                        halBarringInfo.service, new BarringServiceInfo(halBarringInfo.type,
+                                false, 0, 0));
+            }
+        }
+        return new BarringInfo(ci, serviceInfos);
+    }
+
+    /**
+     * Return whether a service is currently barred based on the BarringInfo
+     *
+     * @param service the service to be checked.
+     * @return true if the service is currently being barred, otherwise false
+     */
+    public boolean isServiceBarred(@BarringServiceType int service) {
+        BarringServiceInfo bsi = mBarringServiceInfos.get(service);
+        return bsi != null && (bsi.isBarred());
+    }
+
+    /**
+     * Get the BarringServiceInfo for a specified service.
+     *
+     * @return a BarringServiceInfo struct describing the current barring status for a service
+     */
+    public @NonNull BarringServiceInfo getBarringServiceInfo(@BarringServiceType int service) {
+        BarringServiceInfo bsi = mBarringServiceInfos.get(service);
+        // If barring is reported but not for a particular service, then we report the barring
+        // type as UNKNOWN; if the modem reports barring info but doesn't report for a particular
+        // service then we can safely assume that the service isn't barred (for instance because
+        // that particular service isn't applicable to the current RAN).
+        return (bsi != null) ? bsi : new BarringServiceInfo(
+                mBarringServiceInfos.size() > 0 ? BarringServiceInfo.BARRING_TYPE_NONE :
+                        BarringServiceInfo.BARRING_TYPE_UNKNOWN);
+    }
+
+    /** @hide */
+    @SystemApi
+    public @NonNull BarringInfo createLocationInfoSanitizedCopy() {
+        return new BarringInfo(mCellIdentity.sanitizeLocationInfo(), mBarringServiceInfos);
+    }
+
+    /** @hide */
+    public BarringInfo(Parcel p) {
+        mCellIdentity = p.readParcelable(CellIdentity.class.getClassLoader());
+        mBarringServiceInfos = p.readSparseArray(BarringServiceInfo.class.getClassLoader());
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeParcelable(mCellIdentity, flags);
+        dest.writeSparseArray(mBarringServiceInfos);
+    }
+
+    public static final @NonNull Parcelable.Creator<BarringInfo> CREATOR =
+            new Parcelable.Creator<BarringInfo>() {
+                @Override
+                public BarringInfo createFromParcel(Parcel source) {
+                    return new BarringInfo(source);
+                }
+
+                @Override
+                public BarringInfo[] newArray(int size) {
+                    return new BarringInfo[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = mCellIdentity != null ? mCellIdentity.hashCode() : 7;
+        for (int i = 0; i < mBarringServiceInfos.size(); i++) {
+            hash = hash + 15 * mBarringServiceInfos.keyAt(i);
+            hash = hash + 31 * mBarringServiceInfos.valueAt(i).hashCode();
+        }
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object rhs) {
+        if (!(rhs instanceof BarringInfo)) return false;
+
+        BarringInfo bi = (BarringInfo) rhs;
+
+        if (hashCode() != bi.hashCode()) return false;
+
+        if (mBarringServiceInfos.size() != bi.mBarringServiceInfos.size()) return false;
+
+        for (int i = 0; i < mBarringServiceInfos.size(); i++) {
+            if (mBarringServiceInfos.keyAt(i) != bi.mBarringServiceInfos.keyAt(i)) return false;
+            if (!Objects.equals(mBarringServiceInfos.valueAt(i),
+                        bi.mBarringServiceInfos.valueAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "BarringInfo {mCellIdentity=" + mCellIdentity
+               + ", mBarringServiceInfos=" + mBarringServiceInfos + "}";
+    }
+}
diff --git a/telephony/java/android/telephony/PreciseDataConnectionState.java b/telephony/java/android/telephony/PreciseDataConnectionState.java
index 5e2e554..0cfb8c5 100644
--- a/telephony/java/android/telephony/PreciseDataConnectionState.java
+++ b/telephony/java/android/telephony/PreciseDataConnectionState.java
@@ -138,7 +138,7 @@
      * To check the SDK version for {@link PreciseDataConnectionState#getDataConnectionState}.
      */
     @ChangeId
-    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.R)
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
     private static final long GET_DATA_CONNECTION_STATE_CODE_CHANGE = 147600208L;
 
     /**
diff --git a/telephony/java/android/telephony/ServiceState.java b/telephony/java/android/telephony/ServiceState.java
index 2c62d06..b8856bc 100644
--- a/telephony/java/android/telephony/ServiceState.java
+++ b/telephony/java/android/telephony/ServiceState.java
@@ -16,8 +16,6 @@
 
 package android.telephony;
 
-import com.android.telephony.Rlog;
-
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -36,6 +34,8 @@
 import android.telephony.NetworkRegistrationInfo.NRState;
 import android.text.TextUtils;
 
+import com.android.telephony.Rlog;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -584,8 +584,8 @@
      */
     @DuplexMode
     public int getDuplexMode() {
-        // only support LTE duplex mode
-        if (!isLte(getRilDataRadioTechnology())) {
+        // support LTE/NR duplex mode
+        if (!isPsOnlyTech(getRilDataRadioTechnology())) {
             return DUPLEX_MODE_UNKNOWN;
         }
 
@@ -1624,7 +1624,8 @@
      * @return Current data network type
      * @hide
      */
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    @SystemApi
+    @TestApi
     public @NetworkType int getDataNetworkType() {
         final NetworkRegistrationInfo iwlanRegInfo = getNetworkRegistrationInfo(
                 NetworkRegistrationInfo.DOMAIN_PS, AccessNetworkConstants.TRANSPORT_TYPE_WLAN);
@@ -1717,9 +1718,10 @@
     }
 
     /** @hide */
-    public static boolean isLte(int radioTechnology) {
-        return radioTechnology == RIL_RADIO_TECHNOLOGY_LTE ||
-                radioTechnology == RIL_RADIO_TECHNOLOGY_LTE_CA;
+    public static boolean isPsOnlyTech(int radioTechnology) {
+        return radioTechnology == RIL_RADIO_TECHNOLOGY_LTE
+                || radioTechnology == RIL_RADIO_TECHNOLOGY_LTE_CA
+                || radioTechnology == RIL_RADIO_TECHNOLOGY_NR;
     }
 
     /** @hide */
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 7ee68ea..5e31c5a 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -5449,7 +5449,7 @@
      * To check the SDK version for {@link TelephonyManager#getDataState}.
      */
     @ChangeId
-    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.R)
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
     private static final long GET_DATA_STATE_CODE_CHANGE = 147600208L;
 
     /**
@@ -5535,7 +5535,7 @@
      * To check the SDK version for {@link TelephonyManager#listen}.
      */
     @ChangeId
-    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.P)
     private static final long LISTEN_CODE_CHANGE = 147600208L;
 
     /**
@@ -7649,6 +7649,18 @@
             RILConstants.NETWORK_MODE_NR_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA;
 
     /**
+     * The default preferred network mode constant.
+     *
+     * <p> This constant is used in case of nothing is set in
+     * TelephonyProperties#default_network().
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int DEFAULT_PREFERRED_NETWORK_MODE =
+            RILConstants.DEFAULT_PREFERRED_NETWORK_MODE;
+
+    /**
      * Get the preferred network type.
      * Used for device configuration by some CDMA operators.
      *
@@ -10685,6 +10697,7 @@
      * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}.
      *
      * @param enabled control enable or disable carrier data.
+     * @see #resetAllCarrierActions()
      * @hide
      */
     @SystemApi
@@ -10711,6 +10724,7 @@
      * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}.
      *
      * @param enabled control enable or disable radio.
+     * @see #resetAllCarrierActions()
      * @hide
      */
     @SystemApi
@@ -10737,6 +10751,7 @@
      * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}.
      *
      * @param report control start/stop reporting network status.
+     * @see #resetAllCarrierActions()
      * @hide
      */
     @SystemApi
diff --git a/telephony/java/android/telephony/ims/ProvisioningManager.java b/telephony/java/android/telephony/ims/ProvisioningManager.java
index ab22d46..9b739d3 100644
--- a/telephony/java/android/telephony/ims/ProvisioningManager.java
+++ b/telephony/java/android/telephony/ims/ProvisioningManager.java
@@ -25,8 +25,6 @@
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.annotation.WorkerThread;
-import android.content.pm.IPackageManager;
-import android.content.pm.PackageManager;
 import android.os.Binder;
 import android.os.RemoteException;
 import android.telephony.CarrierConfigManager;
@@ -382,10 +380,6 @@
     @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
     public void registerProvisioningChangedCallback(@NonNull @CallbackExecutor Executor executor,
             @NonNull Callback callback) throws ImsException {
-        if (!isImsAvailableOnDevice()) {
-            throw new ImsException("IMS not available on device.",
-                    ImsException.CODE_ERROR_UNSUPPORTED_OPERATION);
-        }
         callback.setExecutor(executor);
         try {
             getITelephony().registerImsProvisioningChangedCallback(mSubId, callback.getBinder());
@@ -618,26 +612,6 @@
 
     }
 
-    private static boolean isImsAvailableOnDevice() {
-        IPackageManager pm = IPackageManager.Stub.asInterface(
-                TelephonyFrameworkInitializer
-                        .getTelephonyServiceManager()
-                        .getPackageManagerServiceRegisterer()
-                        .get());
-        if (pm == null) {
-            // For some reason package manger is not available.. This will fail internally anyways,
-            // so do not throw error and allow.
-            return true;
-        }
-        try {
-            return pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_IMS, 0);
-        } catch (RemoteException e) {
-            // For some reason package manger is not available.. This will fail internally anyways,
-            // so do not throw error and allow.
-        }
-        return true;
-    }
-
     private static ITelephony getITelephony() {
         ITelephony binder = ITelephony.Stub.asInterface(
                 TelephonyFrameworkInitializer
diff --git a/telephony/java/com/android/internal/telephony/RILConstants.java b/telephony/java/com/android/internal/telephony/RILConstants.java
index 284544b..9ee26c2 100644
--- a/telephony/java/com/android/internal/telephony/RILConstants.java
+++ b/telephony/java/com/android/internal/telephony/RILConstants.java
@@ -233,11 +233,14 @@
     /** NR 5G, LTE, TD-SCDMA, CDMA, EVDO, GSM and WCDMA */
     int NETWORK_MODE_NR_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA = 33;
 
+    /** Default preferred network mode */
+    int DEFAULT_PREFERRED_NETWORK_MODE = NETWORK_MODE_WCDMA_PREF;
+
     @UnsupportedAppUsage
     int PREFERRED_NETWORK_MODE = Optional.of(TelephonyProperties.default_network())
             .filter(list -> !list.isEmpty())
             .map(list -> list.get(0))
-            .orElse(NETWORK_MODE_WCDMA_PREF);
+            .orElse(DEFAULT_PREFERRED_NETWORK_MODE);
 
     int BAND_MODE_UNSPECIFIED = 0;      //"unspecified" (selected by baseband automatically)
     int BAND_MODE_EURO = 1;             //"EURO band" (GSM-900 / DCS-1800 / WCDMA-IMT-2000)
@@ -555,4 +558,5 @@
     int RIL_UNSOL_EMERGENCY_NUMBER_LIST = 1102;
     int RIL_UNSOL_UICC_APPLICATIONS_ENABLEMENT_CHANGED = 1103;
     int RIL_UNSOL_REGISTRATION_FAILED = 1104;
+    int RIL_UNSOL_BARRING_INFO_CHANGED = 1105;
 }
diff --git a/tests/net/java/android/net/TelephonyNetworkSpecifierTest.java b/tests/net/java/android/net/TelephonyNetworkSpecifierTest.java
new file mode 100644
index 0000000..47afed4
--- /dev/null
+++ b/tests/net/java/android/net/TelephonyNetworkSpecifierTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2018 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.net;
+
+import static com.android.testutils.ParcelUtilsKt.assertParcelSane;
+
+import static org.junit.Assert.assertEquals;
+
+import android.telephony.SubscriptionManager;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Unit test for {@link android.net.TelephonyNetworkSpecifier}.
+ */
+@SmallTest
+public class TelephonyNetworkSpecifierTest {
+    private static final int TEST_SUBID = 5;
+
+    /**
+     * Validate that IllegalArgumentException will be thrown if build TelephonyNetworkSpecifier
+     * without calling {@link TelephonyNetworkSpecifier.Builder#setSubscriptionId(int)}.
+     */
+    @Test
+    public void testBuilderBuildWithDefault() {
+        try {
+            new TelephonyNetworkSpecifier.Builder().build();
+        } catch (IllegalArgumentException iae) {
+            // expected, test pass
+        }
+    }
+
+    /**
+     * Validate that no exception will be thrown even if pass invalid subscription id to
+     * {@link TelephonyNetworkSpecifier.Builder#setSubscriptionId(int)}.
+     */
+    @Test
+    public void testBuilderBuildWithInvalidSubId() {
+        TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier.Builder()
+                .setSubscriptionId(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+                .build();
+        assertEquals(specifier.getSubscriptionId(), SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+    }
+
+    /**
+     * Validate the correctness of TelephonyNetworkSpecifier when provide valid subId.
+     */
+    @Test
+    public void testBuilderBuildWithValidSubId() {
+        final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier.Builder()
+                .setSubscriptionId(TEST_SUBID)
+                .build();
+        assertEquals(TEST_SUBID, specifier.getSubscriptionId());
+    }
+
+    /**
+     * Validate that parcel marshalling/unmarshalling works.
+     */
+    @Test
+    public void testParcel() {
+        TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier.Builder()
+                .setSubscriptionId(TEST_SUBID)
+                .build();
+        assertParcelSane(specifier, 1 /* fieldCount */);
+    }
+}
diff --git a/tests/net/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java b/tests/net/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
index b783467..de1028c 100644
--- a/tests/net/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
+++ b/tests/net/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
@@ -51,6 +51,7 @@
 import android.net.NetworkPolicyManager;
 import android.net.NetworkTemplate;
 import android.net.StringNetworkSpecifier;
+import android.net.TelephonyNetworkSpecifier;
 import android.os.Handler;
 import android.provider.Settings;
 import android.telephony.TelephonyManager;
@@ -229,7 +230,7 @@
         verify(mCM).registerNetworkCallback(any(), networkCallback.capture(), any());
 
         // Simulate callback after capability changes
-        final NetworkCapabilities capabilities = new NetworkCapabilities()
+        NetworkCapabilities capabilities = new NetworkCapabilities()
                 .addCapability(NET_CAPABILITY_INTERNET)
                 .addTransportType(TRANSPORT_CELLULAR)
                 .setNetworkSpecifier(new StringNetworkSpecifier("234"));
@@ -239,6 +240,19 @@
         networkCallback.getValue().onCapabilitiesChanged(
                 TEST_NETWORK,
                 capabilities);
+
+        // make sure it also works with the new introduced  TelephonyNetworkSpecifier
+        capabilities = new NetworkCapabilities()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addTransportType(TRANSPORT_CELLULAR)
+                .setNetworkSpecifier(new TelephonyNetworkSpecifier.Builder()
+                        .setSubscriptionId(234).build());
+        if (!roaming) {
+            capabilities.addCapability(NET_CAPABILITY_NOT_ROAMING);
+        }
+        networkCallback.getValue().onCapabilitiesChanged(
+                TEST_NETWORK,
+                capabilities);
     }
 
     @Test
diff --git a/wifi/Android.bp b/wifi/Android.bp
index 70c9bef..791da30 100644
--- a/wifi/Android.bp
+++ b/wifi/Android.bp
@@ -97,7 +97,6 @@
     },
     hostdex: true, // for hiddenapi check
     visibility: [
-        "//frameworks/base", // TODO(b/140299412) remove once all dependencies are fixed
         "//frameworks/opt/net/wifi/service:__subpackages__",
     ] + test_access_hidden_api_whitelist,
     apex_available: [
diff --git a/wifi/java/android/net/wifi/WifiConfiguration.java b/wifi/java/android/net/wifi/WifiConfiguration.java
index d4fd903..a9621fc 100644
--- a/wifi/java/android/net/wifi/WifiConfiguration.java
+++ b/wifi/java/android/net/wifi/WifiConfiguration.java
@@ -373,7 +373,6 @@
      * ECDHE_ECDSA
      * ECDHE_RSA
      * </pre>
-     * @hide
      */
     public static class SuiteBCipher {
         private SuiteBCipher() { }