Merge "Make Car Navigation Bar fully transparent" into qt-dev
diff --git a/api/test-current.txt b/api/test-current.txt
index ee6fb51..1a912a1c 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -1917,6 +1917,10 @@
     method public boolean hasSingleFileDescriptor();
   }
 
+  public final class Parcel {
+    method public int readExceptionCode();
+  }
+
   public class ParcelFileDescriptor implements java.io.Closeable android.os.Parcelable {
     method public static java.io.File getFile(java.io.FileDescriptor) throws java.io.IOException;
   }
@@ -1986,6 +1990,7 @@
   public class SystemProperties {
     method @NonNull public static String get(@NonNull String);
     method @NonNull public static String get(@NonNull String, @Nullable String);
+    method public static boolean getBoolean(@NonNull String, boolean);
   }
 
   public final class UserHandle implements android.os.Parcelable {
@@ -2140,6 +2145,36 @@
 
 }
 
+package android.os.image {
+
+  public class DynamicSystemClient {
+    ctor public DynamicSystemClient(@NonNull android.content.Context);
+    method @RequiresPermission("android.permission.INSTALL_DYNAMIC_SYSTEM") public void bind();
+    method public void setOnStatusChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.os.image.DynamicSystemClient.OnStatusChangedListener);
+    method public void setOnStatusChangedListener(@NonNull android.os.image.DynamicSystemClient.OnStatusChangedListener);
+    method @RequiresPermission("android.permission.INSTALL_DYNAMIC_SYSTEM") public void start(@NonNull android.net.Uri, long);
+    method @RequiresPermission("android.permission.INSTALL_DYNAMIC_SYSTEM") public void start(@NonNull android.net.Uri, long, long);
+    method @RequiresPermission("android.permission.INSTALL_DYNAMIC_SYSTEM") public void unbind();
+    field public static final int CAUSE_ERROR_EXCEPTION = 6; // 0x6
+    field public static final int CAUSE_ERROR_INVALID_URL = 4; // 0x4
+    field public static final int CAUSE_ERROR_IO = 3; // 0x3
+    field public static final int CAUSE_ERROR_IPC = 5; // 0x5
+    field public static final int CAUSE_INSTALL_CANCELLED = 2; // 0x2
+    field public static final int CAUSE_INSTALL_COMPLETED = 1; // 0x1
+    field public static final int CAUSE_NOT_SPECIFIED = 0; // 0x0
+    field public static final int STATUS_IN_PROGRESS = 2; // 0x2
+    field public static final int STATUS_IN_USE = 4; // 0x4
+    field public static final int STATUS_NOT_STARTED = 1; // 0x1
+    field public static final int STATUS_READY = 3; // 0x3
+    field public static final int STATUS_UNKNOWN = 0; // 0x0
+  }
+
+  public static interface DynamicSystemClient.OnStatusChangedListener {
+    method public void onStatusChanged(int, int, long, @Nullable Throwable);
+  }
+
+}
+
 package android.os.storage {
 
   public class StorageManager {
@@ -2946,6 +2981,21 @@
     method public E valueAtUnchecked(int);
   }
 
+  public class FeatureFlagUtils {
+    ctor public FeatureFlagUtils();
+    method public static java.util.Map<java.lang.String,java.lang.String> getAllFeatureFlags();
+    method public static boolean isEnabled(android.content.Context, String);
+    method public static void setEnabled(android.content.Context, String, boolean);
+    field public static final String DYNAMIC_SYSTEM = "settings_dynamic_system";
+    field public static final String FFLAG_OVERRIDE_PREFIX = "sys.fflag.override.";
+    field public static final String FFLAG_PREFIX = "sys.fflag.";
+    field public static final String HEARING_AID_SETTINGS = "settings_bluetooth_hearing_aid";
+    field public static final String PERSIST_PREFIX = "persist.sys.fflag.override.";
+    field public static final String PIXEL_WALLPAPER_CATEGORY_SWITCH = "settings_pixel_wallpaper_category_switch";
+    field public static final String SCREENRECORD_LONG_PRESS = "settings_screenrecord_long_press";
+    field public static final String SEAMLESS_TRANSFER = "settings_seamless_transfer";
+  }
+
   public class TimeUtils {
     method public static String formatDuration(long);
   }
diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp
index 16af071..30f4ad4 100644
--- a/cmds/bootanimation/BootAnimation.cpp
+++ b/cmds/bootanimation/BootAnimation.cpp
@@ -69,6 +69,7 @@
 static const char PRODUCT_BOOTANIMATION_DARK_FILE[] = "/product/media/bootanimation-dark.zip";
 static const char PRODUCT_BOOTANIMATION_FILE[] = "/product/media/bootanimation.zip";
 static const char SYSTEM_BOOTANIMATION_FILE[] = "/system/media/bootanimation.zip";
+static const char APEX_BOOTANIMATION_FILE[] = "/apex/com.android.bootanimation/etc/bootanimation.zip";
 static const char PRODUCT_ENCRYPTED_BOOTANIMATION_FILE[] = "/product/media/bootanimation-encrypted.zip";
 static const char SYSTEM_ENCRYPTED_BOOTANIMATION_FILE[] = "/system/media/bootanimation-encrypted.zip";
 static const char OEM_SHUTDOWNANIMATION_FILE[] = "/oem/media/shutdownanimation.zip";
@@ -358,10 +359,10 @@
 
     const bool playDarkAnim = android::base::GetIntProperty("ro.boot.theme", 0) == 1;
     static const char* bootFiles[] =
-        {playDarkAnim ? PRODUCT_BOOTANIMATION_DARK_FILE : PRODUCT_BOOTANIMATION_FILE,
+        {APEX_BOOTANIMATION_FILE, playDarkAnim ? PRODUCT_BOOTANIMATION_DARK_FILE : PRODUCT_BOOTANIMATION_FILE,
          OEM_BOOTANIMATION_FILE, SYSTEM_BOOTANIMATION_FILE};
     static const char* shutdownFiles[] =
-        {PRODUCT_SHUTDOWNANIMATION_FILE, OEM_SHUTDOWNANIMATION_FILE, SYSTEM_SHUTDOWNANIMATION_FILE};
+        {PRODUCT_SHUTDOWNANIMATION_FILE, OEM_SHUTDOWNANIMATION_FILE, SYSTEM_SHUTDOWNANIMATION_FILE, ""};
 
     for (const char* f : (!mShuttingDown ? bootFiles : shutdownFiles)) {
         if (access(f, R_OK) == 0) {
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index 2899d49..495a09f 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -48,6 +48,7 @@
 import "frameworks/base/core/proto/android/stats/enums.proto";
 import "frameworks/base/core/proto/android/stats/intelligence/enums.proto";
 import "frameworks/base/core/proto/android/stats/launcher/launcher.proto";
+import "frameworks/base/core/proto/android/stats/location/location_enums.proto";
 import "frameworks/base/core/proto/android/stats/mediametrics/mediametrics.proto";
 import "frameworks/base/core/proto/android/stats/storage/storage_enums.proto";
 import "frameworks/base/core/proto/android/stats/style/style_enums.proto";
@@ -301,6 +302,7 @@
         ContentCaptureServiceEvents content_capture_service_events = 207;
         ContentCaptureSessionEvents content_capture_session_events = 208;
         ContentCaptureFlushed content_capture_flushed = 209;
+        LocationManagerApiUsageReported location_manager_api_usage_reported = 210;
     }
 
     // Pulled events will start at field 10000.
@@ -6485,3 +6487,60 @@
     // while the app was in the background (only for trusted requests)
     optional int64 trusted_background_duration_millis = 9;
 }
+
+/**
+ * Location Manager API Usage information(e.g. API under usage,
+ * API call's parameters).
+ * Logged from:
+ *  frameworks/base/services/core/java/com/android/server/LocationManagerService.java
+ */
+message LocationManagerApiUsageReported {
+
+    // Indicating if usage starts or usage ends.
+    optional android.stats.location.UsageState state = 1;
+
+    // LocationManagerService's API in use.
+    // We can identify which API from LocationManager is
+    // invoking current LMS API by the combination of
+    // API parameter(e.g. is_listener_null, is_intent_null,
+    // is_location_request_null)
+    optional android.stats.location.LocationManagerServiceApi api_in_use = 2;
+
+    // Name of the package calling the API.
+    optional string calling_package_name = 3;
+
+    // Type of the location provider.
+    optional android.stats.location.ProviderType provider = 4;
+
+    // Quality of the location request
+    optional android.stats.location.LocationRequestQuality quality = 5;
+
+    // The desired interval for active location updates, in milliseconds.
+    // Bucketized to reduce cardinality.
+    optional android.stats.location.LocationRequestIntervalBucket bucketized_interval = 6;
+
+    // Minimum distance between location updates, in meters.
+    // Bucketized to reduce cardinality.
+    optional android.stats.location.SmallestDisplacementBucket
+            bucketized_smallest_displacement = 7;
+
+    // The number of location updates.
+    optional int64 num_updates = 8;
+
+    // The request expiration time, in millisecond since boot.
+    // Bucketized to reduce cardinality.
+    optional android.stats.location.ExpirationBucket
+            bucketized_expire_in = 9;
+
+    // Type of Callback passed in for this API.
+    optional android.stats.location.CallbackType callback_type = 10;
+
+    // The radius of the central point of the alert
+    // region, in meters. Only for API REQUEST_GEOFENCE.
+    // Bucketized to reduce cardinality.
+    optional android.stats.location.GeofenceRadiusBucket bucketized_radius = 11;
+
+    // Activity Importance of API caller.
+    // Categorized to 3 types that are interesting from location's perspective.
+    optional android.stats.location.ActivityImportance activiy_importance = 12;
+}
diff --git a/config/hiddenapi-greylist-packages.txt b/config/hiddenapi-greylist-packages.txt
index cae3bd9..986d259 100644
--- a/config/hiddenapi-greylist-packages.txt
+++ b/config/hiddenapi-greylist-packages.txt
@@ -1,2 +1,43 @@
+gov.nist.core
+gov.nist.core.net
+gov.nist.javax.sip
+gov.nist.javax.sip.address
+gov.nist.javax.sip.clientauthutils
+gov.nist.javax.sip.header
+gov.nist.javax.sip.header.extensions
+gov.nist.javax.sip.header.ims
+gov.nist.javax.sip.message
+gov.nist.javax.sip.parser
+gov.nist.javax.sip.parser.extensions
+gov.nist.javax.sip.parser.ims
+gov.nist.javax.sip.stack
+org.apache.xalan
+org.apache.xalan.extensions
+org.apache.xalan.processor
+org.apache.xalan.res
+org.apache.xalan.serialize
+org.apache.xalan.templates
+org.apache.xalan.transformer
+org.apache.xalan.xslt
+org.apache.xml.dtm
+org.apache.xml.dtm.ref
+org.apache.xml.dtm.ref.dom2dtm
+org.apache.xml.dtm.ref.sax2dtm
+org.apache.xml.res
+org.apache.xml.serializer
+org.apache.xml.serializer.dom3
+org.apache.xml.serializer.utils
+org.apache.xml.utils
+org.apache.xml.utils.res
+org.apache.xpath
+org.apache.xpath.axes
+org.apache.xpath.compiler
+org.apache.xpath.domapi
+org.apache.xpath.functions
+org.apache.xpath.jaxp
+org.apache.xpath.objects
+org.apache.xpath.operations
+org.apache.xpath.patterns
+org.apache.xpath.res
 org.ccil.cowan.tagsoup
 org.ccil.cowan.tagsoup.jaxp
diff --git a/config/hiddenapi-greylist.txt b/config/hiddenapi-greylist.txt
index 82d0fbe..10f25ea 100644
--- a/config/hiddenapi-greylist.txt
+++ b/config/hiddenapi-greylist.txt
@@ -1774,715 +1774,3 @@
 Lcom/google/android/mms/util/SqliteWrapper;->requery(Landroid/content/Context;Landroid/database/Cursor;)Z
 Lcom/google/android/mms/util/SqliteWrapper;->update(Landroid/content/Context;Landroid/content/ContentResolver;Landroid/net/Uri;Landroid/content/ContentValues;Ljava/lang/String;[Ljava/lang/String;)I
 Lcom/google/android/util/AbstractMessageParser$Token$Type;->values()[Lcom/google/android/util/AbstractMessageParser$Token$Type;
-Lgov/nist/core/Debug;->printStackTrace(Ljava/lang/Exception;)V
-Lgov/nist/core/GenericObject;-><init>()V
-Lgov/nist/core/GenericObject;->dbgPrint()V
-Lgov/nist/core/GenericObject;->debugDump(I)Ljava/lang/String;
-Lgov/nist/core/GenericObject;->encode()Ljava/lang/String;
-Lgov/nist/core/GenericObject;->getMatcher()Lgov/nist/core/Match;
-Lgov/nist/core/GenericObject;->indentation:I
-Lgov/nist/core/GenericObject;->isMySubclass(Ljava/lang/Class;)Z
-Lgov/nist/core/GenericObject;->match(Ljava/lang/Object;)Z
-Lgov/nist/core/GenericObject;->matchExpression:Lgov/nist/core/Match;
-Lgov/nist/core/GenericObject;->merge(Ljava/lang/Object;)V
-Lgov/nist/core/GenericObject;->sprint(Ljava/lang/String;)V
-Lgov/nist/core/GenericObject;->stringRepresentation:Ljava/lang/String;
-Lgov/nist/core/GenericObjectList;-><init>()V
-Lgov/nist/core/GenericObjectList;-><init>(Ljava/lang/String;)V
-Lgov/nist/core/GenericObjectList;-><init>(Ljava/lang/String;Ljava/lang/Class;)V
-Lgov/nist/core/GenericObjectList;-><init>(Ljava/lang/String;Ljava/lang/String;)V
-Lgov/nist/core/GenericObjectList;->concatenate(Lgov/nist/core/GenericObjectList;)V
-Lgov/nist/core/GenericObjectList;->concatenate(Lgov/nist/core/GenericObjectList;Z)V
-Lgov/nist/core/GenericObjectList;->debugDump(I)Ljava/lang/String;
-Lgov/nist/core/GenericObjectList;->first()Lgov/nist/core/GenericObject;
-Lgov/nist/core/GenericObjectList;->getIndentation()Ljava/lang/String;
-Lgov/nist/core/GenericObjectList;->indentation:I
-Lgov/nist/core/GenericObjectList;->isMySubclass(Ljava/lang/Class;)Z
-Lgov/nist/core/GenericObjectList;->match(Ljava/lang/Object;)Z
-Lgov/nist/core/GenericObjectList;->myClass:Ljava/lang/Class;
-Lgov/nist/core/GenericObjectList;->next()Lgov/nist/core/GenericObject;
-Lgov/nist/core/GenericObjectList;->next(Ljava/util/ListIterator;)Lgov/nist/core/GenericObject;
-Lgov/nist/core/GenericObjectList;->setMyClass(Ljava/lang/Class;)V
-Lgov/nist/core/GenericObjectList;->stringRep:Ljava/lang/String;
-Lgov/nist/core/Host;-><init>()V
-Lgov/nist/core/Host;-><init>(Ljava/lang/String;)V
-Lgov/nist/core/Host;->encode()Ljava/lang/String;
-Lgov/nist/core/Host;->getAddress()Ljava/lang/String;
-Lgov/nist/core/Host;->getHostname()Ljava/lang/String;
-Lgov/nist/core/Host;->isIPv6Reference(Ljava/lang/String;)Z
-Lgov/nist/core/Host;->setAddress(Ljava/lang/String;)V
-Lgov/nist/core/Host;->setHostname(Ljava/lang/String;)V
-Lgov/nist/core/HostNameParser;-><init>(Lgov/nist/core/LexerCore;)V
-Lgov/nist/core/HostNameParser;-><init>(Ljava/lang/String;)V
-Lgov/nist/core/HostNameParser;->host()Lgov/nist/core/Host;
-Lgov/nist/core/HostNameParser;->hostPort(Z)Lgov/nist/core/HostPort;
-Lgov/nist/core/HostPort;-><init>()V
-Lgov/nist/core/HostPort;->encode()Ljava/lang/String;
-Lgov/nist/core/HostPort;->encode(Ljava/lang/StringBuffer;)Ljava/lang/StringBuffer;
-Lgov/nist/core/HostPort;->getHost()Lgov/nist/core/Host;
-Lgov/nist/core/HostPort;->getInetAddress()Ljava/net/InetAddress;
-Lgov/nist/core/HostPort;->getPort()I
-Lgov/nist/core/HostPort;->hasPort()Z
-Lgov/nist/core/HostPort;->removePort()V
-Lgov/nist/core/HostPort;->setHost(Lgov/nist/core/Host;)V
-Lgov/nist/core/HostPort;->setPort(I)V
-Lgov/nist/core/InternalErrorHandler;->handleException(Ljava/lang/Exception;)V
-Lgov/nist/core/InternalErrorHandler;->handleException(Ljava/lang/String;)V
-Lgov/nist/core/LexerCore;-><init>(Ljava/lang/String;Ljava/lang/String;)V
-Lgov/nist/core/LexerCore;->byteStringNoSemicolon()Ljava/lang/String;
-Lgov/nist/core/LexerCore;->byteStringNoSlash()Ljava/lang/String;
-Lgov/nist/core/LexerCore;->charAsString(I)Ljava/lang/String;
-Lgov/nist/core/LexerCore;->comment()Ljava/lang/String;
-Lgov/nist/core/LexerCore;->createParseException()Ljava/text/ParseException;
-Lgov/nist/core/LexerCore;->currentLexer:Ljava/util/Hashtable;
-Lgov/nist/core/LexerCore;->getBuffer()Ljava/lang/String;
-Lgov/nist/core/LexerCore;->getNextId()Ljava/lang/String;
-Lgov/nist/core/LexerCore;->getNextToken()Lgov/nist/core/Token;
-Lgov/nist/core/LexerCore;->getPtr()I
-Lgov/nist/core/LexerCore;->getRest()Ljava/lang/String;
-Lgov/nist/core/LexerCore;->getString(C)Ljava/lang/String;
-Lgov/nist/core/LexerCore;->isTokenChar(C)Z
-Lgov/nist/core/LexerCore;->lexerTables:Ljava/util/Hashtable;
-Lgov/nist/core/LexerCore;->markInputPosition()I
-Lgov/nist/core/LexerCore;->match(I)Lgov/nist/core/Token;
-Lgov/nist/core/LexerCore;->number()Ljava/lang/String;
-Lgov/nist/core/LexerCore;->peekNextToken()Lgov/nist/core/Token;
-Lgov/nist/core/LexerCore;->peekNextToken(I)[Lgov/nist/core/Token;
-Lgov/nist/core/LexerCore;->quotedString()Ljava/lang/String;
-Lgov/nist/core/LexerCore;->rewindInputPosition(I)V
-Lgov/nist/core/LexerCore;->selectLexer(Ljava/lang/String;)V
-Lgov/nist/core/LexerCore;->SPorHT()V
-Lgov/nist/core/LexerCore;->startsId()Z
-Lgov/nist/core/LexerCore;->ttoken()Ljava/lang/String;
-Lgov/nist/core/LexerCore;->ttokenSafe()Ljava/lang/String;
-Lgov/nist/core/Match;->match(Ljava/lang/String;)Z
-Lgov/nist/core/NameValue;-><init>()V
-Lgov/nist/core/NameValue;-><init>(Ljava/lang/String;Ljava/lang/Object;)V
-Lgov/nist/core/NameValue;-><init>(Ljava/lang/String;Ljava/lang/Object;Z)V
-Lgov/nist/core/NameValue;->encode()Ljava/lang/String;
-Lgov/nist/core/NameValue;->encode(Ljava/lang/StringBuffer;)Ljava/lang/StringBuffer;
-Lgov/nist/core/NameValue;->getName()Ljava/lang/String;
-Lgov/nist/core/NameValue;->getValueAsObject()Ljava/lang/Object;
-Lgov/nist/core/NameValue;->setName(Ljava/lang/String;)V
-Lgov/nist/core/NameValue;->setQuotedValue()V
-Lgov/nist/core/NameValue;->setSeparator(Ljava/lang/String;)V
-Lgov/nist/core/NameValue;->setValueAsObject(Ljava/lang/Object;)V
-Lgov/nist/core/NameValueList;-><init>()V
-Lgov/nist/core/NameValueList;-><init>(Z)V
-Lgov/nist/core/NameValueList;->delete(Ljava/lang/String;)Z
-Lgov/nist/core/NameValueList;->encode()Ljava/lang/String;
-Lgov/nist/core/NameValueList;->encode(Ljava/lang/StringBuffer;)Ljava/lang/StringBuffer;
-Lgov/nist/core/NameValueList;->getNames()Ljava/util/Iterator;
-Lgov/nist/core/NameValueList;->getNameValue(Ljava/lang/String;)Lgov/nist/core/NameValue;
-Lgov/nist/core/NameValueList;->getParameter(Ljava/lang/String;)Ljava/lang/String;
-Lgov/nist/core/NameValueList;->getValue(Ljava/lang/String;)Ljava/lang/Object;
-Lgov/nist/core/NameValueList;->hasNameValue(Ljava/lang/String;)Z
-Lgov/nist/core/NameValueList;->iterator()Ljava/util/Iterator;
-Lgov/nist/core/NameValueList;->set(Lgov/nist/core/NameValue;)V
-Lgov/nist/core/NameValueList;->set(Ljava/lang/String;Ljava/lang/Object;)V
-Lgov/nist/core/NameValueList;->setSeparator(Ljava/lang/String;)V
-Lgov/nist/core/net/DefaultNetworkLayer;->SINGLETON:Lgov/nist/core/net/DefaultNetworkLayer;
-Lgov/nist/core/net/NetworkLayer;->createDatagramSocket()Ljava/net/DatagramSocket;
-Lgov/nist/core/net/NetworkLayer;->createDatagramSocket(ILjava/net/InetAddress;)Ljava/net/DatagramSocket;
-Lgov/nist/core/net/NetworkLayer;->createServerSocket(IILjava/net/InetAddress;)Ljava/net/ServerSocket;
-Lgov/nist/core/net/NetworkLayer;->createSocket(Ljava/net/InetAddress;I)Ljava/net/Socket;
-Lgov/nist/core/net/NetworkLayer;->createSSLServerSocket(IILjava/net/InetAddress;)Ljavax/net/ssl/SSLServerSocket;
-Lgov/nist/core/net/NetworkLayer;->createSSLSocket(Ljava/net/InetAddress;I)Ljavax/net/ssl/SSLSocket;
-Lgov/nist/core/ParserCore;-><init>()V
-Lgov/nist/core/ParserCore;->lexer:Lgov/nist/core/LexerCore;
-Lgov/nist/core/StringTokenizer;->ptr:I
-Lgov/nist/core/ThreadAuditor$ThreadHandle;->getPingIntervalInMillisecs()J
-Lgov/nist/core/ThreadAuditor$ThreadHandle;->ping()V
-Lgov/nist/core/ThreadAuditor;-><init>()V
-Lgov/nist/core/ThreadAuditor;->addCurrentThread()Lgov/nist/core/ThreadAuditor$ThreadHandle;
-Lgov/nist/core/ThreadAuditor;->getPingIntervalInMillisecs()J
-Lgov/nist/core/ThreadAuditor;->isEnabled()Z
-Lgov/nist/core/ThreadAuditor;->setPingIntervalInMillisecs(J)V
-Lgov/nist/core/Token;-><init>()V
-Lgov/nist/core/Token;->getTokenType()I
-Lgov/nist/core/Token;->getTokenValue()Ljava/lang/String;
-Lgov/nist/javax/sip/address/GenericURI;-><init>()V
-Lgov/nist/javax/sip/address/GenericURI;->encode()Ljava/lang/String;
-Lgov/nist/javax/sip/address/GenericURI;->getScheme()Ljava/lang/String;
-Lgov/nist/javax/sip/address/SipUri;->getHost()Ljava/lang/String;
-Lgov/nist/javax/sip/address/SipUri;->getParameter(Ljava/lang/String;)Ljava/lang/String;
-Lgov/nist/javax/sip/address/SipUri;->getPort()I
-Lgov/nist/javax/sip/address/SipUri;->getUser()Ljava/lang/String;
-Lgov/nist/javax/sip/address/SipUri;->removeParameter(Ljava/lang/String;)V
-Lgov/nist/javax/sip/address/SipUri;->setParameter(Ljava/lang/String;Ljava/lang/String;)V
-Lgov/nist/javax/sip/address/SipUri;->setUserParam(Ljava/lang/String;)V
-Lgov/nist/javax/sip/parser/URLParser;-><init>(Ljava/lang/String;)V
-Lgov/nist/javax/sip/parser/URLParser;->sipURL(Z)Lgov/nist/javax/sip/address/SipUri;
-Lorg/apache/xalan/extensions/ExpressionContext;->getContextNode()Lorg/w3c/dom/Node;
-Lorg/apache/xalan/extensions/ExpressionContext;->getErrorListener()Ljavax/xml/transform/ErrorListener;
-Lorg/apache/xalan/extensions/ExpressionContext;->getVariableOrParam(Lorg/apache/xml/utils/QName;)Lorg/apache/xpath/objects/XObject;
-Lorg/apache/xalan/extensions/ExpressionContext;->getXPathContext()Lorg/apache/xpath/XPathContext;
-Lorg/apache/xalan/extensions/ExtensionHandler;-><init>(Ljava/lang/String;Ljava/lang/String;)V
-Lorg/apache/xalan/extensions/ExtensionHandler;->callFunction(Ljava/lang/String;Ljava/util/Vector;Ljava/lang/Object;Lorg/apache/xalan/extensions/ExpressionContext;)Ljava/lang/Object;
-Lorg/apache/xalan/extensions/ExtensionHandler;->getClassForName(Ljava/lang/String;)Ljava/lang/Class;
-Lorg/apache/xalan/extensions/ObjectFactory$ConfigurationError;-><init>(Ljava/lang/String;Ljava/lang/Exception;)V
-Lorg/apache/xalan/extensions/ObjectFactory;->findClassLoader()Ljava/lang/ClassLoader;
-Lorg/apache/xalan/extensions/ObjectFactory;->findProviderClass(Ljava/lang/String;Ljava/lang/ClassLoader;Z)Ljava/lang/Class;
-Lorg/apache/xalan/processor/TransformerFactoryImpl;-><init>()V
-Lorg/apache/xalan/res/XSLMessages;->createMessage(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
-Lorg/apache/xalan/res/XSLTErrorResources;-><init>()V
-Lorg/apache/xalan/serialize/SerializerUtils;->outputResultTreeFragment(Lorg/apache/xml/serializer/SerializationHandler;Lorg/apache/xpath/objects/XObject;Lorg/apache/xpath/XPathContext;)V
-Lorg/apache/xalan/templates/AVT;->evaluate(Lorg/apache/xpath/XPathContext;ILorg/apache/xml/utils/PrefixResolver;)Ljava/lang/String;
-Lorg/apache/xalan/templates/ElemElement;->execute(Lorg/apache/xalan/transformer/TransformerImpl;)V
-Lorg/apache/xalan/templates/ElemExsltFunction;->execute(Lorg/apache/xalan/transformer/TransformerImpl;[Lorg/apache/xpath/objects/XObject;)V
-Lorg/apache/xalan/templates/ElemExtensionCall;->getAttribute(Ljava/lang/String;Lorg/w3c/dom/Node;Lorg/apache/xalan/transformer/TransformerImpl;)Ljava/lang/String;
-Lorg/apache/xalan/templates/ElemLiteralResult;->getLiteralResultAttribute(Ljava/lang/String;)Lorg/apache/xalan/templates/AVT;
-Lorg/apache/xalan/templates/ElemTemplate;->getMatch()Lorg/apache/xpath/XPath;
-Lorg/apache/xalan/templates/ElemTemplate;->getName()Lorg/apache/xml/utils/QName;
-Lorg/apache/xalan/templates/ElemTemplateElement;->getFirstChildElem()Lorg/apache/xalan/templates/ElemTemplateElement;
-Lorg/apache/xalan/templates/ElemTemplateElement;->getNextSiblingElem()Lorg/apache/xalan/templates/ElemTemplateElement;
-Lorg/apache/xalan/templates/ElemTemplateElement;->getParentElem()Lorg/apache/xalan/templates/ElemTemplateElement;
-Lorg/apache/xalan/templates/ElemTemplateElement;->getStylesheetRoot()Lorg/apache/xalan/templates/StylesheetRoot;
-Lorg/apache/xalan/templates/ElemTemplateElement;->getXSLToken()I
-Lorg/apache/xalan/templates/ElemTextLiteral;->getChars()[C
-Lorg/apache/xalan/templates/KeyDeclaration;->getName()Lorg/apache/xml/utils/QName;
-Lorg/apache/xalan/templates/KeyDeclaration;->getUse()Lorg/apache/xpath/XPath;
-Lorg/apache/xalan/templates/StylesheetRoot;->getDefaultRootRule()Lorg/apache/xalan/templates/ElemTemplate;
-Lorg/apache/xalan/templates/StylesheetRoot;->getDefaultRule()Lorg/apache/xalan/templates/ElemTemplate;
-Lorg/apache/xalan/templates/StylesheetRoot;->getDefaultTextRule()Lorg/apache/xalan/templates/ElemTemplate;
-Lorg/apache/xalan/templates/StylesheetRoot;->getTemplateComposed(Lorg/apache/xml/utils/QName;)Lorg/apache/xalan/templates/ElemTemplate;
-Lorg/apache/xalan/transformer/ClonerToResultTree;->cloneToResultTree(IILorg/apache/xml/dtm/DTM;Lorg/apache/xml/serializer/SerializationHandler;Z)V
-Lorg/apache/xalan/transformer/DecimalToRoman;-><init>(JLjava/lang/String;JLjava/lang/String;)V
-Lorg/apache/xalan/transformer/DecimalToRoman;->m_postLetter:Ljava/lang/String;
-Lorg/apache/xalan/transformer/DecimalToRoman;->m_postValue:J
-Lorg/apache/xalan/transformer/DecimalToRoman;->m_preLetter:Ljava/lang/String;
-Lorg/apache/xalan/transformer/DecimalToRoman;->m_preValue:J
-Lorg/apache/xalan/transformer/MsgMgr;->error(Ljavax/xml/transform/SourceLocator;Lorg/w3c/dom/Node;Lorg/w3c/dom/Node;Ljava/lang/String;)V
-Lorg/apache/xalan/transformer/TransformerImpl;->createSerializationHandler(Ljavax/xml/transform/Result;Lorg/apache/xalan/templates/OutputProperties;)Lorg/apache/xml/serializer/SerializationHandler;
-Lorg/apache/xalan/transformer/TransformerImpl;->executeChildTemplates(Lorg/apache/xalan/templates/ElemTemplateElement;Lorg/w3c/dom/Node;Lorg/apache/xml/utils/QName;Lorg/xml/sax/ContentHandler;)V
-Lorg/apache/xalan/transformer/TransformerImpl;->executeChildTemplates(Lorg/apache/xalan/templates/ElemTemplateElement;Z)V
-Lorg/apache/xalan/transformer/TransformerImpl;->getCountersTable()Lorg/apache/xalan/transformer/CountersTable;
-Lorg/apache/xalan/transformer/TransformerImpl;->getCurrentTemplateElements()Lorg/apache/xml/utils/ObjectStack;
-Lorg/apache/xalan/transformer/TransformerImpl;->getCurrentTemplateElementsCount()I
-Lorg/apache/xalan/transformer/TransformerImpl;->getMatchedNode()I
-Lorg/apache/xalan/transformer/TransformerImpl;->getMatchedTemplate()Lorg/apache/xalan/templates/ElemTemplate;
-Lorg/apache/xalan/transformer/TransformerImpl;->getMode()Lorg/apache/xml/utils/QName;
-Lorg/apache/xalan/transformer/TransformerImpl;->getMsgMgr()Lorg/apache/xalan/transformer/MsgMgr;
-Lorg/apache/xalan/transformer/TransformerImpl;->getOutputFormat()Lorg/apache/xalan/templates/OutputProperties;
-Lorg/apache/xalan/transformer/TransformerImpl;->getResultTreeHandler()Lorg/apache/xml/serializer/SerializationHandler;
-Lorg/apache/xalan/transformer/TransformerImpl;->getSerializationHandler()Lorg/apache/xml/serializer/SerializationHandler;
-Lorg/apache/xalan/transformer/TransformerImpl;->getXPathContext()Lorg/apache/xpath/XPathContext;
-Lorg/apache/xalan/transformer/TransformerImpl;->m_attrSetStack:Ljava/util/Stack;
-Lorg/apache/xalan/transformer/TransformerImpl;->m_currentMatchedNodes:Lorg/apache/xml/utils/NodeVector;
-Lorg/apache/xalan/transformer/TransformerImpl;->m_currentMatchTemplates:Ljava/util/Stack;
-Lorg/apache/xalan/transformer/TransformerImpl;->m_currentTemplateElements:Lorg/apache/xml/utils/ObjectStack;
-Lorg/apache/xalan/transformer/TransformerImpl;->m_currentTemplateRuleIsNull:Lorg/apache/xml/utils/BoolStack;
-Lorg/apache/xalan/transformer/TransformerImpl;->m_inputContentHandler:Lorg/xml/sax/ContentHandler;
-Lorg/apache/xalan/transformer/TransformerImpl;->m_outputTarget:Ljavax/xml/transform/Result;
-Lorg/apache/xalan/transformer/TransformerImpl;->m_stringWriterObjectPool:Lorg/apache/xml/utils/ObjectPool;
-Lorg/apache/xalan/transformer/TransformerImpl;->m_urlOfSource:Ljava/lang/String;
-Lorg/apache/xalan/transformer/TransformerImpl;->m_xcontext:Lorg/apache/xpath/XPathContext;
-Lorg/apache/xalan/transformer/TransformerImpl;->popCurrentFuncResult()Ljava/lang/Object;
-Lorg/apache/xalan/transformer/TransformerImpl;->pushCurrentFuncResult(Ljava/lang/Object;)V
-Lorg/apache/xalan/transformer/TransformerImpl;->pushElemTemplateElement(Lorg/apache/xalan/templates/ElemTemplateElement;)V
-Lorg/apache/xalan/Version;->getVersion()Ljava/lang/String;
-Lorg/apache/xalan/xslt/EnvironmentCheck;-><init>()V
-Lorg/apache/xalan/xslt/EnvironmentCheck;->appendEnvironmentReport(Lorg/w3c/dom/Node;Lorg/w3c/dom/Document;Ljava/util/Hashtable;)V
-Lorg/apache/xalan/xslt/EnvironmentCheck;->getEnvironmentHash()Ljava/util/Hashtable;
-Lorg/apache/xalan/xslt/ObjectFactory;->findClassLoader()Ljava/lang/ClassLoader;
-Lorg/apache/xalan/xslt/ObjectFactory;->newInstance(Ljava/lang/String;Ljava/lang/ClassLoader;Z)Ljava/lang/Object;
-Lorg/apache/xml/dtm/Axis;->getNames(I)Ljava/lang/String;
-Lorg/apache/xml/dtm/Axis;->isReverse(I)Z
-Lorg/apache/xml/dtm/DTM;->getDocument()I
-Lorg/apache/xml/dtm/DTM;->getDocumentRoot(I)I
-Lorg/apache/xml/dtm/DTM;->getFirstChild(I)I
-Lorg/apache/xml/dtm/DTM;->getNextSibling(I)I
-Lorg/apache/xml/dtm/DTM;->getNode(I)Lorg/w3c/dom/Node;
-Lorg/apache/xml/dtm/DTM;->getNodeName(I)Ljava/lang/String;
-Lorg/apache/xml/dtm/DTM;->getNodeType(I)S
-Lorg/apache/xml/dtm/DTM;->getParent(I)I
-Lorg/apache/xml/dtm/DTM;->getSourceLocatorFor(I)Ljavax/xml/transform/SourceLocator;
-Lorg/apache/xml/dtm/DTM;->getStringValue(I)Lorg/apache/xml/utils/XMLString;
-Lorg/apache/xml/dtm/DTM;->migrateTo(Lorg/apache/xml/dtm/DTMManager;)V
-Lorg/apache/xml/dtm/DTMAxisIterator;->cloneIterator()Lorg/apache/xml/dtm/DTMAxisIterator;
-Lorg/apache/xml/dtm/DTMAxisIterator;->getLast()I
-Lorg/apache/xml/dtm/DTMAxisIterator;->getNodeByPosition(I)I
-Lorg/apache/xml/dtm/DTMAxisIterator;->getPosition()I
-Lorg/apache/xml/dtm/DTMAxisIterator;->gotoMark()V
-Lorg/apache/xml/dtm/DTMAxisIterator;->isReverse()Z
-Lorg/apache/xml/dtm/DTMAxisIterator;->next()I
-Lorg/apache/xml/dtm/DTMAxisIterator;->reset()Lorg/apache/xml/dtm/DTMAxisIterator;
-Lorg/apache/xml/dtm/DTMAxisIterator;->setMark()V
-Lorg/apache/xml/dtm/DTMAxisIterator;->setRestartable(Z)V
-Lorg/apache/xml/dtm/DTMAxisIterator;->setStartNode(I)Lorg/apache/xml/dtm/DTMAxisIterator;
-Lorg/apache/xml/dtm/DTMException;-><init>(Ljava/lang/String;)V
-Lorg/apache/xml/dtm/DTMFilter;->acceptNode(II)S
-Lorg/apache/xml/dtm/DTMIterator;->cloneWithReset()Lorg/apache/xml/dtm/DTMIterator;
-Lorg/apache/xml/dtm/DTMIterator;->getCurrentPos()I
-Lorg/apache/xml/dtm/DTMIterator;->getDTM(I)Lorg/apache/xml/dtm/DTM;
-Lorg/apache/xml/dtm/DTMIterator;->nextNode()I
-Lorg/apache/xml/dtm/DTMIterator;->runTo(I)V
-Lorg/apache/xml/dtm/DTMIterator;->setCurrentPos(I)V
-Lorg/apache/xml/dtm/DTMIterator;->setRoot(ILjava/lang/Object;)V
-Lorg/apache/xml/dtm/DTMIterator;->setShouldCacheNodes(Z)V
-Lorg/apache/xml/dtm/DTMManager;->getDTM(Ljavax/xml/transform/Source;ZLorg/apache/xml/dtm/DTMWSFilter;ZZ)Lorg/apache/xml/dtm/DTM;
-Lorg/apache/xml/dtm/DTMManager;->getXMLStringFactory()Lorg/apache/xml/utils/XMLStringFactory;
-Lorg/apache/xml/dtm/DTMManager;->release(Lorg/apache/xml/dtm/DTM;Z)Z
-Lorg/apache/xml/dtm/ref/CoroutineManager;-><init>()V
-Lorg/apache/xml/dtm/ref/CoroutineManager;->co_joinCoroutineSet(I)I
-Lorg/apache/xml/dtm/ref/DTMAxisIteratorBase;-><init>()V
-Lorg/apache/xml/dtm/ref/DTMAxisIteratorBase;->includeSelf()Lorg/apache/xml/dtm/DTMAxisIterator;
-Lorg/apache/xml/dtm/ref/DTMAxisIteratorBase;->reset()Lorg/apache/xml/dtm/DTMAxisIterator;
-Lorg/apache/xml/dtm/ref/DTMAxisIteratorBase;->resetPosition()Lorg/apache/xml/dtm/DTMAxisIterator;
-Lorg/apache/xml/dtm/ref/DTMAxisIteratorBase;->returnNode(I)I
-Lorg/apache/xml/dtm/ref/DTMAxisIteratorBase;->setRestartable(Z)V
-Lorg/apache/xml/dtm/ref/DTMAxisIteratorBase;->_includeSelf:Z
-Lorg/apache/xml/dtm/ref/DTMAxisIteratorBase;->_isRestartable:Z
-Lorg/apache/xml/dtm/ref/DTMAxisIteratorBase;->_last:I
-Lorg/apache/xml/dtm/ref/DTMAxisIteratorBase;->_markedNode:I
-Lorg/apache/xml/dtm/ref/DTMAxisIteratorBase;->_position:I
-Lorg/apache/xml/dtm/ref/DTMAxisIteratorBase;->_startNode:I
-Lorg/apache/xml/dtm/ref/DTMAxisIterNodeList;-><init>(Lorg/apache/xml/dtm/DTM;Lorg/apache/xml/dtm/DTMAxisIterator;)V
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->appendChild(IZZ)V
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->appendTextChild(Ljava/lang/String;)V
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->declareNamespaceInContext(II)V
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->documentRegistration()V
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->documentRelease()V
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->ensureSizeOfIndex(II)V
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->error(Ljava/lang/String;)V
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->findGTE([IIII)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->findInSortedSuballocatedIntVector(Lorg/apache/xml/utils/SuballocatedIntVector;I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->findNamespaceContext(I)Lorg/apache/xml/utils/SuballocatedIntVector;
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getDocument()I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getDocumentAllDeclarationsProcessed()Z
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getDocumentBaseURI()Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getDocumentEncoding(I)Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getDocumentRoot(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getDocumentStandalone(I)Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getDocumentSystemIdentifier(I)Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getDocumentVersion(I)Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getDTMIDs()Lorg/apache/xml/utils/SuballocatedIntVector;
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getExpandedTypeID(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getExpandedTypeID(Ljava/lang/String;Ljava/lang/String;I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getFirstChild(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getFirstNamespaceNode(IZ)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getLastChild(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getLevel(I)S
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getLocalNameFromExpandedNameID(I)Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getManager()Lorg/apache/xml/dtm/DTMManager;
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getNamespaceFromExpandedNameID(I)Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getNamespaceType(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getNextAttribute(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getNextNamespaceNode(IIZ)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getNextSibling(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getNode(I)Lorg/w3c/dom/Node;
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getNodeHandle(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getNodeIdent(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getNodeType(I)S
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getOwnerDocument(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getParent(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getPreviousSibling(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getShouldStripWhitespace()Z
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getStringValueChunk(II[I)[C
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->getStringValueChunkCount(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->hasChildNodes(I)Z
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->indexNode(II)V
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->isCharacterElementContentWhitespace(I)Z
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->isDocumentAllDeclarationsProcessed(I)Z
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->isNodeAfter(II)Z
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->isSupported(Ljava/lang/String;Ljava/lang/String;)Z
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->makeNodeHandle(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->makeNodeIdentity(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->m_expandedNameTable:Lorg/apache/xml/dtm/ref/ExpandedNameTable;
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->m_exptype:Lorg/apache/xml/utils/SuballocatedIntVector;
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->m_firstch:Lorg/apache/xml/utils/SuballocatedIntVector;
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->m_nextsib:Lorg/apache/xml/utils/SuballocatedIntVector;
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->m_parent:Lorg/apache/xml/utils/SuballocatedIntVector;
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->m_prevsib:Lorg/apache/xml/utils/SuballocatedIntVector;
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->m_size:I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->m_wsfilter:Lorg/apache/xml/dtm/DTMWSFilter;
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->m_xstrf:Lorg/apache/xml/utils/XMLStringFactory;
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->popShouldStripWhitespace()V
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->pushShouldStripWhitespace(Z)V
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->setDocumentBaseURI(Ljava/lang/String;)V
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->setFeature(Ljava/lang/String;Z)V
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->setShouldStripWhitespace(Z)V
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->supportsPreStripping()Z
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->_exptype(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->_firstch(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->_level(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->_nextsib(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->_parent(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->_prevsib(I)I
-Lorg/apache/xml/dtm/ref/DTMDefaultBase;->_type(I)S
-Lorg/apache/xml/dtm/ref/DTMDefaultBaseIterators$InternalAxisIteratorBase;-><init>(Lorg/apache/xml/dtm/ref/DTMDefaultBaseIterators;)V
-Lorg/apache/xml/dtm/ref/DTMDefaultBaseIterators$InternalAxisIteratorBase;->_currentNode:I
-Lorg/apache/xml/dtm/ref/DTMDefaultBaseIterators$NamespaceIterator;-><init>(Lorg/apache/xml/dtm/ref/DTMDefaultBaseIterators;)V
-Lorg/apache/xml/dtm/ref/DTMDefaultBaseIterators$NamespaceIterator;->next()I
-Lorg/apache/xml/dtm/ref/DTMDefaultBaseIterators$NamespaceIterator;->setStartNode(I)Lorg/apache/xml/dtm/DTMAxisIterator;
-Lorg/apache/xml/dtm/ref/DTMDefaultBaseIterators$NthDescendantIterator;-><init>(Lorg/apache/xml/dtm/ref/DTMDefaultBaseIterators;I)V
-Lorg/apache/xml/dtm/ref/DTMDefaultBaseIterators$SingletonIterator;-><init>(Lorg/apache/xml/dtm/ref/DTMDefaultBaseIterators;)V
-Lorg/apache/xml/dtm/ref/DTMDefaultBaseIterators$SingletonIterator;-><init>(Lorg/apache/xml/dtm/ref/DTMDefaultBaseIterators;I)V
-Lorg/apache/xml/dtm/ref/DTMDefaultBaseIterators;-><init>(Lorg/apache/xml/dtm/DTMManager;Ljavax/xml/transform/Source;ILorg/apache/xml/dtm/DTMWSFilter;Lorg/apache/xml/utils/XMLStringFactory;Z)V
-Lorg/apache/xml/dtm/ref/DTMDefaultBaseIterators;->getAxisIterator(I)Lorg/apache/xml/dtm/DTMAxisIterator;
-Lorg/apache/xml/dtm/ref/DTMDefaultBaseIterators;->getTypedAxisIterator(II)Lorg/apache/xml/dtm/DTMAxisIterator;
-Lorg/apache/xml/dtm/ref/DTMDefaultBaseTraversers;->getAxisTraverser(I)Lorg/apache/xml/dtm/DTMAxisTraverser;
-Lorg/apache/xml/dtm/ref/DTMManagerDefault;-><init>()V
-Lorg/apache/xml/dtm/ref/DTMManagerDefault;->addDTM(Lorg/apache/xml/dtm/DTM;I)V
-Lorg/apache/xml/dtm/ref/DTMManagerDefault;->addDTM(Lorg/apache/xml/dtm/DTM;II)V
-Lorg/apache/xml/dtm/ref/DTMManagerDefault;->getFirstFreeDTMID()I
-Lorg/apache/xml/dtm/ref/DTMManagerDefault;->getXMLReader(Ljavax/xml/transform/Source;)Lorg/xml/sax/XMLReader;
-Lorg/apache/xml/dtm/ref/DTMManagerDefault;->releaseXMLReader(Lorg/xml/sax/XMLReader;)V
-Lorg/apache/xml/dtm/ref/DTMNodeIterator;-><init>(Lorg/apache/xml/dtm/DTMIterator;)V
-Lorg/apache/xml/dtm/ref/DTMNodeIterator;->getDTMIterator()Lorg/apache/xml/dtm/DTMIterator;
-Lorg/apache/xml/dtm/ref/DTMNodeIterator;->getRoot()Lorg/w3c/dom/Node;
-Lorg/apache/xml/dtm/ref/DTMNodeList;-><init>(Lorg/apache/xml/dtm/DTMIterator;)V
-Lorg/apache/xml/dtm/ref/DTMNodeProxy;-><init>(Lorg/apache/xml/dtm/DTM;I)V
-Lorg/apache/xml/dtm/ref/DTMNodeProxy;->getDTM()Lorg/apache/xml/dtm/DTM;
-Lorg/apache/xml/dtm/ref/DTMNodeProxy;->getDTMNodeNumber()I
-Lorg/apache/xml/dtm/ref/DTMNodeProxy;->getStringValue()Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/DTMStringPool;-><init>()V
-Lorg/apache/xml/dtm/ref/DTMStringPool;->indexToString(I)Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/DTMStringPool;->m_intToString:Ljava/util/Vector;
-Lorg/apache/xml/dtm/ref/DTMStringPool;->removeAllElements()V
-Lorg/apache/xml/dtm/ref/DTMStringPool;->stringToIndex(Ljava/lang/String;)I
-Lorg/apache/xml/dtm/ref/ExpandedNameTable;->getExpandedTypeID(Ljava/lang/String;Ljava/lang/String;I)I
-Lorg/apache/xml/dtm/ref/ExpandedNameTable;->getExpandedTypeID(Ljava/lang/String;Ljava/lang/String;IZ)I
-Lorg/apache/xml/dtm/ref/ExpandedNameTable;->getLocalName(I)Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/ExpandedNameTable;->getSize()I
-Lorg/apache/xml/dtm/ref/ExpandedNameTable;->getType(I)S
-Lorg/apache/xml/dtm/ref/IncrementalSAXSource;->deliverMoreNodes(Z)Ljava/lang/Object;
-Lorg/apache/xml/dtm/ref/IncrementalSAXSource;->setContentHandler(Lorg/xml/sax/ContentHandler;)V
-Lorg/apache/xml/dtm/ref/IncrementalSAXSource;->setLexicalHandler(Lorg/xml/sax/ext/LexicalHandler;)V
-Lorg/apache/xml/dtm/ref/IncrementalSAXSource;->startParse(Lorg/xml/sax/InputSource;)V
-Lorg/apache/xml/dtm/ref/IncrementalSAXSource_Filter;-><init>()V
-Lorg/apache/xml/dtm/ref/IncrementalSAXSource_Filter;->setXMLReader(Lorg/xml/sax/XMLReader;)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$AncestorIterator;-><init>(Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$AncestorIterator;->next()I
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$AncestorIterator;->setStartNode(I)Lorg/apache/xml/dtm/DTMAxisIterator;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$AttributeIterator;-><init>(Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$ChildrenIterator;-><init>(Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$ChildrenIterator;->setStartNode(I)Lorg/apache/xml/dtm/DTMAxisIterator;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$DescendantIterator;-><init>(Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$FollowingIterator;-><init>(Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$FollowingSiblingIterator;-><init>(Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$ParentIterator;-><init>(Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$ParentIterator;->setNodeType(I)Lorg/apache/xml/dtm/DTMAxisIterator;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$PrecedingIterator;-><init>(Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$PrecedingSiblingIterator;-><init>(Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$TypedAncestorIterator;-><init>(Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;I)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$TypedAttributeIterator;-><init>(Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;I)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$TypedChildrenIterator;-><init>(Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;I)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$TypedDescendantIterator;-><init>(Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;I)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$TypedFollowingIterator;-><init>(Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;I)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$TypedFollowingSiblingIterator;-><init>(Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;I)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$TypedPrecedingIterator;-><init>(Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;I)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$TypedPrecedingSiblingIterator;-><init>(Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;I)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2$TypedSingletonIterator;-><init>(Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;I)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;-><init>(Lorg/apache/xml/dtm/DTMManager;Ljavax/xml/transform/Source;ILorg/apache/xml/dtm/DTMWSFilter;Lorg/apache/xml/utils/XMLStringFactory;ZIZZZ)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;->copyAttribute(IILorg/apache/xml/serializer/SerializationHandler;)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;->copyAttributes(ILorg/apache/xml/serializer/SerializationHandler;)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;->copyElement(IILorg/apache/xml/serializer/SerializationHandler;)Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;->copyNS(ILorg/apache/xml/serializer/SerializationHandler;Z)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;->copyTextNode(ILorg/apache/xml/serializer/SerializationHandler;)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;->dispatchCharactersEvents(ILorg/xml/sax/ContentHandler;Z)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;->getFirstAttribute(I)I
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;->getIdForNamespace(Ljava/lang/String;)I
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;->getLocalName(I)Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;->getNodeName(I)Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;->getNodeNameX(I)Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;->getNodeValue(I)Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;->getStringValue()Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;->getStringValue(I)Lorg/apache/xml/utils/XMLString;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;->getStringValueX(I)Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;->m_buildIdIndex:Z
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;->_exptype2(I)I
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;->_exptype2Type(I)I
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;->_firstch2(I)I
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM2;->_nextsib2(I)I
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->dispatchToEvents(ILorg/xml/sax/ContentHandler;)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->getAttributeNode(ILjava/lang/String;Ljava/lang/String;)I
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->getContentHandler()Lorg/xml/sax/ContentHandler;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->getDeclHandler()Lorg/xml/sax/ext/DeclHandler;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->getDocumentTypeDeclarationPublicIdentifier()Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->getDocumentTypeDeclarationSystemIdentifier()Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->getDTDHandler()Lorg/xml/sax/DTDHandler;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->getEntityResolver()Lorg/xml/sax/EntityResolver;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->getErrorHandler()Lorg/xml/sax/ErrorHandler;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->getLexicalHandler()Lorg/xml/sax/ext/LexicalHandler;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->getNamespaceURI(I)Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->getNumberOfNodes()I
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->getPrefix(I)Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->getSourceLocatorFor(I)Ljavax/xml/transform/SourceLocator;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->getUnparsedEntityURI(Ljava/lang/String;)Ljava/lang/String;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->isAttributeSpecified(I)Z
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->migrateTo(Lorg/apache/xml/dtm/DTMManager;)V
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->m_idAttributes:Ljava/util/Hashtable;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->m_parents:Lorg/apache/xml/utils/IntStack;
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->m_previous:I
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->needsTwoThreads()Z
-Lorg/apache/xml/dtm/ref/sax2dtm/SAX2DTM;->setProperty(Ljava/lang/String;Ljava/lang/Object;)V
-Lorg/apache/xml/dtm/ref/SecuritySupport;->getContextClassLoader()Ljava/lang/ClassLoader;
-Lorg/apache/xml/dtm/ref/SecuritySupport;->getFileExists(Ljava/io/File;)Z
-Lorg/apache/xml/dtm/ref/SecuritySupport;->getFileInputStream(Ljava/io/File;)Ljava/io/FileInputStream;
-Lorg/apache/xml/dtm/ref/SecuritySupport;->getInstance()Lorg/apache/xml/dtm/ref/SecuritySupport;
-Lorg/apache/xml/dtm/ref/SecuritySupport;->getLastModified(Ljava/io/File;)J
-Lorg/apache/xml/dtm/ref/SecuritySupport;->getParentClassLoader(Ljava/lang/ClassLoader;)Ljava/lang/ClassLoader;
-Lorg/apache/xml/dtm/ref/SecuritySupport;->getResourceAsStream(Ljava/lang/ClassLoader;Ljava/lang/String;)Ljava/io/InputStream;
-Lorg/apache/xml/dtm/ref/SecuritySupport;->getSystemClassLoader()Ljava/lang/ClassLoader;
-Lorg/apache/xml/dtm/ref/SecuritySupport;->getSystemProperty(Ljava/lang/String;)Ljava/lang/String;
-Lorg/apache/xml/res/XMLErrorResources;-><init>()V
-Lorg/apache/xml/res/XMLMessages;->createXMLMessage(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
-Lorg/apache/xml/serializer/CharInfo$CharKey;-><init>(C)V
-Lorg/apache/xml/serializer/CharInfo;-><init>(Ljava/lang/String;Ljava/lang/String;Z)V
-Lorg/apache/xml/serializer/CharInfo;->get(I)Z
-Lorg/apache/xml/serializer/CharInfo;->getCharInfo(Ljava/lang/String;Ljava/lang/String;)Lorg/apache/xml/serializer/CharInfo;
-Lorg/apache/xml/serializer/CharInfo;->set(I)V
-Lorg/apache/xml/serializer/dom3/LSSerializerImpl;-><init>()V
-Lorg/apache/xml/serializer/DOMSerializer;->serialize(Lorg/w3c/dom/Node;)V
-Lorg/apache/xml/serializer/ElemContext;->m_elementName:Ljava/lang/String;
-Lorg/apache/xml/serializer/ElemContext;->m_elementURI:Ljava/lang/String;
-Lorg/apache/xml/serializer/ElemContext;->m_startTagOpen:Z
-Lorg/apache/xml/serializer/ElemContext;->push(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lorg/apache/xml/serializer/ElemContext;
-Lorg/apache/xml/serializer/ElemDesc;->isAttrFlagSet(Ljava/lang/String;I)Z
-Lorg/apache/xml/serializer/Encodings;->convertMime2JavaEncoding(Ljava/lang/String;)Ljava/lang/String;
-Lorg/apache/xml/serializer/Encodings;->getMimeEncoding(Ljava/lang/String;)Ljava/lang/String;
-Lorg/apache/xml/serializer/Encodings;->getWriter(Ljava/io/OutputStream;Ljava/lang/String;)Ljava/io/Writer;
-Lorg/apache/xml/serializer/NamespaceMappings;-><init>()V
-Lorg/apache/xml/serializer/NamespaceMappings;->generateNextPrefix()Ljava/lang/String;
-Lorg/apache/xml/serializer/NamespaceMappings;->lookupNamespace(Ljava/lang/String;)Ljava/lang/String;
-Lorg/apache/xml/serializer/NamespaceMappings;->lookupPrefix(Ljava/lang/String;)Ljava/lang/String;
-Lorg/apache/xml/serializer/OutputPropertiesFactory;->getDefaultMethodProperties(Ljava/lang/String;)Ljava/util/Properties;
-Lorg/apache/xml/serializer/OutputPropertyUtils;->getBooleanProperty(Ljava/lang/String;Ljava/util/Properties;)Z
-Lorg/apache/xml/serializer/OutputPropertyUtils;->getIntProperty(Ljava/lang/String;Ljava/util/Properties;)I
-Lorg/apache/xml/serializer/SerializationHandler;->close()V
-Lorg/apache/xml/serializer/SerializationHandler;->flushPending()V
-Lorg/apache/xml/serializer/SerializationHandler;->setEscaping(Z)Z
-Lorg/apache/xml/serializer/SerializationHandler;->setIndentAmount(I)V
-Lorg/apache/xml/serializer/SerializationHandler;->setNamespaceMappings(Lorg/apache/xml/serializer/NamespaceMappings;)V
-Lorg/apache/xml/serializer/Serializer;->asContentHandler()Lorg/xml/sax/ContentHandler;
-Lorg/apache/xml/serializer/Serializer;->asDOMSerializer()Lorg/apache/xml/serializer/DOMSerializer;
-Lorg/apache/xml/serializer/Serializer;->getOutputFormat()Ljava/util/Properties;
-Lorg/apache/xml/serializer/Serializer;->getOutputStream()Ljava/io/OutputStream;
-Lorg/apache/xml/serializer/Serializer;->getWriter()Ljava/io/Writer;
-Lorg/apache/xml/serializer/Serializer;->reset()Z
-Lorg/apache/xml/serializer/Serializer;->setOutputFormat(Ljava/util/Properties;)V
-Lorg/apache/xml/serializer/Serializer;->setOutputStream(Ljava/io/OutputStream;)V
-Lorg/apache/xml/serializer/Serializer;->setWriter(Ljava/io/Writer;)V
-Lorg/apache/xml/serializer/SerializerBase;->fireCharEvent([CII)V
-Lorg/apache/xml/serializer/SerializerBase;->fireCommentEvent([CII)V
-Lorg/apache/xml/serializer/SerializerBase;->fireEndDoc()V
-Lorg/apache/xml/serializer/SerializerBase;->fireEndElem(Ljava/lang/String;)V
-Lorg/apache/xml/serializer/SerializerBase;->fireEscapingEvent(Ljava/lang/String;Ljava/lang/String;)V
-Lorg/apache/xml/serializer/SerializerBase;->getDoctypePublic()Ljava/lang/String;
-Lorg/apache/xml/serializer/SerializerBase;->getDoctypeSystem()Ljava/lang/String;
-Lorg/apache/xml/serializer/SerializerBase;->getEncoding()Ljava/lang/String;
-Lorg/apache/xml/serializer/SerializerBase;->getPrefixPart(Ljava/lang/String;)Ljava/lang/String;
-Lorg/apache/xml/serializer/SerializerBase;->getVersion()Ljava/lang/String;
-Lorg/apache/xml/serializer/SerializerBase;->m_attributes:Lorg/apache/xml/serializer/AttributesImplSerializer;
-Lorg/apache/xml/serializer/SerializerBase;->m_charsBuff:[C
-Lorg/apache/xml/serializer/SerializerBase;->m_elemContext:Lorg/apache/xml/serializer/ElemContext;
-Lorg/apache/xml/serializer/SerializerBase;->m_needToCallStartDocument:Z
-Lorg/apache/xml/serializer/SerializerBase;->m_tracer:Lorg/apache/xml/serializer/SerializerTrace;
-Lorg/apache/xml/serializer/SerializerBase;->setDoctypePublic(Ljava/lang/String;)V
-Lorg/apache/xml/serializer/SerializerBase;->setDoctypeSystem(Ljava/lang/String;)V
-Lorg/apache/xml/serializer/SerializerBase;->setIndent(Z)V
-Lorg/apache/xml/serializer/SerializerBase;->setMediaType(Ljava/lang/String;)V
-Lorg/apache/xml/serializer/SerializerBase;->setOmitXMLDeclaration(Z)V
-Lorg/apache/xml/serializer/SerializerBase;->setStandalone(Ljava/lang/String;)V
-Lorg/apache/xml/serializer/SerializerBase;->setStandaloneInternal(Ljava/lang/String;)V
-Lorg/apache/xml/serializer/SerializerBase;->setVersion(Ljava/lang/String;)V
-Lorg/apache/xml/serializer/SerializerFactory;->getSerializer(Ljava/util/Properties;)Lorg/apache/xml/serializer/Serializer;
-Lorg/apache/xml/serializer/SerializerTraceWriter;-><init>(Ljava/io/Writer;Lorg/apache/xml/serializer/SerializerTrace;)V
-Lorg/apache/xml/serializer/ToHTMLStream;-><init>()V
-Lorg/apache/xml/serializer/ToHTMLStream;->getElemDesc(Ljava/lang/String;)Lorg/apache/xml/serializer/ElemDesc;
-Lorg/apache/xml/serializer/ToSAXHandler;-><init>(Lorg/xml/sax/ContentHandler;Ljava/lang/String;)V
-Lorg/apache/xml/serializer/ToSAXHandler;-><init>(Lorg/xml/sax/ContentHandler;Lorg/xml/sax/ext/LexicalHandler;Ljava/lang/String;)V
-Lorg/apache/xml/serializer/ToSAXHandler;->m_lexHandler:Lorg/xml/sax/ext/LexicalHandler;
-Lorg/apache/xml/serializer/ToSAXHandler;->m_saxHandler:Lorg/xml/sax/ContentHandler;
-Lorg/apache/xml/serializer/ToSAXHandler;->reset()Z
-Lorg/apache/xml/serializer/ToSAXHandler;->startDocumentInternal()V
-Lorg/apache/xml/serializer/ToSAXHandler;->startElement(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
-Lorg/apache/xml/serializer/ToStream;->setCdataSectionElements(Ljava/lang/String;Ljava/util/Properties;)V
-Lorg/apache/xml/serializer/ToStream;->setEncoding(Ljava/lang/String;)V
-Lorg/apache/xml/serializer/ToStream;->setIndentAmount(I)V
-Lorg/apache/xml/serializer/ToTextSAXHandler;-><init>(Lorg/xml/sax/ContentHandler;Ljava/lang/String;)V
-Lorg/apache/xml/serializer/ToTextSAXHandler;-><init>(Lorg/xml/sax/ContentHandler;Lorg/xml/sax/ext/LexicalHandler;Ljava/lang/String;)V
-Lorg/apache/xml/serializer/ToTextStream;-><init>()V
-Lorg/apache/xml/serializer/ToUnknownStream;-><init>()V
-Lorg/apache/xml/serializer/ToXMLSAXHandler;-><init>(Lorg/xml/sax/ContentHandler;Ljava/lang/String;)V
-Lorg/apache/xml/serializer/ToXMLSAXHandler;-><init>(Lorg/xml/sax/ContentHandler;Lorg/xml/sax/ext/LexicalHandler;Ljava/lang/String;)V
-Lorg/apache/xml/serializer/ToXMLStream;-><init>()V
-Lorg/apache/xml/serializer/WriterToASCI;-><init>(Ljava/io/OutputStream;)V
-Lorg/apache/xml/serializer/WriterToUTF8Buffered;-><init>(Ljava/io/OutputStream;)V
-Lorg/apache/xml/utils/DefaultErrorHandler;-><init>()V
-Lorg/apache/xml/utils/DefaultErrorHandler;->printLocation(Ljava/io/PrintWriter;Ljava/lang/Throwable;)V
-Lorg/apache/xml/utils/DOMHelper;->isNodeAfter(Lorg/w3c/dom/Node;Lorg/w3c/dom/Node;)Z
-Lorg/apache/xml/utils/DOMHelper;->isNodeTheSame(Lorg/w3c/dom/Node;Lorg/w3c/dom/Node;)Z
-Lorg/apache/xml/utils/FastStringBuffer;->append(Ljava/lang/String;)V
-Lorg/apache/xml/utils/FastStringBuffer;->getString(II)Ljava/lang/String;
-Lorg/apache/xml/utils/FastStringBuffer;->length()I
-Lorg/apache/xml/utils/IntStack;->peek()I
-Lorg/apache/xml/utils/ObjectVector;->elementAt(I)Ljava/lang/Object;
-Lorg/apache/xml/utils/ObjectVector;->size()I
-Lorg/apache/xml/utils/PrefixResolverDefault;-><init>(Lorg/w3c/dom/Node;)V
-Lorg/apache/xml/utils/PrefixResolverDefault;->getNamespaceForPrefix(Ljava/lang/String;)Ljava/lang/String;
-Lorg/apache/xml/utils/QName;-><init>(Ljava/lang/String;)V
-Lorg/apache/xml/utils/QName;-><init>(Ljava/lang/String;Ljava/lang/String;)V
-Lorg/apache/xml/utils/QName;->getLocalName()Ljava/lang/String;
-Lorg/apache/xml/utils/SAXSourceLocator;-><init>(Lorg/xml/sax/SAXParseException;)V
-Lorg/apache/xml/utils/StringBufferPool;->free(Lorg/apache/xml/utils/FastStringBuffer;)V
-Lorg/apache/xml/utils/StringBufferPool;->get()Lorg/apache/xml/utils/FastStringBuffer;
-Lorg/apache/xml/utils/StringVector;->elementAt(I)Ljava/lang/String;
-Lorg/apache/xml/utils/StringVector;->size()I
-Lorg/apache/xml/utils/StylesheetPIHandler;-><init>(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
-Lorg/apache/xml/utils/StylesheetPIHandler;->getAssociatedStylesheet()Ljavax/xml/transform/Source;
-Lorg/apache/xml/utils/StylesheetPIHandler;->setBaseId(Ljava/lang/String;)V
-Lorg/apache/xml/utils/StylesheetPIHandler;->setURIResolver(Ljavax/xml/transform/URIResolver;)V
-Lorg/apache/xml/utils/SuballocatedIntVector;-><init>(I)V
-Lorg/apache/xml/utils/SuballocatedIntVector;->elementAt(I)I
-Lorg/apache/xml/utils/SuballocatedIntVector;->setElementAt(II)V
-Lorg/apache/xml/utils/SuballocatedIntVector;->size()I
-Lorg/apache/xml/utils/SystemIDResolver;->getAbsoluteURI(Ljava/lang/String;)Ljava/lang/String;
-Lorg/apache/xml/utils/SystemIDResolver;->getAbsoluteURI(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
-Lorg/apache/xml/utils/SystemIDResolver;->getAbsoluteURIFromRelative(Ljava/lang/String;)Ljava/lang/String;
-Lorg/apache/xml/utils/SystemIDResolver;->isAbsoluteURI(Ljava/lang/String;)Z
-Lorg/apache/xml/utils/URI$MalformedURIException;-><init>(Ljava/lang/String;)V
-Lorg/apache/xml/utils/URI;-><init>(Ljava/lang/String;)V
-Lorg/apache/xml/utils/URI;-><init>(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
-Lorg/apache/xml/utils/URI;-><init>(Lorg/apache/xml/utils/URI;)V
-Lorg/apache/xml/utils/URI;-><init>(Lorg/apache/xml/utils/URI;Ljava/lang/String;)V
-Lorg/apache/xml/utils/URI;->getFragment()Ljava/lang/String;
-Lorg/apache/xml/utils/URI;->getHost()Ljava/lang/String;
-Lorg/apache/xml/utils/URI;->getPath()Ljava/lang/String;
-Lorg/apache/xml/utils/URI;->getPort()I
-Lorg/apache/xml/utils/URI;->getQueryString()Ljava/lang/String;
-Lorg/apache/xml/utils/URI;->getScheme()Ljava/lang/String;
-Lorg/apache/xml/utils/URI;->getUserinfo()Ljava/lang/String;
-Lorg/apache/xml/utils/URI;->setFragment(Ljava/lang/String;)V
-Lorg/apache/xml/utils/URI;->setHost(Ljava/lang/String;)V
-Lorg/apache/xml/utils/URI;->setPort(I)V
-Lorg/apache/xml/utils/URI;->setScheme(Ljava/lang/String;)V
-Lorg/apache/xml/utils/URI;->setUserinfo(Ljava/lang/String;)V
-Lorg/apache/xml/utils/WrappedRuntimeException;-><init>(Ljava/lang/Exception;)V
-Lorg/apache/xml/utils/WrappedRuntimeException;->getException()Ljava/lang/Exception;
-Lorg/apache/xml/utils/XML11Char;->isXML11ValidNCName(Ljava/lang/String;)Z
-Lorg/apache/xml/utils/XML11Char;->isXML11ValidQName(Ljava/lang/String;)Z
-Lorg/apache/xml/utils/XMLReaderManager;->getInstance()Lorg/apache/xml/utils/XMLReaderManager;
-Lorg/apache/xml/utils/XMLReaderManager;->getXMLReader()Lorg/xml/sax/XMLReader;
-Lorg/apache/xml/utils/XMLReaderManager;->releaseXMLReader(Lorg/xml/sax/XMLReader;)V
-Lorg/apache/xml/utils/XMLString;->dispatchCharactersEvents(Lorg/xml/sax/ContentHandler;)V
-Lorg/apache/xml/utils/XMLString;->equals(Lorg/apache/xml/utils/XMLString;)Z
-Lorg/apache/xml/utils/XMLString;->fixWhiteSpace(ZZZ)Lorg/apache/xml/utils/XMLString;
-Lorg/apache/xml/utils/XMLStringDefault;-><init>(Ljava/lang/String;)V
-Lorg/apache/xml/utils/XMLStringFactory;-><init>()V
-Lorg/apache/xml/utils/XMLStringFactory;->emptystr()Lorg/apache/xml/utils/XMLString;
-Lorg/apache/xml/utils/XMLStringFactory;->newstr(Ljava/lang/String;)Lorg/apache/xml/utils/XMLString;
-Lorg/apache/xpath/axes/ChildTestIterator;-><init>(Lorg/apache/xml/dtm/DTMAxisTraverser;)V
-Lorg/apache/xpath/axes/DescendantIterator;-><init>()V
-Lorg/apache/xpath/axes/LocPathIterator;->getDTM(I)Lorg/apache/xml/dtm/DTM;
-Lorg/apache/xpath/axes/LocPathIterator;->getPrefixResolver()Lorg/apache/xml/utils/PrefixResolver;
-Lorg/apache/xpath/axes/LocPathIterator;->getXPathContext()Lorg/apache/xpath/XPathContext;
-Lorg/apache/xpath/axes/NodeSequence;->getContainedIter()Lorg/apache/xml/dtm/DTMIterator;
-Lorg/apache/xpath/axes/NodeSequence;->nextNode()I
-Lorg/apache/xpath/axes/OneStepIterator;-><init>(Lorg/apache/xml/dtm/DTMAxisIterator;I)V
-Lorg/apache/xpath/CachedXPathAPI;-><init>()V
-Lorg/apache/xpath/CachedXPathAPI;-><init>(Lorg/apache/xpath/CachedXPathAPI;)V
-Lorg/apache/xpath/CachedXPathAPI;->eval(Lorg/w3c/dom/Node;Ljava/lang/String;)Lorg/apache/xpath/objects/XObject;
-Lorg/apache/xpath/CachedXPathAPI;->getXPathContext()Lorg/apache/xpath/XPathContext;
-Lorg/apache/xpath/CachedXPathAPI;->selectNodeList(Lorg/w3c/dom/Node;Ljava/lang/String;)Lorg/w3c/dom/NodeList;
-Lorg/apache/xpath/CachedXPathAPI;->selectNodeList(Lorg/w3c/dom/Node;Ljava/lang/String;Lorg/w3c/dom/Node;)Lorg/w3c/dom/NodeList;
-Lorg/apache/xpath/CachedXPathAPI;->selectSingleNode(Lorg/w3c/dom/Node;Ljava/lang/String;)Lorg/w3c/dom/Node;
-Lorg/apache/xpath/CachedXPathAPI;->selectSingleNode(Lorg/w3c/dom/Node;Ljava/lang/String;Lorg/w3c/dom/Node;)Lorg/w3c/dom/Node;
-Lorg/apache/xpath/compiler/FunctionTable;-><init>()V
-Lorg/apache/xpath/compiler/FunctionTable;->installFunction(Ljava/lang/String;Ljava/lang/Class;)I
-Lorg/apache/xpath/Expression;->assertion(ZLjava/lang/String;)V
-Lorg/apache/xpath/Expression;->error(Lorg/apache/xpath/XPathContext;Ljava/lang/String;[Ljava/lang/Object;)V
-Lorg/apache/xpath/Expression;->exprGetParent()Lorg/apache/xpath/ExpressionNode;
-Lorg/apache/xpath/ExpressionNode;->exprGetParent()Lorg/apache/xpath/ExpressionNode;
-Lorg/apache/xpath/functions/FuncCurrent;-><init>()V
-Lorg/apache/xpath/functions/FuncExtFunction;->getFunctionName()Ljava/lang/String;
-Lorg/apache/xpath/functions/FuncExtFunction;->getMethodKey()Ljava/lang/Object;
-Lorg/apache/xpath/functions/Function;-><init>()V
-Lorg/apache/xpath/functions/WrongNumberArgsException;-><init>(Ljava/lang/String;)V
-Lorg/apache/xpath/NodeSet;-><init>()V
-Lorg/apache/xpath/NodeSet;-><init>(Lorg/w3c/dom/Node;)V
-Lorg/apache/xpath/NodeSet;-><init>(Lorg/w3c/dom/NodeList;)V
-Lorg/apache/xpath/NodeSet;-><init>(Lorg/w3c/dom/traversal/NodeIterator;)V
-Lorg/apache/xpath/NodeSet;->addElement(Lorg/w3c/dom/Node;)V
-Lorg/apache/xpath/NodeSet;->addNode(Lorg/w3c/dom/Node;)V
-Lorg/apache/xpath/NodeSet;->contains(Lorg/w3c/dom/Node;)Z
-Lorg/apache/xpath/NodeSet;->elementAt(I)Lorg/w3c/dom/Node;
-Lorg/apache/xpath/NodeSet;->setShouldCacheNodes(Z)V
-Lorg/apache/xpath/NodeSetDTM;-><init>(Lorg/w3c/dom/NodeList;Lorg/apache/xpath/XPathContext;)V
-Lorg/apache/xpath/NodeSetDTM;-><init>(Lorg/w3c/dom/traversal/NodeIterator;Lorg/apache/xpath/XPathContext;)V
-Lorg/apache/xpath/NodeSetDTM;->addNode(I)V
-Lorg/apache/xpath/NodeSetDTM;->detach()V
-Lorg/apache/xpath/NodeSetDTM;->getLength()I
-Lorg/apache/xpath/NodeSetDTM;->item(I)I
-Lorg/apache/xpath/objects/XBoolean;-><init>(Z)V
-Lorg/apache/xpath/objects/XBoolean;->bool()Z
-Lorg/apache/xpath/objects/XBoolean;->str()Ljava/lang/String;
-Lorg/apache/xpath/objects/XBooleanStatic;-><init>(Z)V
-Lorg/apache/xpath/objects/XNodeSet;-><init>(ILorg/apache/xml/dtm/DTMManager;)V
-Lorg/apache/xpath/objects/XNodeSet;-><init>(Lorg/apache/xml/dtm/DTMIterator;)V
-Lorg/apache/xpath/objects/XNodeSet;-><init>(Lorg/apache/xml/dtm/DTMManager;)V
-Lorg/apache/xpath/objects/XNodeSet;->iterRaw()Lorg/apache/xml/dtm/DTMIterator;
-Lorg/apache/xpath/objects/XNodeSet;->mutableNodeset()Lorg/apache/xpath/NodeSetDTM;
-Lorg/apache/xpath/objects/XNodeSet;->nodelist()Lorg/w3c/dom/NodeList;
-Lorg/apache/xpath/objects/XNumber;-><init>(D)V
-Lorg/apache/xpath/objects/XNumber;->num()D
-Lorg/apache/xpath/objects/XNumber;->str()Ljava/lang/String;
-Lorg/apache/xpath/objects/XObject;->bool()Z
-Lorg/apache/xpath/objects/XObject;->create(Ljava/lang/Object;)Lorg/apache/xpath/objects/XObject;
-Lorg/apache/xpath/objects/XObject;->getType()I
-Lorg/apache/xpath/objects/XObject;->getTypeString()Ljava/lang/String;
-Lorg/apache/xpath/objects/XObject;->iter()Lorg/apache/xml/dtm/DTMIterator;
-Lorg/apache/xpath/objects/XObject;->nodelist()Lorg/w3c/dom/NodeList;
-Lorg/apache/xpath/objects/XObject;->nodeset()Lorg/w3c/dom/traversal/NodeIterator;
-Lorg/apache/xpath/objects/XObject;->num()D
-Lorg/apache/xpath/objects/XObject;->object()Ljava/lang/Object;
-Lorg/apache/xpath/objects/XObject;->str()Ljava/lang/String;
-Lorg/apache/xpath/objects/XObject;->xstr()Lorg/apache/xml/utils/XMLString;
-Lorg/apache/xpath/objects/XRTreeFrag;-><init>(ILorg/apache/xpath/XPathContext;)V
-Lorg/apache/xpath/objects/XRTreeFrag;->asNodeIterator()Lorg/apache/xml/dtm/DTMIterator;
-Lorg/apache/xpath/objects/XRTreeFrag;->convertToNodeset()Lorg/w3c/dom/NodeList;
-Lorg/apache/xpath/objects/XString;-><init>(Ljava/lang/String;)V
-Lorg/apache/xpath/objects/XString;->num()D
-Lorg/apache/xpath/patterns/NodeTest;->setWhatToShow(I)V
-Lorg/apache/xpath/res/XPATHErrorResources;-><init>()V
-Lorg/apache/xpath/res/XPATHMessages;->createXPATHMessage(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
-Lorg/apache/xpath/XPath;-><init>(Ljava/lang/String;Ljavax/xml/transform/SourceLocator;Lorg/apache/xml/utils/PrefixResolver;I)V
-Lorg/apache/xpath/XPath;-><init>(Ljava/lang/String;Ljavax/xml/transform/SourceLocator;Lorg/apache/xml/utils/PrefixResolver;ILjavax/xml/transform/ErrorListener;)V
-Lorg/apache/xpath/XPath;->execute(Lorg/apache/xpath/XPathContext;ILorg/apache/xml/utils/PrefixResolver;)Lorg/apache/xpath/objects/XObject;
-Lorg/apache/xpath/XPath;->execute(Lorg/apache/xpath/XPathContext;Lorg/w3c/dom/Node;Lorg/apache/xml/utils/PrefixResolver;)Lorg/apache/xpath/objects/XObject;
-Lorg/apache/xpath/XPath;->getPatternString()Ljava/lang/String;
-Lorg/apache/xpath/XPathAPI;->selectNodeList(Lorg/w3c/dom/Node;Ljava/lang/String;)Lorg/w3c/dom/NodeList;
-Lorg/apache/xpath/XPathAPI;->selectNodeList(Lorg/w3c/dom/Node;Ljava/lang/String;Lorg/w3c/dom/Node;)Lorg/w3c/dom/NodeList;
-Lorg/apache/xpath/XPathAPI;->selectSingleNode(Lorg/w3c/dom/Node;Ljava/lang/String;)Lorg/w3c/dom/Node;
-Lorg/apache/xpath/XPathAPI;->selectSingleNode(Lorg/w3c/dom/Node;Ljava/lang/String;Lorg/w3c/dom/Node;)Lorg/w3c/dom/Node;
-Lorg/apache/xpath/XPathContext$XPathExpressionContext;->getDTMManager()Lorg/apache/xml/dtm/DTMManager;
-Lorg/apache/xpath/XPathContext$XPathExpressionContext;->getXPathContext()Lorg/apache/xpath/XPathContext;
-Lorg/apache/xpath/XPathContext;-><init>()V
-Lorg/apache/xpath/XPathContext;-><init>(Ljava/lang/Object;)V
-Lorg/apache/xpath/XPathContext;->getAxesIteratorStackStacks()Ljava/util/Stack;
-Lorg/apache/xpath/XPathContext;->getContextNodeList()Lorg/apache/xml/dtm/DTMIterator;
-Lorg/apache/xpath/XPathContext;->getContextNodeListsStack()Ljava/util/Stack;
-Lorg/apache/xpath/XPathContext;->getCurrentExpressionNodeStack()Lorg/apache/xml/utils/IntStack;
-Lorg/apache/xpath/XPathContext;->getCurrentNode()I
-Lorg/apache/xpath/XPathContext;->getCurrentNodeStack()Lorg/apache/xml/utils/IntStack;
-Lorg/apache/xpath/XPathContext;->getDTM(I)Lorg/apache/xml/dtm/DTM;
-Lorg/apache/xpath/XPathContext;->getDTMHandleFromNode(Lorg/w3c/dom/Node;)I
-Lorg/apache/xpath/XPathContext;->getDTMManager()Lorg/apache/xml/dtm/DTMManager;
-Lorg/apache/xpath/XPathContext;->getExpressionContext()Lorg/apache/xalan/extensions/ExpressionContext;
-Lorg/apache/xpath/XPathContext;->getNamespaceContext()Lorg/apache/xml/utils/PrefixResolver;
-Lorg/apache/xpath/XPathContext;->getOwnerObject()Ljava/lang/Object;
-Lorg/apache/xpath/XPathContext;->getSAXLocator()Ljavax/xml/transform/SourceLocator;
-Lorg/apache/xpath/XPathContext;->getVarStack()Lorg/apache/xpath/VariableStack;
-Lorg/apache/xpath/XPathContext;->m_dtmManager:Lorg/apache/xml/dtm/DTMManager;
-Lorg/apache/xpath/XPathContext;->popContextNodeList()V
-Lorg/apache/xpath/XPathContext;->popCurrentNode()V
-Lorg/apache/xpath/XPathContext;->pushContextNodeList(Lorg/apache/xml/dtm/DTMIterator;)V
-Lorg/apache/xpath/XPathContext;->pushCurrentNode(I)V
-Lorg/apache/xpath/XPathContext;->reset()V
-Lorg/apache/xpath/XPathContext;->setAxesIteratorStackStacks(Ljava/util/Stack;)V
-Lorg/apache/xpath/XPathContext;->setContextNodeListsStack(Ljava/util/Stack;)V
-Lorg/apache/xpath/XPathContext;->setCurrentExpressionNodeStack(Lorg/apache/xml/utils/IntStack;)V
-Lorg/apache/xpath/XPathContext;->setCurrentNodeStack(Lorg/apache/xml/utils/IntStack;)V
-Lorg/apache/xpath/XPathContext;->setSecureProcessing(Z)V
-Lorg/apache/xpath/XPathContext;->setVarStack(Lorg/apache/xpath/VariableStack;)V
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 500a04f..46a520a 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -278,7 +278,7 @@
             String resolvedType, boolean fgRequired, String callingPackage, int userId,
             boolean allowBackgroundActivityStarts) throws TransactionTooLargeException;
 
-    public abstract void disconnectActivityFromServices(Object connectionHolder);
+    public abstract void disconnectActivityFromServices(Object connectionHolder, Object conns);
     public abstract void cleanUpServices(int userId, ComponentName component, Intent baseIntent);
     public abstract ActivityInfo getActivityInfoForUser(ActivityInfo aInfo, int userId);
     public abstract void ensureBootCompleted();
diff --git a/core/java/android/app/admin/DevicePolicyEventLogger.java b/core/java/android/app/admin/DevicePolicyEventLogger.java
index 44ea218..95a7973 100644
--- a/core/java/android/app/admin/DevicePolicyEventLogger.java
+++ b/core/java/android/app/admin/DevicePolicyEventLogger.java
@@ -22,9 +22,10 @@
 import android.util.StatsLog;
 
 import com.android.framework.protobuf.nano.MessageNano;
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
 
+import java.util.Arrays;
+
 /**
  * A wrapper for logging managed device events using {@link StatsLog}.
  * <p/>
@@ -45,7 +46,7 @@
  * @see StatsLog
  * @hide
  */
-public final class DevicePolicyEventLogger {
+public class DevicePolicyEventLogger {
     private final int mEventId;
     private int mIntValue;
     private boolean mBooleanValue;
@@ -71,7 +72,6 @@
     /**
      * Returns the event id.
      */
-    @VisibleForTesting
     public int getEventId() {
         return mEventId;
     }
@@ -87,7 +87,6 @@
     /**
      * Returns the generic <code>int</code> value.
      */
-    @VisibleForTesting
     public int getInt() {
         return mIntValue;
     }
@@ -103,7 +102,6 @@
     /**
      * Returns the generic <code>boolean</code> value.
      */
-    @VisibleForTesting
     public boolean getBoolean() {
         return mBooleanValue;
     }
@@ -119,7 +117,6 @@
     /**
      * Returns the time period in milliseconds.
      */
-    @VisibleForTesting
     public long getTimePeriod() {
         return mTimePeriodMs;
     }
@@ -162,11 +159,13 @@
     }
 
     /**
-     * Returns the generic <code>String[]</code> value.
+     * Returns a copy of the generic <code>String[]</code> value.
      */
-    @VisibleForTesting
     public String[] getStringArray() {
-        return mStringArrayValue;
+        if (mStringArrayValue == null) {
+            return null;
+        }
+        return Arrays.copyOf(mStringArrayValue, mStringArrayValue.length);
     }
 
     /**
@@ -188,7 +187,6 @@
     /**
      * Returns the package name of the admin application.
      */
-    @VisibleForTesting
     public String getAdminPackageName() {
         return mAdminPackageName;
     }
diff --git a/core/java/android/bluetooth/BluetoothProfileConnector.java b/core/java/android/bluetooth/BluetoothProfileConnector.java
index d9987249..863fd36 100644
--- a/core/java/android/bluetooth/BluetoothProfileConnector.java
+++ b/core/java/android/bluetooth/BluetoothProfileConnector.java
@@ -32,12 +32,12 @@
  * @hide
  */
 public abstract class BluetoothProfileConnector<T> {
-    private int mProfileId;
+    private final int mProfileId;
     private BluetoothProfile.ServiceListener mServiceListener;
-    private BluetoothProfile mProfileProxy;
+    private final BluetoothProfile mProfileProxy;
     private Context mContext;
-    private String mProfileName;
-    private String mServiceName;
+    private final String mProfileName;
+    private final String mServiceName;
     private volatile T mService;
 
     private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback =
@@ -65,7 +65,7 @@
             logDebug("Proxy object disconnected");
             doUnbind();
             if (mServiceListener != null) {
-                mServiceListener.onServiceDisconnected(BluetoothProfile.A2DP);
+                mServiceListener.onServiceDisconnected(mProfileId);
             }
         }
     };
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index b09eada..cb939f0 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -807,7 +807,7 @@
      *
      * @hide
      */
-    public static final int INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS = 0x00000200;
+    public static final int INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS = 0x00400000;
 
     /** {@hide} */
     public static final int INSTALL_FORCE_VOLUME_UUID = 0x00000200;
diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java
index 41be38a..58aacc2 100644
--- a/core/java/android/content/pm/ShortcutInfo.java
+++ b/core/java/android/content/pm/ShortcutInfo.java
@@ -153,14 +153,21 @@
     public static final int CLONE_REMOVE_RES_NAMES = 1 << 3;
 
     /** @hide */
+    public static final int CLONE_REMOVE_PERSON = 1 << 4;
+
+    /** @hide */
     public static final int CLONE_REMOVE_FOR_CREATOR = CLONE_REMOVE_ICON | CLONE_REMOVE_RES_NAMES;
 
     /** @hide */
     public static final int CLONE_REMOVE_FOR_LAUNCHER = CLONE_REMOVE_ICON | CLONE_REMOVE_INTENT
-            | CLONE_REMOVE_RES_NAMES;
+            | CLONE_REMOVE_RES_NAMES | CLONE_REMOVE_PERSON;
 
     /** @hide */
     public static final int CLONE_REMOVE_FOR_LAUNCHER_APPROVAL = CLONE_REMOVE_INTENT
+            | CLONE_REMOVE_RES_NAMES | CLONE_REMOVE_PERSON;
+
+    /** @hide */
+    public static final int CLONE_REMOVE_FOR_APP_PREDICTION = CLONE_REMOVE_ICON
             | CLONE_REMOVE_RES_NAMES;
 
     /** @hide */
@@ -169,8 +176,11 @@
             CLONE_REMOVE_INTENT,
             CLONE_REMOVE_NON_KEY_INFO,
             CLONE_REMOVE_RES_NAMES,
+            CLONE_REMOVE_PERSON,
             CLONE_REMOVE_FOR_CREATOR,
-            CLONE_REMOVE_FOR_LAUNCHER
+            CLONE_REMOVE_FOR_LAUNCHER,
+            CLONE_REMOVE_FOR_LAUNCHER_APPROVAL,
+            CLONE_REMOVE_FOR_APP_PREDICTION
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CloneFlags {}
@@ -548,7 +558,9 @@
             mDisabledMessage = source.mDisabledMessage;
             mDisabledMessageResId = source.mDisabledMessageResId;
             mCategories = cloneCategories(source.mCategories);
-            mPersons = clonePersons(source.mPersons);
+            if ((cloneFlags & CLONE_REMOVE_PERSON) == 0) {
+                mPersons = clonePersons(source.mPersons);
+            }
             if ((cloneFlags & CLONE_REMOVE_INTENT) == 0) {
                 mIntents = cloneIntents(source.mIntents);
                 mIntentPersistableExtrases =
diff --git a/core/java/android/content/rollback/IRollbackManager.aidl b/core/java/android/content/rollback/IRollbackManager.aidl
index db9fcb6..1b84f29 100644
--- a/core/java/android/content/rollback/IRollbackManager.aidl
+++ b/core/java/android/content/rollback/IRollbackManager.aidl
@@ -30,9 +30,9 @@
             String callerPackageName, in IntentSender statusReceiver);
 
     // Exposed for use from the system server only. Callback from the package
-    // manager during the install flow when user data can be restored for a given
+    // manager during the install flow when user data can be backed up and restored for a given
     // package.
-    void restoreUserData(String packageName, in int[] userIds, int appId, long ceDataInode,
+    void snapshotAndRestoreUserData(String packageName, in int[] userIds, int appId, long ceDataInode,
             String seInfo, int token);
 
     // Exposed for test purposes only.
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index ab630fd..82d4d1d 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -472,8 +472,12 @@
          */
         @MainThread
         @Override
-        public final void initializeInternal(IBinder token, int displayId,
+        public final void initializeInternal(@NonNull IBinder token, int displayId,
                 IInputMethodPrivilegedOperations privilegedOperations) {
+            if (InputMethodPrivilegedOperationsRegistry.isRegistered(token)) {
+                Log.w(TAG, "The token has already registered, ignore this initialization.");
+                return;
+            }
             mPrivOps.set(privilegedOperations);
             InputMethodPrivilegedOperationsRegistry.put(token, mPrivOps);
             updateInputMethodDisplay(displayId);
diff --git a/core/java/android/net/DnsResolver.java b/core/java/android/net/DnsResolver.java
index 4b2b4c3..7a85dcb 100644
--- a/core/java/android/net/DnsResolver.java
+++ b/core/java/android/net/DnsResolver.java
@@ -16,7 +16,7 @@
 
 package android.net;
 
-import static android.net.NetworkUtils.getDnsNetId;
+import static android.net.NetworkUtils.getDnsNetwork;
 import static android.net.NetworkUtils.resNetworkCancel;
 import static android.net.NetworkUtils.resNetworkQuery;
 import static android.net.NetworkUtils.resNetworkResult;
@@ -333,7 +333,7 @@
         final Object lock = new Object();
         final Network queryNetwork;
         try {
-            queryNetwork = (network != null) ? network : new Network(getDnsNetId());
+            queryNetwork = (network != null) ? network : getDnsNetwork();
         } catch (ErrnoException e) {
             executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e)));
             return;
@@ -433,7 +433,7 @@
         final FileDescriptor queryfd;
         final Network queryNetwork;
         try {
-            queryNetwork = (network != null) ? network : new Network(getDnsNetId());
+            queryNetwork = (network != null) ? network : getDnsNetwork();
             queryfd = resNetworkQuery(queryNetwork.getNetIdForResolv(), domain, CLASS_IN, nsType,
                     flags);
         } catch (ErrnoException e) {
diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java
index e892b65..bb344e2 100644
--- a/core/java/android/net/NetworkStats.java
+++ b/core/java/android/net/NetworkStats.java
@@ -18,6 +18,7 @@
 
 import static android.os.Process.CLAT_UID;
 
+import android.annotation.NonNull;
 import android.annotation.UnsupportedAppUsage;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -33,6 +34,7 @@
 import java.io.CharArrayWriter;
 import java.io.PrintWriter;
 import java.util.Arrays;
+import java.util.function.Predicate;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Objects;
@@ -993,23 +995,33 @@
         if (limitUid == UID_ALL && limitTag == TAG_ALL && limitIfaces == INTERFACES_ALL) {
             return;
         }
+        filter(e -> (limitUid == UID_ALL || limitUid == e.uid)
+            && (limitTag == TAG_ALL || limitTag == e.tag)
+            && (limitIfaces == INTERFACES_ALL
+                    || ArrayUtils.contains(limitIfaces, e.iface)));
+    }
 
+    /**
+     * Only keep entries with {@link #set} value less than {@link #SET_DEBUG_START}.
+     *
+     * <p>This mutates the original structure in place.
+     */
+    public void filterDebugEntries() {
+        filter(e -> e.set < SET_DEBUG_START);
+    }
+
+    private void filter(Predicate<Entry> predicate) {
         Entry entry = new Entry();
         int nextOutputEntry = 0;
         for (int i = 0; i < size; i++) {
             entry = getValues(i, entry);
-            final boolean matches =
-                    (limitUid == UID_ALL || limitUid == entry.uid)
-                    && (limitTag == TAG_ALL || limitTag == entry.tag)
-                    && (limitIfaces == INTERFACES_ALL
-                            || ArrayUtils.contains(limitIfaces, entry.iface));
-
-            if (matches) {
-                setValues(nextOutputEntry, entry);
+            if (predicate.test(entry)) {
+                if (nextOutputEntry != i) {
+                    setValues(nextOutputEntry, entry);
+                }
                 nextOutputEntry++;
             }
         }
-
         size = nextOutputEntry;
     }
 
@@ -1175,133 +1187,221 @@
     /**
      * VPN accounting. Move some VPN's underlying traffic to other UIDs that use tun0 iface.
      *
-     * This method should only be called on delta NetworkStats. Do not call this method on a
-     * snapshot {@link NetworkStats} object because the tunUid and/or the underlyingIface may
-     * change over time.
+     * <p>This method should only be called on delta NetworkStats. Do not call this method on a
+     * snapshot {@link NetworkStats} object because the tunUid and/or the underlyingIface may change
+     * over time.
      *
-     * This method performs adjustments for one active VPN package and one VPN iface at a time.
-     *
-     * It is possible for the VPN software to use multiple underlying networks. This method
-     * only migrates traffic for the primary underlying network.
+     * <p>This method performs adjustments for one active VPN package and one VPN iface at a time.
      *
      * @param tunUid uid of the VPN application
      * @param tunIface iface of the vpn tunnel
-     * @param underlyingIface the primary underlying network iface used by the VPN application
-     * @return true if it successfully adjusts the accounting for VPN, false otherwise
+     * @param underlyingIfaces underlying network ifaces used by the VPN application
      */
-    public boolean migrateTun(int tunUid, String tunIface, String underlyingIface) {
-        Entry tunIfaceTotal = new Entry();
-        Entry underlyingIfaceTotal = new Entry();
+    public void migrateTun(int tunUid, @NonNull String tunIface,
+            @NonNull String[] underlyingIfaces) {
+        // Combined usage by all apps using VPN.
+        final Entry tunIfaceTotal = new Entry();
+        // Usage by VPN, grouped by its {@code underlyingIfaces}.
+        final Entry[] perInterfaceTotal = new Entry[underlyingIfaces.length];
+        // Usage by VPN, summed across all its {@code underlyingIfaces}.
+        final Entry underlyingIfacesTotal = new Entry();
 
-        tunAdjustmentInit(tunUid, tunIface, underlyingIface, tunIfaceTotal, underlyingIfaceTotal);
+        for (int i = 0; i < perInterfaceTotal.length; i++) {
+            perInterfaceTotal[i] = new Entry();
+        }
 
-        // If tunIface < underlyingIface, it leaves the overhead traffic in the VPN app.
-        // If tunIface > underlyingIface, the VPN app doesn't get credit for data compression.
+        tunAdjustmentInit(tunUid, tunIface, underlyingIfaces, tunIfaceTotal, perInterfaceTotal,
+                underlyingIfacesTotal);
+
+        // If tunIface < underlyingIfacesTotal, it leaves the overhead traffic in the VPN app.
+        // If tunIface > underlyingIfacesTotal, the VPN app doesn't get credit for data compression.
         // Negative stats should be avoided.
-        Entry pool = tunGetPool(tunIfaceTotal, underlyingIfaceTotal);
-        if (pool.isEmpty()) {
-            return true;
-        }
-        Entry moved =
-                addTrafficToApplications(tunUid, tunIface, underlyingIface, tunIfaceTotal, pool);
-        deductTrafficFromVpnApp(tunUid, underlyingIface, moved);
-
-        if (!moved.isEmpty()) {
-            Slog.wtf(TAG, "Failed to deduct underlying network traffic from VPN package. Moved="
-                    + moved);
-            return false;
-        }
-        return true;
+        final Entry[] moved =
+                addTrafficToApplications(tunUid, tunIface, underlyingIfaces, tunIfaceTotal,
+                        perInterfaceTotal, underlyingIfacesTotal);
+        deductTrafficFromVpnApp(tunUid, underlyingIfaces, moved);
     }
 
     /**
      * Initializes the data used by the migrateTun() method.
      *
-     * This is the first pass iteration which does the following work:
-     * (1) Adds up all the traffic through the tunUid's underlyingIface
-     *     (both foreground and background).
-     * (2) Adds up all the traffic through tun0 excluding traffic from the vpn app itself.
+     * <p>This is the first pass iteration which does the following work:
+     *
+     * <ul>
+     *   <li>Adds up all the traffic through the tunUid's underlyingIfaces (both foreground and
+     *       background).
+     *   <li>Adds up all the traffic through tun0 excluding traffic from the vpn app itself.
+     * </ul>
+     *
+     * @param tunUid uid of the VPN application
+     * @param tunIface iface of the vpn tunnel
+     * @param underlyingIfaces underlying network ifaces used by the VPN application
+     * @param tunIfaceTotal output parameter; combined data usage by all apps using VPN
+     * @param perInterfaceTotal output parameter; data usage by VPN app, grouped by its {@code
+     *     underlyingIfaces}
+     * @param underlyingIfacesTotal output parameter; data usage by VPN, summed across all of its
+     *     {@code underlyingIfaces}
      */
-    private void tunAdjustmentInit(int tunUid, String tunIface, String underlyingIface,
-            Entry tunIfaceTotal, Entry underlyingIfaceTotal) {
-        Entry recycle = new Entry();
+    private void tunAdjustmentInit(int tunUid, @NonNull String tunIface,
+            @NonNull String[] underlyingIfaces, @NonNull Entry tunIfaceTotal,
+            @NonNull Entry[] perInterfaceTotal, @NonNull Entry underlyingIfacesTotal) {
+        final Entry recycle = new Entry();
         for (int i = 0; i < size; i++) {
             getValues(i, recycle);
             if (recycle.uid == UID_ALL) {
                 throw new IllegalStateException(
                         "Cannot adjust VPN accounting on an iface aggregated NetworkStats.");
-            } if (recycle.set == SET_DBG_VPN_IN || recycle.set == SET_DBG_VPN_OUT) {
+            }
+            if (recycle.set == SET_DBG_VPN_IN || recycle.set == SET_DBG_VPN_OUT) {
                 throw new IllegalStateException(
                         "Cannot adjust VPN accounting on a NetworkStats containing SET_DBG_VPN_*");
             }
-
-            if (recycle.uid == tunUid && recycle.tag == TAG_NONE
-                    && Objects.equals(underlyingIface, recycle.iface)) {
-                underlyingIfaceTotal.add(recycle);
+            if (recycle.tag != TAG_NONE) {
+                // TODO(b/123666283): Take all tags for tunUid into account.
+                continue;
             }
 
-            if (recycle.uid != tunUid && recycle.tag == TAG_NONE
-                    && Objects.equals(tunIface, recycle.iface)) {
+            if (recycle.uid == tunUid) {
+                // Add up traffic through tunUid's underlying interfaces.
+                for (int j = 0; j < underlyingIfaces.length; j++) {
+                    if (Objects.equals(underlyingIfaces[j], recycle.iface)) {
+                        perInterfaceTotal[j].add(recycle);
+                        underlyingIfacesTotal.add(recycle);
+                        break;
+                    }
+                }
+            } else if (tunIface.equals(recycle.iface)) {
                 // Add up all tunIface traffic excluding traffic from the vpn app itself.
                 tunIfaceTotal.add(recycle);
             }
         }
     }
 
-    private static Entry tunGetPool(Entry tunIfaceTotal, Entry underlyingIfaceTotal) {
-        Entry pool = new Entry();
-        pool.rxBytes = Math.min(tunIfaceTotal.rxBytes, underlyingIfaceTotal.rxBytes);
-        pool.rxPackets = Math.min(tunIfaceTotal.rxPackets, underlyingIfaceTotal.rxPackets);
-        pool.txBytes = Math.min(tunIfaceTotal.txBytes, underlyingIfaceTotal.txBytes);
-        pool.txPackets = Math.min(tunIfaceTotal.txPackets, underlyingIfaceTotal.txPackets);
-        pool.operations = Math.min(tunIfaceTotal.operations, underlyingIfaceTotal.operations);
-        return pool;
-    }
+    /**
+     * Distributes traffic across apps that are using given {@code tunIface}, and returns the total
+     * traffic that should be moved off of {@code tunUid} grouped by {@code underlyingIfaces}.
+     *
+     * @param tunUid uid of the VPN application
+     * @param tunIface iface of the vpn tunnel
+     * @param underlyingIfaces underlying network ifaces used by the VPN application
+     * @param tunIfaceTotal combined data usage across all apps using {@code tunIface}
+     * @param perInterfaceTotal data usage by VPN app, grouped by its {@code underlyingIfaces}
+     * @param underlyingIfacesTotal data usage by VPN, summed across all of its {@code
+     *     underlyingIfaces}
+     */
+    private Entry[] addTrafficToApplications(int tunUid, @NonNull String tunIface,
+            @NonNull String[] underlyingIfaces, @NonNull Entry tunIfaceTotal,
+            @NonNull Entry[] perInterfaceTotal, @NonNull Entry underlyingIfacesTotal) {
+        // Traffic that should be moved off of each underlying interface for tunUid (see
+        // deductTrafficFromVpnApp below).
+        final Entry[] moved = new Entry[underlyingIfaces.length];
+        for (int i = 0; i < underlyingIfaces.length; i++) {
+            moved[i] = new Entry();
+        }
 
-    private Entry addTrafficToApplications(int tunUid, String tunIface, String underlyingIface,
-            Entry tunIfaceTotal, Entry pool) {
-        Entry moved = new Entry();
-        Entry tmpEntry = new Entry();
-        tmpEntry.iface = underlyingIface;
-        for (int i = 0; i < size; i++) {
-            // the vpn app is excluded from the redistribution but all moved traffic will be
-            // deducted from the vpn app (see deductTrafficFromVpnApp below).
-            if (Objects.equals(iface[i], tunIface) && uid[i] != tunUid) {
-                if (tunIfaceTotal.rxBytes > 0) {
-                    tmpEntry.rxBytes = pool.rxBytes * rxBytes[i] / tunIfaceTotal.rxBytes;
-                } else {
-                    tmpEntry.rxBytes = 0;
-                }
-                if (tunIfaceTotal.rxPackets > 0) {
-                    tmpEntry.rxPackets = pool.rxPackets * rxPackets[i] / tunIfaceTotal.rxPackets;
-                } else {
-                    tmpEntry.rxPackets = 0;
-                }
-                if (tunIfaceTotal.txBytes > 0) {
-                    tmpEntry.txBytes = pool.txBytes * txBytes[i] / tunIfaceTotal.txBytes;
-                } else {
-                    tmpEntry.txBytes = 0;
-                }
-                if (tunIfaceTotal.txPackets > 0) {
-                    tmpEntry.txPackets = pool.txPackets * txPackets[i] / tunIfaceTotal.txPackets;
-                } else {
-                    tmpEntry.txPackets = 0;
-                }
-                if (tunIfaceTotal.operations > 0) {
-                    tmpEntry.operations =
-                            pool.operations * operations[i] / tunIfaceTotal.operations;
-                } else {
-                    tmpEntry.operations = 0;
-                }
-                tmpEntry.uid = uid[i];
-                tmpEntry.tag = tag[i];
+        final Entry tmpEntry = new Entry();
+        final int origSize = size;
+        for (int i = 0; i < origSize; i++) {
+            if (!Objects.equals(iface[i], tunIface)) {
+                // Consider only entries that go onto the VPN interface.
+                continue;
+            }
+            if (uid[i] == tunUid) {
+                // Exclude VPN app from the redistribution, as it can choose to create packet
+                // streams by writing to itself.
+                continue;
+            }
+            tmpEntry.uid = uid[i];
+            tmpEntry.tag = tag[i];
+            tmpEntry.metered = metered[i];
+            tmpEntry.roaming = roaming[i];
+            tmpEntry.defaultNetwork = defaultNetwork[i];
+
+            // In a first pass, compute this entry's total share of data across all
+            // underlyingIfaces. This is computed on the basis of the share of this entry's usage
+            // over tunIface.
+            // TODO: Consider refactoring first pass into a separate helper method.
+            long totalRxBytes = 0;
+            if (tunIfaceTotal.rxBytes > 0) {
+                // Note - The multiplication below should not overflow since NetworkStatsService
+                // processes this every time device has transmitted/received amount equivalent to
+                // global threshold alert (~ 2MB) across all interfaces.
+                final long rxBytesAcrossUnderlyingIfaces =
+                        underlyingIfacesTotal.rxBytes * rxBytes[i] / tunIfaceTotal.rxBytes;
+                // app must not be blamed for more than it consumed on tunIface
+                totalRxBytes = Math.min(rxBytes[i], rxBytesAcrossUnderlyingIfaces);
+            }
+            long totalRxPackets = 0;
+            if (tunIfaceTotal.rxPackets > 0) {
+                final long rxPacketsAcrossUnderlyingIfaces =
+                        underlyingIfacesTotal.rxPackets * rxPackets[i] / tunIfaceTotal.rxPackets;
+                totalRxPackets = Math.min(rxPackets[i], rxPacketsAcrossUnderlyingIfaces);
+            }
+            long totalTxBytes = 0;
+            if (tunIfaceTotal.txBytes > 0) {
+                final long txBytesAcrossUnderlyingIfaces =
+                        underlyingIfacesTotal.txBytes * txBytes[i] / tunIfaceTotal.txBytes;
+                totalTxBytes = Math.min(txBytes[i], txBytesAcrossUnderlyingIfaces);
+            }
+            long totalTxPackets = 0;
+            if (tunIfaceTotal.txPackets > 0) {
+                final long txPacketsAcrossUnderlyingIfaces =
+                        underlyingIfacesTotal.txPackets * txPackets[i] / tunIfaceTotal.txPackets;
+                totalTxPackets = Math.min(txPackets[i], txPacketsAcrossUnderlyingIfaces);
+            }
+            long totalOperations = 0;
+            if (tunIfaceTotal.operations > 0) {
+                final long operationsAcrossUnderlyingIfaces =
+                        underlyingIfacesTotal.operations * operations[i] / tunIfaceTotal.operations;
+                totalOperations = Math.min(operations[i], operationsAcrossUnderlyingIfaces);
+            }
+            // In a second pass, distribute these values across interfaces in the proportion that
+            // each interface represents of the total traffic of the underlying interfaces.
+            for (int j = 0; j < underlyingIfaces.length; j++) {
+                tmpEntry.iface = underlyingIfaces[j];
+                tmpEntry.rxBytes = 0;
+                // Reset 'set' to correct value since it gets updated when adding debug info below.
                 tmpEntry.set = set[i];
-                tmpEntry.metered = metered[i];
-                tmpEntry.roaming = roaming[i];
-                tmpEntry.defaultNetwork = defaultNetwork[i];
+                if (underlyingIfacesTotal.rxBytes > 0) {
+                    tmpEntry.rxBytes =
+                            totalRxBytes
+                                    * perInterfaceTotal[j].rxBytes
+                                    / underlyingIfacesTotal.rxBytes;
+                }
+                tmpEntry.rxPackets = 0;
+                if (underlyingIfacesTotal.rxPackets > 0) {
+                    tmpEntry.rxPackets =
+                            totalRxPackets
+                                    * perInterfaceTotal[j].rxPackets
+                                    / underlyingIfacesTotal.rxPackets;
+                }
+                tmpEntry.txBytes = 0;
+                if (underlyingIfacesTotal.txBytes > 0) {
+                    tmpEntry.txBytes =
+                            totalTxBytes
+                                    * perInterfaceTotal[j].txBytes
+                                    / underlyingIfacesTotal.txBytes;
+                }
+                tmpEntry.txPackets = 0;
+                if (underlyingIfacesTotal.txPackets > 0) {
+                    tmpEntry.txPackets =
+                            totalTxPackets
+                                    * perInterfaceTotal[j].txPackets
+                                    / underlyingIfacesTotal.txPackets;
+                }
+                tmpEntry.operations = 0;
+                if (underlyingIfacesTotal.operations > 0) {
+                    tmpEntry.operations =
+                            totalOperations
+                                    * perInterfaceTotal[j].operations
+                                    / underlyingIfacesTotal.operations;
+                }
+                // tmpEntry now contains the migrated data of the i-th entry for the j-th underlying
+                // interface. Add that data usage to this object.
                 combineValues(tmpEntry);
                 if (tag[i] == TAG_NONE) {
-                    moved.add(tmpEntry);
+                    // Add the migrated data to moved so it is deducted from the VPN app later.
+                    moved[j].add(tmpEntry);
                     // Add debug info
                     tmpEntry.set = SET_DBG_VPN_IN;
                     combineValues(tmpEntry);
@@ -1311,38 +1411,45 @@
         return moved;
     }
 
-    private void deductTrafficFromVpnApp(int tunUid, String underlyingIface, Entry moved) {
-        // Add debug info
-        moved.uid = tunUid;
-        moved.set = SET_DBG_VPN_OUT;
-        moved.tag = TAG_NONE;
-        moved.iface = underlyingIface;
-        moved.metered = METERED_ALL;
-        moved.roaming = ROAMING_ALL;
-        moved.defaultNetwork = DEFAULT_NETWORK_ALL;
-        combineValues(moved);
+    private void deductTrafficFromVpnApp(
+            int tunUid,
+            @NonNull String[] underlyingIfaces,
+            @NonNull Entry[] moved) {
+        for (int i = 0; i < underlyingIfaces.length; i++) {
+            moved[i].uid = tunUid;
+            // Add debug info
+            moved[i].set = SET_DBG_VPN_OUT;
+            moved[i].tag = TAG_NONE;
+            moved[i].iface = underlyingIfaces[i];
+            moved[i].metered = METERED_ALL;
+            moved[i].roaming = ROAMING_ALL;
+            moved[i].defaultNetwork = DEFAULT_NETWORK_ALL;
+            combineValues(moved[i]);
 
-        // Caveat: if the vpn software uses tag, the total tagged traffic may be greater than
-        // the TAG_NONE traffic.
-        //
-        // Relies on the fact that the underlying traffic only has state ROAMING_NO and METERED_NO,
-        // which should be the case as it comes directly from the /proc file. We only blend in the
-        // roaming data after applying these adjustments, by checking the NetworkIdentity of the
-        // underlying iface.
-        int idxVpnBackground = findIndex(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE,
-                METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO);
-        if (idxVpnBackground != -1) {
-            tunSubtract(idxVpnBackground, this, moved);
-        }
+            // Caveat: if the vpn software uses tag, the total tagged traffic may be greater than
+            // the TAG_NONE traffic.
+            //
+            // Relies on the fact that the underlying traffic only has state ROAMING_NO and
+            // METERED_NO, which should be the case as it comes directly from the /proc file.
+            // We only blend in the roaming data after applying these adjustments, by checking the
+            // NetworkIdentity of the underlying iface.
+            final int idxVpnBackground = findIndex(underlyingIfaces[i], tunUid, SET_DEFAULT,
+                            TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO);
+            if (idxVpnBackground != -1) {
+                // Note - tunSubtract also updates moved[i]; whatever traffic that's left is removed
+                // from foreground usage.
+                tunSubtract(idxVpnBackground, this, moved[i]);
+            }
 
-        int idxVpnForeground = findIndex(underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE,
-                METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO);
-        if (idxVpnForeground != -1) {
-            tunSubtract(idxVpnForeground, this, moved);
+            final int idxVpnForeground = findIndex(underlyingIfaces[i], tunUid, SET_FOREGROUND,
+                            TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO);
+            if (idxVpnForeground != -1) {
+                tunSubtract(idxVpnForeground, this, moved[i]);
+            }
         }
     }
 
-    private static void tunSubtract(int i, NetworkStats left, Entry right) {
+    private static void tunSubtract(int i, @NonNull NetworkStats left, @NonNull Entry right) {
         long rxBytes = Math.min(left.rxBytes[i], right.rxBytes);
         left.rxBytes[i] -= rxBytes;
         right.rxBytes -= rxBytes;
diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java
index a640f83..d0f54b4 100644
--- a/core/java/android/net/NetworkUtils.java
+++ b/core/java/android/net/NetworkUtils.java
@@ -158,10 +158,9 @@
 
     /**
      * DNS resolver series jni method.
-     * Attempts to get netid of network which resolver will
-     * use if no network is explicitly selected.
+     * Attempts to get network which resolver will use if no network is explicitly selected.
      */
-    public static native int getDnsNetId() throws ErrnoException;
+    public static native Network getDnsNetwork() throws ErrnoException;
 
     /**
      * Get the tcp repair window associated with the {@code fd}.
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
index deb9eba..77d367f 100755
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -113,8 +113,7 @@
 
     /**
      * A hardware serial number, if available. Alphanumeric only, case-insensitive.
-     * For apps targeting SDK higher than {@link Build.VERSION_CODES#O_MR1} this
-     * field is set to {@link Build#UNKNOWN}.
+     * This field is always set to {@link Build#UNKNOWN}.
      *
      * @deprecated Use {@link #getSerial()} instead.
      **/
diff --git a/core/java/android/os/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java
index 5039b31..24a1477 100644
--- a/core/java/android/os/GraphicsEnvironment.java
+++ b/core/java/android/os/GraphicsEnvironment.java
@@ -231,13 +231,6 @@
     }
 
     /**
-     * Check whether application is profileable
-     */
-    private static boolean isProfileable(Context context) {
-        return context.getApplicationInfo().isProfileableByShell();
-    }
-
-    /**
      * Store the layer paths available to the loader.
      */
     public void setLayerPaths(ClassLoader classLoader,
@@ -287,11 +280,11 @@
         String layerPaths = "";
 
         // Only enable additional debug functionality if the following conditions are met:
-        // 1. App is debuggable, profileable, or device is rooted
+        // 1. App is debuggable or device is rooted
         // 2. ENABLE_GPU_DEBUG_LAYERS is true
         // 3. Package name is equal to GPU_DEBUG_APP
 
-        if (isDebuggable(context) || isProfileable(context) || (getCanLoadSystemLibraries() == 1)) {
+        if (isDebuggable(context) || (getCanLoadSystemLibraries() == 1)) {
 
             final int enable = coreSettings.getInt(Settings.Global.ENABLE_GPU_DEBUG_LAYERS, 0);
 
@@ -473,9 +466,8 @@
      */
     private String getAngleDebugPackage(Context context, Bundle coreSettings) {
         final boolean appIsDebuggable = isDebuggable(context);
-        final boolean appIsProfileable = isProfileable(context);
         final boolean deviceIsDebuggable = getCanLoadSystemLibraries() == 1;
-        if (appIsDebuggable || appIsProfileable || deviceIsDebuggable) {
+        if (appIsDebuggable || deviceIsDebuggable) {
             String debugPackage;
 
             if (coreSettings != null) {
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index de963c9..fe2e948 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.TestApi;
 import android.annotation.UnsupportedAppUsage;
 import android.text.TextUtils;
 import android.util.ArrayMap;
@@ -2001,6 +2002,7 @@
      * @hide
      */
     @UnsupportedAppUsage
+    @TestApi
     public final int readExceptionCode() {
         int code = readInt();
         if (code == EX_HAS_REPLY_HEADER) {
diff --git a/core/java/android/os/SystemProperties.java b/core/java/android/os/SystemProperties.java
index edfdda8bb..4538410 100644
--- a/core/java/android/os/SystemProperties.java
+++ b/core/java/android/os/SystemProperties.java
@@ -174,6 +174,7 @@
      * @hide
      */
     @SystemApi
+    @TestApi
     public static boolean getBoolean(@NonNull String key, boolean def) {
         if (TRACK_KEY_ACCESS) onKeyAccess(key);
         return native_get_boolean(key, def);
diff --git a/core/java/android/os/image/DynamicSystemClient.java b/core/java/android/os/image/DynamicSystemClient.java
index f1f24fb..921f0f2 100644
--- a/core/java/android/os/image/DynamicSystemClient.java
+++ b/core/java/android/os/image/DynamicSystemClient.java
@@ -22,6 +22,7 @@
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
+import android.annotation.TestApi;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -67,6 +68,7 @@
  * @hide
  */
 @SystemApi
+@TestApi
 public class DynamicSystemClient {
     /** @hide */
     @IntDef(prefix = { "STATUS_" }, value = {
@@ -283,6 +285,7 @@
      * @hide
      */
     @SystemApi
+    @TestApi
     public DynamicSystemClient(@NonNull Context context) {
         mContext = context;
         mConnection = new DynSystemServiceConnection();
@@ -314,8 +317,11 @@
      * Bind to {@code DynamicSystem} installation service. Binding to the installation service
      * allows it to send status updates to {@link #OnStatusChangedListener}. It is recommanded
      * to bind before calling {@link #start} and get status updates.
+     * @hide
      */
     @RequiresPermission(android.Manifest.permission.INSTALL_DYNAMIC_SYSTEM)
+    @SystemApi
+    @TestApi
     public void bind() {
         if (!featureFlagEnabled()) {
             Slog.w(TAG, FeatureFlagUtils.DYNAMIC_SYSTEM + " not enabled; bind() aborted.");
@@ -334,8 +340,11 @@
     /**
      * Unbind from {@code DynamicSystem} installation service. Unbinding from the installation
      * service stops it from sending following status updates.
+     * @hide
      */
     @RequiresPermission(android.Manifest.permission.INSTALL_DYNAMIC_SYSTEM)
+    @SystemApi
+    @TestApi
     public void unbind() {
         if (!mBound) {
             return;
@@ -367,8 +376,11 @@
      *
      * @param systemUrl a network Uri, a file Uri or a content Uri pointing to a system image file.
      * @param systemSize size of system image.
+     * @hide
      */
     @RequiresPermission(android.Manifest.permission.INSTALL_DYNAMIC_SYSTEM)
+    @SystemApi
+    @TestApi
     public void start(@NonNull Uri systemUrl, @BytesLong long systemSize) {
         start(systemUrl, systemSize, DEFAULT_USERDATA_SIZE);
     }
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 66b9d168..0db5c36 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -8081,6 +8081,15 @@
                 "lock_screen_show_silent_notifications";
 
         /**
+         * Indicates whether snooze options should be shown on notifications
+         * <p>
+         * Type: int (0 for false, 1 for true)
+         *
+         * @hide
+         */
+        public static final String SHOW_NOTIFICATION_SNOOZE = "show_notification_snooze";
+
+        /**
          * List of TV inputs that are currently hidden. This is a string
          * containing the IDs of all hidden TV inputs. Each ID is encoded by
          * {@link android.net.Uri#encode(String)} and separated by ':'.
@@ -8968,6 +8977,7 @@
             LOCK_SCREEN_CUSTOM_CLOCK_FACE,
             LOCK_SCREEN_SHOW_NOTIFICATIONS,
             LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS,
+            SHOW_NOTIFICATION_SNOOZE,
             ZEN_DURATION,
             SHOW_ZEN_UPGRADE_NOTIFICATION,
             SHOW_ZEN_SETTINGS_SUGGESTION,
@@ -9150,6 +9160,7 @@
             VALIDATORS.put(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, BOOLEAN_VALIDATOR);
             VALIDATORS.put(LOCK_SCREEN_SHOW_NOTIFICATIONS, BOOLEAN_VALIDATOR);
             VALIDATORS.put(LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, BOOLEAN_VALIDATOR);
+            VALIDATORS.put(SHOW_NOTIFICATION_SNOOZE, BOOLEAN_VALIDATOR);
             VALIDATORS.put(ZEN_DURATION, ZEN_DURATION_VALIDATOR);
             VALIDATORS.put(SHOW_ZEN_UPGRADE_NOTIFICATION, BOOLEAN_VALIDATOR);
             VALIDATORS.put(SHOW_ZEN_SETTINGS_SUGGESTION, BOOLEAN_VALIDATOR);
diff --git a/core/java/android/service/notification/Adjustment.java b/core/java/android/service/notification/Adjustment.java
index e81ce7f..aa11445 100644
--- a/core/java/android/service/notification/Adjustment.java
+++ b/core/java/android/service/notification/Adjustment.java
@@ -16,6 +16,7 @@
 package android.service.notification;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.StringDef;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
@@ -48,6 +49,7 @@
     private final CharSequence mExplanation;
     private final Bundle mSignals;
     private final int mUser;
+    @Nullable private String mIssuer;
 
     /** @hide */
     @StringDef (prefix = { "KEY_" }, value = {
@@ -183,6 +185,7 @@
         }
         mSignals = in.readBundle();
         mUser = in.readInt();
+        mIssuer = in.readString();
     }
 
     public static final @android.annotation.NonNull Creator<Adjustment> CREATOR = new Creator<Adjustment>() {
@@ -251,6 +254,7 @@
         }
         dest.writeBundle(mSignals);
         dest.writeInt(mUser);
+        dest.writeString(mIssuer);
     }
 
     @Override
@@ -259,4 +263,14 @@
                 + "mSignals=" + mSignals
                 + '}';
     }
+
+    /** @hide */
+    public void setIssuer(@Nullable String issuer) {
+        mIssuer = issuer;
+    }
+
+    /** @hide */
+    public @Nullable String getIssuer() {
+        return mIssuer;
+    }
 }
diff --git a/core/java/android/service/notification/NotificationAssistantService.java b/core/java/android/service/notification/NotificationAssistantService.java
index cafeb87..12d3228 100644
--- a/core/java/android/service/notification/NotificationAssistantService.java
+++ b/core/java/android/service/notification/NotificationAssistantService.java
@@ -236,6 +236,7 @@
     public final void adjustNotification(@NonNull Adjustment adjustment) {
         if (!isBound()) return;
         try {
+            setAdjustmentIssuer(adjustment);
             getNotificationInterface().applyEnqueuedAdjustmentFromAssistant(mWrapper, adjustment);
         } catch (android.os.RemoteException ex) {
             Log.v(TAG, "Unable to contact notification manager", ex);
@@ -253,6 +254,9 @@
     public final void adjustNotifications(@NonNull List<Adjustment> adjustments) {
         if (!isBound()) return;
         try {
+            for (Adjustment adjustment : adjustments) {
+                setAdjustmentIssuer(adjustment);
+            }
             getNotificationInterface().applyAdjustmentsFromAssistant(mWrapper, adjustments);
         } catch (android.os.RemoteException ex) {
             Log.v(TAG, "Unable to contact notification manager", ex);
@@ -366,6 +370,12 @@
         }
     }
 
+    private void setAdjustmentIssuer(@Nullable Adjustment adjustment) {
+        if (adjustment != null) {
+            adjustment.setIssuer(getOpPackageName() + "/" + getClass().getName());
+        }
+    }
+
     private final class MyHandler extends Handler {
         public static final int MSG_ON_NOTIFICATION_ENQUEUED = 1;
         public static final int MSG_ON_NOTIFICATION_SNOOZED = 2;
@@ -389,6 +399,7 @@
                     NotificationChannel channel = (NotificationChannel) args.arg2;
                     args.recycle();
                     Adjustment adjustment = onNotificationEnqueued(sbn, channel);
+                    setAdjustmentIssuer(adjustment);
                     if (adjustment != null) {
                         if (!isBound()) {
                             Log.w(TAG, "MSG_ON_NOTIFICATION_ENQUEUED: service not bound, skip.");
diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java
index c42dc81..324e02c 100644
--- a/core/java/android/util/FeatureFlagUtils.java
+++ b/core/java/android/util/FeatureFlagUtils.java
@@ -16,6 +16,7 @@
 
 package android.util;
 
+import android.annotation.TestApi;
 import android.content.Context;
 import android.os.SystemProperties;
 import android.provider.Settings;
@@ -29,6 +30,7 @@
  *
  * @hide
  */
+@TestApi
 public class FeatureFlagUtils {
 
     public static final String FFLAG_PREFIX = "sys.fflag.";
diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java
index 9436633..73e0e4b 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -1301,7 +1301,7 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
         final boolean isOutside =
-                event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
+                event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
                 || event.getAction() == MotionEvent.ACTION_OUTSIDE;
         if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
             return true;
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index a88c51a..ee99837 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -185,7 +185,7 @@
     private static final int SHARE_TARGET_QUERY_PACKAGE_LIMIT = 20;
 
     private static final int QUERY_TARGET_SERVICE_LIMIT = 5;
-    private static final int WATCHDOG_TIMEOUT_MILLIS = 3000;
+    private static final int WATCHDOG_TIMEOUT_MILLIS = 5000;
 
     private static final int DEFAULT_SALT_EXPIRATION_DAYS = 7;
     private int mMaxHashSaltDays = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI,
@@ -252,6 +252,123 @@
     // Sorted list of DisplayResolveInfos for the alphabetical app section.
     private List<ResolverActivity.DisplayResolveInfo> mSortedList = new ArrayList<>();
 
+    private ContentPreviewCoordinator mPreviewCoord;
+
+    private class ContentPreviewCoordinator {
+        private static final int IMAGE_LOAD_TIMEOUT_MILLIS = 300;
+        private static final int IMAGE_FADE_IN_MILLIS = 150;
+        private static final int IMAGE_LOAD_TIMEOUT = 1;
+        private static final int IMAGE_LOAD_INTO_VIEW = 2;
+
+        private final View mParentView;
+        private boolean mHideParentOnFail;
+        private boolean mAtLeastOneLoaded = false;
+
+        class LoadUriTask {
+            public final Uri mUri;
+            public final int mImageResourceId;
+            public final int mExtraCount;
+            public final Bitmap mBmp;
+
+            LoadUriTask(int imageResourceId, Uri uri, int extraCount, Bitmap bmp) {
+                this.mImageResourceId = imageResourceId;
+                this.mUri = uri;
+                this.mExtraCount = extraCount;
+                this.mBmp = bmp;
+            }
+        }
+
+        // If at least one image loads within the timeout period, allow other
+        // loads to continue. Otherwise terminate and optionally hide
+        // the parent area
+        private final Handler mHandler = new Handler() {
+            @Override
+            public void handleMessage(Message msg) {
+                switch (msg.what) {
+                    case IMAGE_LOAD_TIMEOUT:
+                        maybeHideContentPreview();
+                        break;
+
+                    case IMAGE_LOAD_INTO_VIEW:
+                        if (isFinishing()) break;
+
+                        LoadUriTask task = (LoadUriTask) msg.obj;
+                        RoundedRectImageView imageView = mParentView.findViewById(
+                                task.mImageResourceId);
+                        if (task.mBmp == null) {
+                            imageView.setVisibility(View.GONE);
+                            maybeHideContentPreview();
+                            return;
+                        }
+
+                        mAtLeastOneLoaded = true;
+                        imageView.setVisibility(View.VISIBLE);
+                        imageView.setAlpha(0.0f);
+                        imageView.setImageBitmap(task.mBmp);
+
+                        ValueAnimator fadeAnim = ObjectAnimator.ofFloat(imageView, "alpha", 0.0f,
+                                1.0f);
+                        fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f));
+                        fadeAnim.setDuration(IMAGE_FADE_IN_MILLIS);
+                        fadeAnim.start();
+
+                        if (task.mExtraCount > 0) {
+                            imageView.setExtraImageCount(task.mExtraCount);
+                        }
+                }
+            }
+        };
+
+        ContentPreviewCoordinator(View parentView, boolean hideParentOnFail) {
+            super();
+
+            this.mParentView = parentView;
+            this.mHideParentOnFail = hideParentOnFail;
+        }
+
+        private void loadUriIntoView(final int imageResourceId, final Uri uri,
+                final int extraImages) {
+            mHandler.sendEmptyMessageDelayed(IMAGE_LOAD_TIMEOUT, IMAGE_LOAD_TIMEOUT_MILLIS);
+
+            AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
+                final Bitmap bmp = loadThumbnail(uri, new Size(200, 200));
+                final Message msg = Message.obtain();
+                msg.what = IMAGE_LOAD_INTO_VIEW;
+                msg.obj = new LoadUriTask(imageResourceId, uri, extraImages, bmp);
+                mHandler.sendMessage(msg);
+            });
+        }
+
+        private void cancelLoads() {
+            mHandler.removeMessages(IMAGE_LOAD_INTO_VIEW);
+            mHandler.removeMessages(IMAGE_LOAD_TIMEOUT);
+        }
+
+        private void maybeHideContentPreview() {
+            if (!mAtLeastOneLoaded && mHideParentOnFail) {
+                Log.i(TAG, "Hiding image preview area. Timed out waiting for preview to load"
+                        + " within " + IMAGE_LOAD_TIMEOUT_MILLIS + "ms.");
+                collapseParentView();
+                if (mChooserRowAdapter != null) {
+                    mChooserRowAdapter.hideContentPreview();
+                }
+                mHideParentOnFail = false;
+            }
+        }
+
+        private void collapseParentView() {
+            // This will effectively hide the content preview row by forcing the height
+            // to zero. It is faster than forcing a relayout of the listview
+            final View v = mParentView;
+            int widthSpec = MeasureSpec.makeMeasureSpec(v.getWidth(), MeasureSpec.EXACTLY);
+            int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY);
+            v.measure(widthSpec, heightSpec);
+            v.getLayoutParams().height = 0;
+            v.layout(v.getLeft(), v.getTop(), v.getRight(), v.getTop());
+            v.invalidate();
+        }
+    }
+
     private final Handler mChooserHandler = new Handler() {
         @Override
         public void handleMessage(Message msg) {
@@ -621,14 +738,15 @@
     private ViewGroup displayContentPreview(@ContentPreviewType int previewType,
             Intent targetIntent, LayoutInflater layoutInflater, ViewGroup convertView,
             ViewGroup parent) {
+        if (convertView != null) return convertView;
+
         switch (previewType) {
             case CONTENT_PREVIEW_TEXT:
-                return displayTextContentPreview(targetIntent, layoutInflater, convertView, parent);
+                return displayTextContentPreview(targetIntent, layoutInflater, parent);
             case CONTENT_PREVIEW_IMAGE:
-                return displayImageContentPreview(targetIntent, layoutInflater, convertView,
-                        parent);
+                return displayImageContentPreview(targetIntent, layoutInflater, parent);
             case CONTENT_PREVIEW_FILE:
-                return displayFileContentPreview(targetIntent, layoutInflater, convertView, parent);
+                return displayFileContentPreview(targetIntent, layoutInflater, parent);
             default:
                 Log.e(TAG, "Unexpected content preview type: " + previewType);
         }
@@ -637,10 +755,9 @@
     }
 
     private ViewGroup displayTextContentPreview(Intent targetIntent, LayoutInflater layoutInflater,
-            ViewGroup convertView, ViewGroup parent) {
-        ViewGroup contentPreviewLayout =
-                convertView != null ? convertView : (ViewGroup) layoutInflater.inflate(
-                        R.layout.chooser_grid_preview_text, parent, false);
+            ViewGroup parent) {
+        ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
+                R.layout.chooser_grid_preview_text, parent, false);
 
         contentPreviewLayout.findViewById(R.id.copy_button).setOnClickListener(
                 this::onCopyButtonClicked);
@@ -677,12 +794,8 @@
             if (previewThumbnail == null) {
                 previewThumbnailView.setVisibility(View.GONE);
             } else {
-                Bitmap bmp = loadThumbnail(previewThumbnail, new Size(100, 100));
-                if (bmp == null) {
-                    previewThumbnailView.setVisibility(View.GONE);
-                } else {
-                    previewThumbnailView.setImageBitmap(bmp);
-                }
+                mPreviewCoord = new ContentPreviewCoordinator(contentPreviewLayout, false);
+                mPreviewCoord.loadUriIntoView(R.id.content_preview_thumbnail, previewThumbnail, 0);
             }
         }
 
@@ -690,15 +803,15 @@
     }
 
     private ViewGroup displayImageContentPreview(Intent targetIntent, LayoutInflater layoutInflater,
-            ViewGroup convertView, ViewGroup parent) {
-        ViewGroup contentPreviewLayout =
-                convertView != null ? convertView : (ViewGroup) layoutInflater.inflate(
-                        R.layout.chooser_grid_preview_image, parent, false);
+            ViewGroup parent) {
+        ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
+                R.layout.chooser_grid_preview_image, parent, false);
+        mPreviewCoord = new ContentPreviewCoordinator(contentPreviewLayout, true);
 
         String action = targetIntent.getAction();
         if (Intent.ACTION_SEND.equals(action)) {
             Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
-            loadUriIntoView(R.id.content_preview_image_1_large, uri, contentPreviewLayout);
+            mPreviewCoord.loadUriIntoView(R.id.content_preview_image_1_large, uri, 0);
         } else {
             ContentResolver resolver = getContentResolver();
 
@@ -717,21 +830,16 @@
                 return contentPreviewLayout;
             }
 
-            loadUriIntoView(R.id.content_preview_image_1_large, imageUris.get(0),
-                    contentPreviewLayout);
+            mPreviewCoord.loadUriIntoView(R.id.content_preview_image_1_large, imageUris.get(0), 0);
 
             if (imageUris.size() == 2) {
-                loadUriIntoView(R.id.content_preview_image_2_large, imageUris.get(1),
-                        contentPreviewLayout);
+                mPreviewCoord.loadUriIntoView(R.id.content_preview_image_2_large,
+                        imageUris.get(1), 0);
             } else if (imageUris.size() > 2) {
-                loadUriIntoView(R.id.content_preview_image_2_small, imageUris.get(1),
-                        contentPreviewLayout);
-                RoundedRectImageView imageView = loadUriIntoView(
-                        R.id.content_preview_image_3_small, imageUris.get(2), contentPreviewLayout);
-
-                if (imageUris.size() > 3) {
-                    imageView.setExtraImageCount(imageUris.size() - 3);
-                }
+                mPreviewCoord.loadUriIntoView(R.id.content_preview_image_2_small,
+                        imageUris.get(1), 0);
+                mPreviewCoord.loadUriIntoView(R.id.content_preview_image_3_small,
+                        imageUris.get(2), imageUris.size() - 3);
             }
         }
 
@@ -803,11 +911,10 @@
     }
 
     private ViewGroup displayFileContentPreview(Intent targetIntent, LayoutInflater layoutInflater,
-            ViewGroup convertView, ViewGroup parent) {
+            ViewGroup parent) {
 
-        ViewGroup contentPreviewLayout =
-                convertView != null ? convertView : (ViewGroup) layoutInflater.inflate(
-                        R.layout.chooser_grid_preview_file, parent, false);
+        ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
+                R.layout.chooser_grid_preview_file, parent, false);
 
         // TODO(b/120417119): Disable file copy until after moving to sysui,
         // due to permissions issues
@@ -839,6 +946,10 @@
                         R.id.content_preview_filename);
                 fileNameView.setText(fileName);
 
+                View thumbnailView = contentPreviewLayout.findViewById(
+                        R.id.content_preview_file_thumbnail);
+                thumbnailView.setVisibility(View.GONE);
+
                 ImageView fileIconView = contentPreviewLayout.findViewById(
                         R.id.content_preview_file_icon);
                 fileIconView.setVisibility(View.VISIBLE);
@@ -849,32 +960,25 @@
         return contentPreviewLayout;
     }
 
-    private void loadFileUriIntoView(Uri uri, View parent) {
+    private void loadFileUriIntoView(final Uri uri, final View parent) {
         FileInfo fileInfo = extractFileInfo(uri, getContentResolver());
 
         TextView fileNameView = parent.findViewById(R.id.content_preview_filename);
         fileNameView.setText(fileInfo.name);
 
         if (fileInfo.hasThumbnail) {
-            loadUriIntoView(R.id.content_preview_file_thumbnail, uri, parent);
+            mPreviewCoord = new ContentPreviewCoordinator(parent, false);
+            mPreviewCoord.loadUriIntoView(R.id.content_preview_file_thumbnail, uri, 0);
         } else {
+            View thumbnailView = parent.findViewById(R.id.content_preview_file_thumbnail);
+            thumbnailView.setVisibility(View.GONE);
+
             ImageView fileIconView = parent.findViewById(R.id.content_preview_file_icon);
             fileIconView.setVisibility(View.VISIBLE);
             fileIconView.setImageResource(R.drawable.chooser_file_generic);
         }
     }
 
-    private RoundedRectImageView loadUriIntoView(int imageResourceId, Uri uri, View parent) {
-        RoundedRectImageView imageView = parent.findViewById(imageResourceId);
-        Bitmap bmp = loadThumbnail(uri, new Size(200, 200));
-        if (bmp != null) {
-            imageView.setVisibility(View.VISIBLE);
-            imageView.setImageBitmap(bmp);
-        }
-
-        return imageView;
-    }
-
     @VisibleForTesting
     protected boolean isImageType(String mimeType) {
         return mimeType != null && mimeType.startsWith("image/");
@@ -944,6 +1048,9 @@
         mChooserHandler.removeMessages(CHOOSER_TARGET_SERVICE_RESULT);
         mChooserHandler.removeMessages(SHORTCUT_MANAGER_SHARE_TARGET_RESULT);
         mChooserHandler.removeMessages(SHORTCUT_MANAGER_SHARE_TARGET_RESULT_COMPLETED);
+
+        if (mPreviewCoord != null) mPreviewCoord.cancelLoads();
+
         if (mAppPredictor != null) {
             mAppPredictor.unregisterPredictionUpdates(mAppPredictorCallback);
             mAppPredictor.destroy();
@@ -1327,6 +1434,22 @@
                 List<ShortcutManager.ShareShortcutInfo> resultList,
                 List<DisplayResolveInfo> driList,
                 @Nullable List<AppTarget> appTargets) {
+        if (appTargets != null && appTargets.size() != resultList.size()) {
+            throw new RuntimeException("resultList and appTargets must have the same size."
+                    + " resultList.size()=" + resultList.size()
+                    + " appTargets.size()=" + appTargets.size());
+        }
+
+        for (int i = resultList.size() - 1; i >= 0; i--) {
+            final String packageName = resultList.get(i).getTargetComponent().getPackageName();
+            if (!isPackageEnabled(packageName)) {
+                resultList.remove(i);
+                if (appTargets != null) {
+                    appTargets.remove(i);
+                }
+            }
+        }
+
         // Match ShareShortcutInfos with DisplayResolveInfos to be able to use the old code path
         // for direct share targets. After ShareSheet is refactored we should use the
         // ShareShortcutInfos directly.
@@ -1340,7 +1463,6 @@
                     ChooserTarget chooserTarget = convertToChooserTarget(shareShortcutInfo);
                     chooserTargets.add(chooserTarget);
                     if (mDirectShareAppTargetCache != null && appTargets != null) {
-                        // Note that appTargets.size() == resultList.size() is always true.
                         mDirectShareAppTargetCache.put(chooserTarget, appTargets.get(j));
                     }
                 }
@@ -1366,6 +1488,24 @@
         mChooserHandler.sendMessage(msg);
     }
 
+    private boolean isPackageEnabled(String packageName) {
+        if (TextUtils.isEmpty(packageName)) {
+            return false;
+        }
+        ApplicationInfo appInfo;
+        try {
+            appInfo = getPackageManager().getApplicationInfo(packageName, 0);
+        } catch (NameNotFoundException e) {
+            return false;
+        }
+
+        if (appInfo != null && appInfo.enabled
+                && (appInfo.flags & ApplicationInfo.FLAG_SUSPENDED) == 0) {
+            return true;
+        }
+        return false;
+    }
+
     private ChooserTarget convertToChooserTarget(ShortcutManager.ShareShortcutInfo shareShortcut) {
         ShortcutInfo shortcutInfo = shareShortcut.getShortcutInfo();
         Bundle extras = new Bundle();
@@ -1496,7 +1636,8 @@
      */
     @Nullable
     private AppPredictor getAppPredictorForDirectShareIfEnabled() {
-        return USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS ? getAppPredictor() : null;
+        return USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS && !ActivityManager.isLowRamDeviceStatic()
+                ? getAppPredictor() : null;
     }
 
     /**
@@ -2036,9 +2177,12 @@
         }
 
         int availableWidth = right - left - v.getPaddingLeft() - v.getPaddingRight();
-        if (mChooserRowAdapter.calculateChooserTargetWidth(availableWidth)
+        if (mChooserRowAdapter.consumeLayoutRequest()
+                || mChooserRowAdapter.calculateChooserTargetWidth(availableWidth)
                 || mAdapterView.getAdapter() == null) {
-            mAdapterView.setAdapter(mChooserRowAdapter);
+            if (mAdapterView.getAdapter() == null) {
+                mAdapterView.setAdapter(mChooserRowAdapter);
+            }
 
             getMainThreadHandler().post(() -> {
                 if (mResolverDrawerLayout == null || mChooserRowAdapter == null) {
@@ -2239,6 +2383,8 @@
 
         @Override
         public void onListRebuilt() {
+            updateAlphabeticalList();
+
             // don't support direct share on low ram devices
             if (ActivityManager.isLowRamDeviceStatic()) {
                 return;
@@ -2269,7 +2415,6 @@
 
                 queryTargetServices(this);
             }
-            updateAlphabeticalList();
         }
 
         @Override
@@ -2589,6 +2734,9 @@
         private int mChooserTargetWidth = 0;
         private boolean mShowAzLabelIfPoss;
 
+        private boolean mHideContentPreview = false;
+        private boolean mLayoutRequested = false;
+
         private static final int VIEW_TYPE_DIRECT_SHARE = 0;
         private static final int VIEW_TYPE_NORMAL = 1;
         private static final int VIEW_TYPE_CONTENT_PREVIEW = 2;
@@ -2651,6 +2799,18 @@
             return maxTargets;
         }
 
+        public void hideContentPreview() {
+            mHideContentPreview = true;
+            mLayoutRequested = true;
+            notifyDataSetChanged();
+        }
+
+        public boolean consumeLayoutRequest() {
+            boolean oldValue = mLayoutRequested;
+            mLayoutRequested = false;
+            return oldValue;
+        }
+
         @Override
         public boolean areAllItemsEnabled() {
             return false;
@@ -2684,7 +2844,8 @@
                 return 0;
             }
 
-            if (mChooserListAdapter == null || mChooserListAdapter.getCount() == 0) {
+            if (mHideContentPreview || mChooserListAdapter == null
+                    || mChooserListAdapter.getCount() == 0) {
                 return 0;
             }
 
@@ -2704,7 +2865,7 @@
         // There can be at most one row in the listview, that is internally
         // a ViewGroup with 2 rows
         public int getServiceTargetRowCount() {
-            if (isSendAction(getTargetIntent())) {
+            if (isSendAction(getTargetIntent()) && !ActivityManager.isLowRamDeviceStatic()) {
                 return 1;
             }
             return 0;
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 538c81d..0a01beb 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -383,9 +383,11 @@
                 mSystemWindowInsets.right, 0);
 
         View emptyView = findViewById(R.id.empty);
-        emptyView.setPadding(0, 0, 0, mSystemWindowInsets.bottom
-                + getResources().getDimensionPixelSize(
-                        R.dimen.chooser_edge_margin_normal) * 2);
+        if (emptyView != null) {
+            emptyView.setPadding(0, 0, 0, mSystemWindowInsets.bottom
+                    + getResources().getDimensionPixelSize(
+                            R.dimen.chooser_edge_margin_normal) * 2);
+        }
 
         if (mFooterSpacer == null) {
             mFooterSpacer = new Space(getApplicationContext());
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
index 14fe6ab..1b0a458 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
@@ -113,5 +113,37 @@
      */
     public static final String PROPERTY_PERMISSIONS_HUB_ENABLED = "permissions_hub_enabled";
 
+    // Flags related to Assistant Handles
+
+    /**
+     * (String) Which behavior mode for the Assistant Handles to use.
+     */
+    public static final String ASSIST_HANDLES_BEHAVIOR_MODE = "assist_handles_behavior_mode";
+
+    /**
+     * (long) How long, in milliseconds, to display Assist Handles when showing them temporarily.
+     */
+    public static final String ASSIST_HANDLES_SHOW_AND_GO_DURATION_MS =
+            "assist_handles_show_and_go_duration_ms";
+
+    /**
+     * (long) How long, in milliseconds, to wait before displaying Assist Handles temporarily after
+     * hiding them.
+     */
+    public static final String ASSIST_HANDLES_SHOWN_FREQUENCY_THRESHOLD_MS =
+            "assist_handles_shown_frequency_threshold_ms";
+
+    /**
+     * (long) How long, in milliseconds, for teaching behaviors to wait before considering the user
+     * taught.
+     */
+    public static final String ASSIST_HANDLES_LEARN_TIME_MS = "assist_handles_learn_time_ms";
+
+    /**
+     * (int) How many times for teaching behaviors to see the user perform an action to consider it
+     * taught.
+     */
+    public static final String ASSIST_HANDLES_LEARN_COUNT = "assist_handles_learn_count";
+
     private SystemUiDeviceConfigFlags() { }
 }
diff --git a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperationsRegistry.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperationsRegistry.java
index 1436aed..049f952 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperationsRegistry.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperationsRegistry.java
@@ -132,4 +132,21 @@
             }
         }
     }
+
+    /**
+     * Check the given IME token registration status.
+     *
+     * @param token IME token
+     * @return {@code true} when the IME token has already registered
+     *         {@link InputMethodPrivilegedOperations}, {@code false} otherwise.
+     */
+    @AnyThread
+    public static boolean isRegistered(IBinder token) {
+        synchronized (sLock) {
+            if (sRegistry == null) {
+                return false;
+            }
+            return sRegistry.containsKey(token);
+        }
+    }
 }
diff --git a/core/java/com/android/internal/net/VpnInfo.java b/core/java/com/android/internal/net/VpnInfo.java
index b1a41287..e74af5e 100644
--- a/core/java/com/android/internal/net/VpnInfo.java
+++ b/core/java/com/android/internal/net/VpnInfo.java
@@ -19,6 +19,8 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import java.util.Arrays;
+
 /**
  * A lightweight container used to carry information of the ongoing VPN.
  * Internal use only..
@@ -28,14 +30,14 @@
 public class VpnInfo implements Parcelable {
     public int ownerUid;
     public String vpnIface;
-    public String primaryUnderlyingIface;
+    public String[] underlyingIfaces;
 
     @Override
     public String toString() {
         return "VpnInfo{"
                 + "ownerUid=" + ownerUid
                 + ", vpnIface='" + vpnIface + '\''
-                + ", primaryUnderlyingIface='" + primaryUnderlyingIface + '\''
+                + ", underlyingIfaces='" + Arrays.toString(underlyingIfaces) + '\''
                 + '}';
     }
 
@@ -48,7 +50,7 @@
     public void writeToParcel(Parcel dest, int flags) {
         dest.writeInt(ownerUid);
         dest.writeString(vpnIface);
-        dest.writeString(primaryUnderlyingIface);
+        dest.writeStringArray(underlyingIfaces);
     }
 
     public static final Parcelable.Creator<VpnInfo> CREATOR = new Parcelable.Creator<VpnInfo>() {
@@ -57,7 +59,7 @@
             VpnInfo info = new VpnInfo();
             info.ownerUid = source.readInt();
             info.vpnIface = source.readString();
-            info.primaryUnderlyingIface = source.readString();
+            info.underlyingIfaces = source.readStringArray();
             return info;
         }
 
diff --git a/core/jni/android_media_AudioTrack.cpp b/core/jni/android_media_AudioTrack.cpp
index f9f28da..daa6347 100644
--- a/core/jni/android_media_AudioTrack.cpp
+++ b/core/jni/android_media_AudioTrack.cpp
@@ -1301,18 +1301,6 @@
     lpTrack->setParameters(param.toString());
 }
 
-static void android_media_AudioTrack_set_eos(JNIEnv *env,  jobject thiz) {
-    sp<AudioTrack> lpTrack = getAudioTrack(env, thiz);
-    if (lpTrack == NULL) {
-        jniThrowException(env, "java/lang/IllegalStateException",
-                          "AudioTrack not initialized");
-        return;
-    }
-    AudioParameter param = AudioParameter();
-    param.addInt(String8("EOS"), 1);
-    lpTrack->setParameters(param.toString());
-}
-
 // ----------------------------------------------------------------------------
 // ----------------------------------------------------------------------------
 static const JNINativeMethod gMethods[] = {
@@ -1389,7 +1377,6 @@
     {"native_setPresentation", "(II)I", (void *)android_media_AudioTrack_setPresentation},
     {"native_getPortId", "()I", (void *)android_media_AudioTrack_get_port_id},
     {"native_set_delay_padding", "(II)V", (void *)android_media_AudioTrack_set_delay_padding},
-    {"native_set_eos",        "()V",    (void *)android_media_AudioTrack_set_eos},
 };
 
 
diff --git a/core/jni/android_net_NetUtils.cpp b/core/jni/android_net_NetUtils.cpp
index 00e0e3a..08aa1d9 100644
--- a/core/jni/android_net_NetUtils.cpp
+++ b/core/jni/android_net_NetUtils.cpp
@@ -304,13 +304,19 @@
     jniSetFileDescriptorOfFD(env, javaFd, -1);
 }
 
-static jint android_net_utils_getDnsNetId(JNIEnv *env, jobject thiz) {
-    int dnsNetId = getNetworkForDns();
-    if (dnsNetId < 0) {
-        throwErrnoException(env, "getDnsNetId", -dnsNetId);
+static jobject android_net_utils_getDnsNetwork(JNIEnv *env, jobject thiz) {
+    unsigned dnsNetId = 0;
+    if (int res = getNetworkForDns(&dnsNetId) < 0) {
+        throwErrnoException(env, "getDnsNetId", -res);
+        return nullptr;
     }
+    bool privateDnsBypass = dnsNetId & NETID_USE_LOCAL_NAMESERVERS;
 
-    return dnsNetId;
+    static jclass class_Network = MakeGlobalRefOrDie(
+            env, FindClassOrDie(env, "android/net/Network"));
+    static jmethodID ctor = env->GetMethodID(class_Network, "<init>", "(IZ)V");
+    return env->NewObject(
+            class_Network, ctor, dnsNetId & ~NETID_USE_LOCAL_NAMESERVERS, privateDnsBypass);
 }
 
 static jobject android_net_utils_getTcpRepairWindow(JNIEnv *env, jobject thiz, jobject javaFd) {
@@ -369,7 +375,7 @@
     { "resNetworkQuery", "(ILjava/lang/String;III)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkQuery },
     { "resNetworkResult", "(Ljava/io/FileDescriptor;)Landroid/net/DnsResolver$DnsResponse;", (void*) android_net_utils_resNetworkResult },
     { "resNetworkCancel", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_resNetworkCancel },
-    { "getDnsNetId", "()I", (void*) android_net_utils_getDnsNetId },
+    { "getDnsNetwork", "()Landroid/net/Network;", (void*) android_net_utils_getDnsNetwork },
 };
 
 int register_android_net_NetworkUtils(JNIEnv* env)
diff --git a/core/jni/android_os_Debug.cpp b/core/jni/android_os_Debug.cpp
index 69a7c4d..a67cb34 100644
--- a/core/jni/android_os_Debug.cpp
+++ b/core/jni/android_os_Debug.cpp
@@ -258,7 +258,7 @@
         } else if (base::StartsWith(name, "[anon:libc_malloc]")) {
             which_heap = HEAP_NATIVE;
         } else if (base::StartsWith(name, "[stack")) {
-            which_heap = HEAP_NATIVE;
+            which_heap = HEAP_STACK;
         } else if (base::EndsWith(name, ".so")) {
             which_heap = HEAP_SO;
             is_swappable = true;
diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto
index 3f8ddff..2873379 100644
--- a/core/proto/android/server/jobscheduler.proto
+++ b/core/proto/android/server/jobscheduler.proto
@@ -273,9 +273,11 @@
         // The maximum number of jobs an app can run within this particular standby bucket's
         // window size.
         optional int32 max_job_count_rare = 11;
+        // The period of time used to rate limit recently run jobs.
+        optional int32 rate_limiting_window_ms = 19;
         // The maximum number of jobs that should be allowed to run in the past
-        // {@link QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS}.
-        optional int32 max_job_count_per_allowed_time = 12;
+        // rate_limiting_window_ms.
+        optional int32 max_job_count_per_rate_limiting_window = 12;
         // The maximum number of timing sessions an app can run within this particular standby
         // bucket's window size.
         optional int32 max_session_count_active = 13;
@@ -289,8 +291,8 @@
         // bucket's window size.
         optional int32 max_session_count_rare = 16;
         // The maximum number of timing sessions that should be allowed to run in the past
-        // {@link QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS}.
-        optional int32 max_session_count_per_allowed_time = 17;
+        // rate_limiting_window_ms.
+        optional int32 max_session_count_per_rate_limiting_window = 17;
         // Treat two distinct {@link TimingSession}s as the same if they start and end within this
         // amount of time of each other.
         optional int64 timing_session_coalescing_duration_ms = 18;
@@ -517,63 +519,56 @@
             optional int64 expiration_time_elapsed = 2;
             optional int64 window_size_ms = 3;
 
-            /** The total amount of time the app ran in its respective bucket window size. */
+            optional int32 job_count_limit = 14;
+            optional int32 session_count_limit = 15;
+
+            // The total amount of time the app ran in its respective bucket window size.
             optional int64 execution_time_in_window_ms = 4;
             optional int32 bg_job_count_in_window = 5;
 
-            /**
-             * The total amount of time the app ran in the last
-             * {@link QuotaController#MAX_PERIOD_MS}.
-             */
+            // The total amount of time the app ran in the last
+            // {@link QuotaController#MAX_PERIOD_MS}.
             optional int64 execution_time_in_max_period_ms = 6;
             optional int32 bg_job_count_in_max_period = 7;
 
-            /**
-             * The number of {@link TimingSession}s within the bucket window size. This will include
-             * sessions that started before the window as long as they end within the window.
-             */
+            // The number of {@link TimingSession}s within the bucket window size. This will include
+            // sessions that started before the window as long as they end within the window.
             optional int32 session_count_in_window = 11;
 
-            /**
-             * The time after which the sum of all the app's sessions plus
-             * ConstantsProto.QuotaController.in_quota_buffer_ms equals the quota. This is only
-             * valid if
-             * execution_time_in_window_ms >=
-             *   ConstantsProto.QuotaController.allowed_time_per_period_ms
-             * or
-             * execution_time_in_max_period_ms >=
-             *   ConstantsProto.QuotaController.max_execution_time_ms.
-             */
-            optional int64 quota_cutoff_time_elapsed = 8;
+            // The time after which the app will be under the bucket quota. This is only valid if
+            // execution_time_in_window_ms >=
+            //   ConstantsProto.QuotaController.allowed_time_per_period_ms
+            // or
+            // execution_time_in_max_period_ms >=
+            //   ConstantsProto.QuotaController.max_execution_time_ms
+            // or
+            // bg_job_count_in_window >= job_count_limit
+            // or
+            // session_count_in_window >= session_count_limit.
+            optional int64 in_quota_time_elapsed = 8;
 
-            /**
-             * The time after which job_count_in_allowed_time should be considered invalid, in the
-             * elapsed realtime timebase.
-             */
+            // The time after which job_count_in_rate_limiting_window should be considered invalid,
+            // in the elapsed realtime timebase.
             optional int64 job_count_expiration_time_elapsed = 9;
 
-            /**
-             * The number of jobs that ran in at least the last
-             * ConstantsProto.QuotaController.allowed_time_per_period_ms.
-             * It may contain a few stale entries since cleanup won't happen exactly every
-             * ConstantsProto.QuotaController.allowed_time_per_period_ms.
-             */
-            optional int32 job_count_in_allowed_time = 10;
+            // The number of jobs that ran in at least the last
+            // ConstantsProto.QuotaController.rate_limiting_window_ms.
+            // It may contain a few stale entries since cleanup won't happen exactly every
+            // ConstantsProto.QuotaController.rate_limiting_window_ms. This should only be
+            // considered valid before elapsed realtime has reached
+            // job_count_expiration_time_elapsed.
+            optional int32 job_count_in_rate_limiting_window = 10;
 
-            /**
-             * The time after which {@link #timingSessionCountInAllowedTime} should be considered
-             * invalid, in the elapsed realtime timebase.
-             */
+            // The time after which {@link #timingSessionCountInAllowedTime} should be considered
+            // invalid, in the elapsed realtime timebase.
             optional int64 session_count_expiration_time_elapsed = 12;
 
-            /**
-             * The number of {@link TimingSession}s that ran in at least the last
-             * {@link #mAllowedTimePerPeriodMs}. It may contain a few stale entries since cleanup won't
-             * happen exactly every {@link #mAllowedTimePerPeriodMs}. This should only be considered
-             * valid before elapsed realtime has reached
-             * {@link #timingSessionCountExpirationTimeElapsed}.
-             */
-            optional int32 session_count_in_allowed_time = 13;
+            // The number of {@link TimingSession}s that ran in at least the last
+            // ConstantsProto.QuotaController.rate_limiting_window_ms. It may contain a few stale
+            // entries since cleanup won't happen exactly every
+            // ConstantsProto.QuotaController.rate_limiting_window_ms. This should only be considered
+            // valid before elapsed realtime has reached session_count_expiration_time_elapsed.
+            optional int32 session_count_in_rate_limiting_window = 13;
         }
 
         message Package {
diff --git a/core/proto/android/stats/location/location_enums.proto b/core/proto/android/stats/location/location_enums.proto
new file mode 100644
index 0000000..553c01c
--- /dev/null
+++ b/core/proto/android/stats/location/location_enums.proto
@@ -0,0 +1,122 @@
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+
+package android.stats.location;
+option java_outer_classname = "LocationStatsEnums";
+
+
+// APIs from LocationManagerService
+enum LocationManagerServiceApi {
+    API_UNKNOWN = 0;
+    API_REQUEST_LOCATION_UPDATES = 1;
+    API_ADD_GNSS_MEASUREMENTS_LISTENER = 2;
+    API_REGISTER_GNSS_STATUS_CALLBACK = 3;
+    API_REQUEST_GEOFENCE = 4;
+    API_SEND_EXTRA_COMMAND = 5;
+}
+
+enum UsageState {
+    USAGE_STARTED = 0;
+    USAGE_ENDED = 1;
+}
+
+// Type of location providers
+enum ProviderType {
+    PROVIDER_UNKNOWN = 0;
+    PROVIDER_NETWORK = 1;
+    PROVIDER_GPS = 2;
+    PROVIDER_PASSIVE = 3;
+    PROVIDER_FUSED = 4;
+}
+
+// Type of Callback passed in for this API
+enum CallbackType {
+    CALLBACK_UNKNOWN = 0;
+    // Current API does not need a callback, e.g. sendExtraCommand
+    CALLBACK_NOT_APPLICABLE = 1;
+    CALLBACK_LISTENER = 2;
+    CALLBACK_PENDING_INTENT = 3;
+}
+
+// Possible values for mQuality field in
+// frameworks/base/location/java/android/location/LocationRequest.java
+enum LocationRequestQuality {
+    QUALITY_UNKNOWN = 0;
+    ACCURACY_FINE = 100;
+    ACCURACY_BLOCK = 102;
+    ACCURACY_CITY = 104;
+    POWER_NONE = 200;
+    POWER_LOW = 201;
+    POWER_HIGH = 203;
+}
+
+// Bucketized values for interval field in
+// frameworks/base/location/java/android/location/LocationRequest.java
+enum LocationRequestIntervalBucket {
+    INTERVAL_UNKNOWN = 0;
+    INTERVAL_BETWEEN_0_SEC_AND_1_SEC = 1;
+    INTERVAL_BETWEEN_1_SEC_AND_5_SEC = 2;
+    INTERVAL_BETWEEN_5_SEC_AND_1_MIN = 3;
+    INTERVAL_BETWEEN_1_MIN_AND_10_MIN = 4;
+    INTERVAL_BETWEEN_10_MIN_AND_1_HOUR = 5;
+    INTERVAL_LARGER_THAN_1_HOUR = 6;
+}
+
+// Bucketized values for small displacement field in
+// frameworks/base/location/java/android/location/LocationRequest.java
+// Value in meters.
+enum SmallestDisplacementBucket {
+    DISTANCE_UNKNOWN = 0;
+    DISTANCE_ZERO = 1;
+    DISTANCE_BETWEEN_0_AND_100 = 2;
+    DISTANCE_LARGER_THAN_100 = 3;
+}
+
+// Bucketized values for expire_in field in
+// frameworks/base/location/java/android/location/LocationRequest.java
+enum ExpirationBucket {
+    EXPIRATION_UNKNOWN = 0;
+    EXPIRATION_BETWEEN_0_AND_20_SEC = 1;
+    EXPIRATION_BETWEEN_20_SEC_AND_1_MIN = 2;
+    EXPIRATION_BETWEEN_1_MIN_AND_10_MIN = 3;
+    EXPIRATION_BETWEEN_10_MIN_AND_1_HOUR = 4;
+    EXPIRATION_LARGER_THAN_1_HOUR = 5;
+    EXPIRATION_NO_EXPIRY = 6;
+}
+
+// Bucketized values for radius field in
+// frameworks/base/location/java/android/location/Geofence.java
+// Value in meters.
+enum GeofenceRadiusBucket {
+    RADIUS_UNKNOWN = 0;
+    RADIUS_BETWEEN_0_AND_100 = 1;
+    RADIUS_BETWEEN_100_AND_200 = 2;
+    RADIUS_BETWEEN_200_AND_300 = 3;
+    RADIUS_BETWEEN_300_AND_1000 = 4;
+    RADIUS_BETWEEN_1000_AND_10000 = 5;
+    RADIUS_LARGER_THAN_100000 = 6;
+    RADIUS_NEGATIVE = 7;
+}
+
+// Caller Activity Importance.
+enum ActivityImportance {
+    IMPORTANCE_UNKNOWN = 0;
+    IMPORTANCE_TOP = 1;
+    IMPORTANCE_FORGROUND_SERVICE = 2;
+    IMPORTANCE_BACKGROUND = 3;
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index ab7aed3..890ad5e 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2337,7 +2337,7 @@
          The app can check whether it has this authorization by calling
          {@link android.provider.Settings#canDrawOverlays
          Settings.canDrawOverlays()}.
-         <p>Protection level: signature -->
+         <p>Protection level: signature|preinstalled|appop|pre23|development -->
     <permission android:name="android.permission.SYSTEM_ALERT_WINDOW"
         android:label="@string/permlab_systemAlertWindow"
         android:description="@string/permdesc_systemAlertWindow"
@@ -2522,7 +2522,7 @@
         can check whether it has this authorization by calling {@link
         android.provider.Settings.System#canWrite Settings.System.canWrite()}.
 
-        <p>Protection level: signature
+        <p>Protection level: signature|preinstalled|appop|pre23
     -->
     <permission android:name="android.permission.WRITE_SETTINGS"
         android:label="@string/permlab_writeSettings"
diff --git a/core/res/res/layout/chooser_grid_preview_file.xml b/core/res/res/layout/chooser_grid_preview_file.xml
index 27c6041..f7d60c9 100644
--- a/core/res/res/layout/chooser_grid_preview_file.xml
+++ b/core/res/res/layout/chooser_grid_preview_file.xml
@@ -44,8 +44,7 @@
           android:adjustViewBounds="true"
           android:layout_gravity="center_vertical"
           android:gravity="center"
-          android:scaleType="centerCrop"
-          android:visibility="gone"/>
+          android:scaleType="centerCrop"/>
     <ImageView
         android:id="@+id/content_preview_file_icon"
         android:layout_width="36dp"
diff --git a/core/res/res/layout/chooser_grid_preview_image.xml b/core/res/res/layout/chooser_grid_preview_image.xml
index ad31e0d..79a0de4 100644
--- a/core/res/res/layout/chooser_grid_preview_image.xml
+++ b/core/res/res/layout/chooser_grid_preview_image.xml
@@ -33,7 +33,6 @@
 
     <view class="com.android.internal.app.ChooserActivity$RoundedRectImageView"
           android:id="@+id/content_preview_image_1_large"
-          android:visibility="gone"
           android:layout_width="120dp"
           android:layout_height="140dp"
           android:layout_alignParentTop="true"
diff --git a/packages/SystemUI/res/values-mcc310-mnc030/config.xml b/core/res/res/values-mcc310-mnc030/config.xml
similarity index 100%
rename from packages/SystemUI/res/values-mcc310-mnc030/config.xml
rename to core/res/res/values-mcc310-mnc030/config.xml
diff --git a/packages/SystemUI/res/values-mcc310-mnc070/config.xml b/core/res/res/values-mcc310-mnc070/config.xml
similarity index 100%
rename from packages/SystemUI/res/values-mcc310-mnc070/config.xml
rename to core/res/res/values-mcc310-mnc070/config.xml
diff --git a/packages/SystemUI/res/values-mcc310-mnc170/config.xml b/core/res/res/values-mcc310-mnc170/config.xml
similarity index 100%
rename from packages/SystemUI/res/values-mcc310-mnc170/config.xml
rename to core/res/res/values-mcc310-mnc170/config.xml
diff --git a/packages/SystemUI/res/values-mcc310-mnc280/config.xml b/core/res/res/values-mcc310-mnc280/config.xml
similarity index 100%
rename from packages/SystemUI/res/values-mcc310-mnc280/config.xml
rename to core/res/res/values-mcc310-mnc280/config.xml
diff --git a/packages/SystemUI/res/values-mcc310-mnc380/config.xml b/core/res/res/values-mcc310-mnc380/config.xml
similarity index 100%
rename from packages/SystemUI/res/values-mcc310-mnc380/config.xml
rename to core/res/res/values-mcc310-mnc380/config.xml
diff --git a/core/res/res/values-mcc310-mnc410/config.xml b/core/res/res/values-mcc310-mnc410/config.xml
index 00ab712..3fb3f0f 100644
--- a/core/res/res/values-mcc310-mnc410/config.xml
+++ b/core/res/res/values-mcc310-mnc410/config.xml
@@ -48,4 +48,8 @@
         <item>"#8"</item>
         <item>"#9"</item>
     </string-array>
+
+    <!-- Enable 5 bar signal strength icon -->
+    <bool name="config_inflateSignalStrength">true</bool>
+
 </resources>
diff --git a/packages/SystemUI/res/values-mcc310-mnc560/config.xml b/core/res/res/values-mcc310-mnc560/config.xml
similarity index 100%
rename from packages/SystemUI/res/values-mcc310-mnc560/config.xml
rename to core/res/res/values-mcc310-mnc560/config.xml
diff --git a/packages/SystemUI/res/values-mcc310-mnc950/config.xml b/core/res/res/values-mcc310-mnc950/config.xml
similarity index 100%
rename from packages/SystemUI/res/values-mcc310-mnc950/config.xml
rename to core/res/res/values-mcc310-mnc950/config.xml
diff --git a/packages/SystemUI/res/values-mcc311-mnc180/config.xml b/core/res/res/values-mcc311-mnc180/config.xml
similarity index 100%
rename from packages/SystemUI/res/values-mcc311-mnc180/config.xml
rename to core/res/res/values-mcc311-mnc180/config.xml
diff --git a/core/res/res/values-mcc311-mnc480/config.xml b/core/res/res/values-mcc311-mnc480/config.xml
index db2f8d0..336e30e 100755
--- a/core/res/res/values-mcc311-mnc480/config.xml
+++ b/core/res/res/values-mcc311-mnc480/config.xml
@@ -40,4 +40,7 @@
 
     <bool name="config_use_sim_language_file">true</bool>
 
+    <!-- Enable 5 bar signal strength icon -->
+    <bool name="config_inflateSignalStrength">true</bool>
+
 </resources>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 2de5397..9bd56ad 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1276,9 +1276,14 @@
          meanings. -->
     <integer name="config_defaultRingVibrationIntensity">2</integer>
 
+    <!-- Whether to use the strict phone number matcher by default. -->
     <bool name="config_use_strict_phone_number_comparation">false</bool>
 
-    <bool name="config_use_strict_phone_number_comparation_for_russian">true</bool>
+    <!-- Whether to use the strict phone number matcher in Russia. -->
+    <bool name="config_use_strict_phone_number_comparation_for_russia">true</bool>
+
+    <!-- Whether to use the strict phone number matcher in Kazakhstan. -->
+    <bool name="config_use_strict_phone_number_comparation_for_kazakhstan">true</bool>
 
     <!-- Display low battery warning when battery level dips to this value.
          Also, the battery stats are flushed to disk when we hit this level.  -->
@@ -3306,6 +3311,10 @@
          (which normally prevents seamless rotation). -->
     <bool name="config_allowSeamlessRotationDespiteNavBarMoving">false</bool>
 
+    <!-- Controls whether hints for gestural navigation are shown when the device is setup.
+         This should only be set when the device has gestural navigation enabled by default. -->
+    <bool name="config_showGesturalNavigationHints">false</bool>
+
     <!-- Default insets [LEFT/RIGHTxTOP/BOTTOM] from the screen edge for picture-in-picture windows.
          These values are in DPs and will be converted to pixel sizes internally. -->
     <string translatable="false" name="config_defaultPictureInPictureScreenEdgeInsets">16x16</string>
@@ -3893,6 +3902,10 @@
          {@see android.view.Display#FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS} -->
     <string name="config_secondaryHomeComponent" translatable="false">com.android.launcher3/com.android.launcher3.SecondaryDisplayLauncher</string>
 
+    <!-- Force secondary home launcher specified in config_secondaryHomeComponent always. If this is
+         not set, secondary home launcher can be replaced by user. -->
+    <bool name ="config_useSystemProvidedLauncherForSecondary">false</bool>
+
     <!-- If device supports corner radius on windows.
          This should be turned off on low-end devices to improve animation performance. -->
     <bool name="config_supportsRoundedCornersOnWindows">true</bool>
@@ -4121,4 +4134,9 @@
     even after user setup is complete. The defined component should be used for supervision purposes
     only. The component must be part of a system app. -->
     <string name="config_defaultSupervisionProfileOwnerComponent" translatable="false"></string>
+
+    <!-- Whether to artificially interpret all signal strengths as
+         one bar higher than they actually are -->
+    <bool name="config_inflateSignalStrength">false</bool>
+
 </resources>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index e0ab6c8..5363ef92 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -59,9 +59,7 @@
     <!-- Height of the bottom navigation bar frame in landscape -->
     <dimen name="navigation_bar_frame_height_landscape">@dimen/navigation_bar_frame_height</dimen>
 
-    <!-- The height of the navigation gesture area; if the size is larger than the navigation bar
-        frame width/height, then the difference is the spacing from the navigation bar window to
-        the area that detects gestures. -->
+    <!-- The height of the navigation gesture area if the gesture is starting from the bottom. -->
     <dimen name="navigation_bar_gesture_height">@dimen/navigation_bar_frame_height</dimen>
 
     <!-- Height of the bottom navigation / system bar in car mode. -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 6cdb3d6..3b3a062 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -313,7 +313,8 @@
   <java-symbol type="bool" name="config_ui_enableFadingMarquee" />
   <java-symbol type="bool" name="config_enableHapticTextHandle" />
   <java-symbol type="bool" name="config_use_strict_phone_number_comparation" />
-  <java-symbol type="bool" name="config_use_strict_phone_number_comparation_for_russian" />
+  <java-symbol type="bool" name="config_use_strict_phone_number_comparation_for_russia" />
+  <java-symbol type="bool" name="config_use_strict_phone_number_comparation_for_kazakhstan" />
   <java-symbol type="bool" name="config_single_volume" />
   <java-symbol type="bool" name="config_voice_capable" />
   <java-symbol type="bool" name="config_requireCallCapableAccountForHandle" />
@@ -2880,6 +2881,7 @@
   <java-symbol type="bool" name="config_allowSeamlessRotationDespiteNavBarMoving" />
   <java-symbol type="dimen" name="config_backGestureInset" />
   <java-symbol type="color" name="system_bar_background_semi_transparent" />
+  <java-symbol type="bool" name="config_showGesturalNavigationHints" />
 
   <!-- EditText suggestion popup. -->
   <java-symbol type="id" name="suggestionWindowContainer" />
@@ -3697,6 +3699,7 @@
 
   <!-- For Secondary Launcher -->
   <java-symbol type="string" name="config_secondaryHomeComponent" />
+  <java-symbol type="bool" name="config_useSystemProvidedLauncherForSecondary" />
 
   <java-symbol type="string" name="battery_saver_notification_channel_name" />
   <java-symbol type="string" name="battery_saver_sticky_disabled_notification_title" />
@@ -3797,4 +3800,5 @@
   <java-symbol type="style" name="Animation.DeviceDefault.Activity.Resolver" />
   
   <java-symbol type="string" name="config_defaultSupervisionProfileOwnerComponent" />
+  <java-symbol type="bool" name="config_inflateSignalStrength" />
 </resources>
diff --git a/core/tests/benchmarks/src/android/net/NetworkStatsBenchmark.java b/core/tests/benchmarks/src/android/net/NetworkStatsBenchmark.java
index 1b65603..707d7b3 100644
--- a/core/tests/benchmarks/src/android/net/NetworkStatsBenchmark.java
+++ b/core/tests/benchmarks/src/android/net/NetworkStatsBenchmark.java
@@ -19,13 +19,22 @@
 import com.google.caliper.BeforeExperiment;
 import com.google.caliper.Param;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
 public class NetworkStatsBenchmark {
-    private static final String UNDERLYING_IFACE = "wlan0";
+    private static final String[] UNDERLYING_IFACES = {"wlan0", "rmnet0"};
     private static final String TUN_IFACE = "tun0";
     private static final int TUN_UID = 999999999;
 
     @Param({"100", "1000"})
     private int mSize;
+    /**
+     * Should not be more than the length of {@link #UNDERLYING_IFACES}.
+     */
+    @Param({"1", "2"})
+    private int mNumUnderlyingIfaces;
     private NetworkStats mNetworkStats;
 
     @BeforeExperiment
@@ -33,8 +42,10 @@
         mNetworkStats = new NetworkStats(0, mSize + 2);
         int uid = 0;
         NetworkStats.Entry recycle = new NetworkStats.Entry();
+        final List<String> allIfaces = getAllIfacesForBenchmark(); // also contains TUN_IFACE.
+        final int totalIfaces = allIfaces.size();
         for (int i = 0; i < mSize; i++) {
-            recycle.iface = (i < mSize / 2) ? TUN_IFACE : UNDERLYING_IFACE;
+            recycle.iface = allIfaces.get(i % totalIfaces);
             recycle.uid = uid;
             recycle.set = i % 2;
             recycle.tag = NetworkStats.TAG_NONE;
@@ -48,22 +59,39 @@
                 uid++;
             }
         }
-        recycle.iface = UNDERLYING_IFACE;
-        recycle.uid = TUN_UID;
-        recycle.set = NetworkStats.SET_FOREGROUND;
-        recycle.tag = NetworkStats.TAG_NONE;
-        recycle.rxBytes = 90000 * mSize;
-        recycle.rxPackets = 40 * mSize;
-        recycle.txBytes = 180000 * mSize;
-        recycle.txPackets = 1200 * mSize;
-        recycle.operations = 0;
-        mNetworkStats.addValues(recycle);
+
+        for (int i = 0; i < mNumUnderlyingIfaces; i++) {
+            recycle.iface = UNDERLYING_IFACES[i];
+            recycle.uid = TUN_UID;
+            recycle.set = NetworkStats.SET_FOREGROUND;
+            recycle.tag = NetworkStats.TAG_NONE;
+            recycle.rxBytes = 90000 * mSize;
+            recycle.rxPackets = 40 * mSize;
+            recycle.txBytes = 180000 * mSize;
+            recycle.txPackets = 1200 * mSize;
+            recycle.operations = 0;
+            mNetworkStats.addValues(recycle);
+        }
+    }
+
+    private String[] getVpnUnderlyingIfaces() {
+        return Arrays.copyOf(UNDERLYING_IFACES, mNumUnderlyingIfaces);
+    }
+
+    /**
+     * Same as {@link #getVpnUnderlyingIfaces}, but also contains {@link #TUN_IFACE}.
+     */
+    private List<String> getAllIfacesForBenchmark() {
+        List<String> ifaces = new ArrayList<>();
+        ifaces.add(TUN_IFACE);
+        ifaces.addAll(Arrays.asList(getVpnUnderlyingIfaces()));
+        return ifaces;
     }
 
     public void timeMigrateTun(int reps) {
         for (int i = 0; i < reps; i++) {
             NetworkStats stats = mNetworkStats.clone();
-            stats.migrateTun(TUN_UID, TUN_IFACE, UNDERLYING_IFACE);
+            stats.migrateTun(TUN_UID, TUN_IFACE, getVpnUnderlyingIfaces());
         }
     }
 
diff --git a/data/etc/car/Android.bp b/data/etc/car/Android.bp
index 37020fc..9272ea5 100644
--- a/data/etc/car/Android.bp
+++ b/data/etc/car/Android.bp
@@ -31,6 +31,13 @@
 }
 
 prebuilt_etc {
+    name: "privapp_whitelist_android.car.cluster",
+    sub_dir: "permissions",
+    src: "android.car.cluster.xml",
+    filename_from_src: true,
+}
+
+prebuilt_etc {
     name: "privapp_whitelist_android.car.usb.handler",
     sub_dir: "permissions",
     src: "android.car.usb.handler.xml",
diff --git a/data/etc/car/android.car.cluster.xml b/data/etc/car/android.car.cluster.xml
new file mode 100644
index 0000000..d7f29da
--- /dev/null
+++ b/data/etc/car/android.car.cluster.xml
@@ -0,0 +1,24 @@
+<?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
+  -->
+<permissions>
+    <privapp-permissions package="android.car.cluster">
+        <permission name="android.permission.CONTROL_INCALL_EXPERIENCE"/>
+        <permission name="android.permission.INTERACT_ACROSS_USERS"/>
+        <permission name="android.permission.MANAGE_USERS"/>
+        <permission name="android.permission.WRITE_SECURE_SETTINGS"/>
+    </privapp-permissions>
+</permissions>
diff --git a/data/etc/car/com.android.car.xml b/data/etc/car/com.android.car.xml
index f1797de..19548bc 100644
--- a/data/etc/car/com.android.car.xml
+++ b/data/etc/car/com.android.car.xml
@@ -24,6 +24,7 @@
         <permission name="android.permission.PROVIDE_TRUST_AGENT"/>
         <permission name="android.permission.REAL_GET_TASKS"/>
         <permission name="android.permission.REBOOT"/>
+        <permission name="android.permission.READ_LOGS"/>
         <permission name="android.permission.WRITE_SECURE_SETTINGS"/>
     </privapp-permissions>
 </permissions>
diff --git a/data/etc/car/com.google.android.car.kitchensink.xml b/data/etc/car/com.google.android.car.kitchensink.xml
index 6b26e8f..d36a826 100644
--- a/data/etc/car/com.google.android.car.kitchensink.xml
+++ b/data/etc/car/com.google.android.car.kitchensink.xml
@@ -25,6 +25,7 @@
         <permission name="android.permission.MODIFY_PHONE_STATE"/>
         <permission name="android.permission.PROVIDE_TRUST_AGENT"/>
         <permission name="android.permission.REAL_GET_TASKS"/>
+        <permission name="android.permission.READ_LOGS"/>
         <permission name="android.permission.REBOOT"/>
         <permission name="android.permission.WRITE_SECURE_SETTINGS"/>
     </privapp-permissions>
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index b3856d5..a640122 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -196,6 +196,7 @@
         <permission name="android.permission.USE_RESERVED_DISK"/>
         <permission name="android.permission.WRITE_MEDIA_STORAGE"/>
         <permission name="android.permission.WATCH_APPOPS"/>
+        <permission name="android.permission.UPDATE_DEVICE_STATS"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.providers.telephony">
diff --git a/graphics/java/android/graphics/Outline.java b/graphics/java/android/graphics/Outline.java
index 98c990a..1fc056c 100644
--- a/graphics/java/android/graphics/Outline.java
+++ b/graphics/java/android/graphics/Outline.java
@@ -273,8 +273,12 @@
     }
 
     /**
-     * Sets the Constructs an Outline from a
+     * Sets the Outline to a
      * {@link android.graphics.Path#isConvex() convex path}.
+     *
+     * @param convexPath used to construct the Outline. As of
+     * {@link android.os.Build.VERSION_CODES#Q}, it is no longer required to be
+     * convex.
      */
     public void setConvexPath(@NonNull Path convexPath) {
         if (convexPath.isEmpty()) {
@@ -282,10 +286,6 @@
             return;
         }
 
-        if (!convexPath.isConvex()) {
-            throw new IllegalArgumentException("path must be convex");
-        }
-
         if (mPath == null) {
             mPath = new Path();
         }
diff --git a/keystore/java/android/security/keystore/KeyStoreCryptoOperationChunkedStreamer.java b/keystore/java/android/security/keystore/KeyStoreCryptoOperationChunkedStreamer.java
index dbb79bc..e030478 100644
--- a/keystore/java/android/security/keystore/KeyStoreCryptoOperationChunkedStreamer.java
+++ b/keystore/java/android/security/keystore/KeyStoreCryptoOperationChunkedStreamer.java
@@ -162,15 +162,15 @@
             }
 
             if ((opResult.output != null) && (opResult.output.length > 0)) {
-                if (inputLength > 0) {
+                if (inputLength + mBufferedLength > 0) {
                     // More output might be produced in this loop -- buffer the current output
                     if (bufferedOutput == null) {
                         bufferedOutput = new ByteArrayOutputStream();
-                        try {
-                            bufferedOutput.write(opResult.output);
-                        } catch (IOException e) {
-                            throw new ProviderException("Failed to buffer output", e);
-                        }
+                    }
+                    try {
+                        bufferedOutput.write(opResult.output);
+                    } catch (IOException e) {
+                        throw new ProviderException("Failed to buffer output", e);
                     }
                 } else {
                     // No more output will be produced in this loop
diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
index 0a28949..16c8b89 100644
--- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
+++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
@@ -161,8 +161,7 @@
 // Recording Canvas draw operations: Bitmaps
 // ----------------------------------------------------------------------------
 
-SkiaCanvas::PaintCoW&& SkiaRecordingCanvas::filterBitmap(PaintCoW&& paint,
-                                                         sk_sp<SkColorFilter> colorSpaceFilter) {
+SkiaCanvas::PaintCoW&& SkiaRecordingCanvas::filterBitmap(PaintCoW&& paint) {
     bool fixBlending = false;
     bool fixAA = false;
     if (paint) {
@@ -172,23 +171,13 @@
         fixAA = paint->isAntiAlias();
     }
 
-    if (fixBlending || fixAA || colorSpaceFilter) {
+    if (fixBlending || fixAA) {
         SkPaint& tmpPaint = paint.writeable();
 
         if (fixBlending) {
             tmpPaint.setBlendMode(SkBlendMode::kDstOut);
         }
 
-        if (colorSpaceFilter) {
-            if (tmpPaint.getColorFilter()) {
-                tmpPaint.setColorFilter(SkColorFilter::MakeComposeFilter(
-                        tmpPaint.refColorFilter(), std::move(colorSpaceFilter)));
-            } else {
-                tmpPaint.setColorFilter(std::move(colorSpaceFilter));
-            }
-            LOG_ALWAYS_FATAL_IF(!tmpPaint.getColorFilter());
-        }
-
         // disabling AA on bitmap draws matches legacy HWUI behavior
         tmpPaint.setAntiAlias(false);
     }
@@ -198,7 +187,7 @@
 
 void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, float left, float top, const SkPaint* paint) {
     sk_sp<SkImage> image = bitmap.makeImage();
-    mRecorder.drawImage(image, left, top, filterPaint(paint), bitmap.palette());
+    mRecorder.drawImage(image, left, top, filterBitmap(paint), bitmap.palette());
     // if image->unique() is true, then mRecorder.drawImage failed for some reason. It also means
     // it is not safe to store a raw SkImage pointer, because the image object will be destroyed
     // when this function ends.
@@ -212,7 +201,7 @@
     concat(matrix);
 
     sk_sp<SkImage> image = bitmap.makeImage();
-    mRecorder.drawImage(image, 0, 0, filterPaint(paint), bitmap.palette());
+    mRecorder.drawImage(image, 0, 0, filterBitmap(paint), bitmap.palette());
     if (!bitmap.isImmutable() && image.get() && !image->unique()) {
         mDisplayList->mMutableImages.push_back(image.get());
     }
@@ -225,7 +214,7 @@
     SkRect dstRect = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom);
 
     sk_sp<SkImage> image = bitmap.makeImage();
-    mRecorder.drawImageRect(image, srcRect, dstRect, filterPaint(paint),
+    mRecorder.drawImageRect(image, srcRect, dstRect, filterBitmap(paint),
                             SkCanvas::kFast_SrcRectConstraint, bitmap.palette());
     if (!bitmap.isImmutable() && image.get() && !image->unique() && !srcRect.isEmpty() &&
         !dstRect.isEmpty()) {
@@ -263,7 +252,7 @@
         filteredPaint.writeable().setFilterQuality(kLow_SkFilterQuality);
     }
     sk_sp<SkImage> image = bitmap.makeImage();
-    mRecorder.drawImageLattice(image, lattice, dst, filterPaint(std::move(filteredPaint)),
+    mRecorder.drawImageLattice(image, lattice, dst, filterBitmap(std::move(filteredPaint)),
                                bitmap.palette());
     if (!bitmap.isImmutable() && image.get() && !image->unique() && !dst.isEmpty()) {
         mDisplayList->mMutableImages.push_back(image.get());
diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
index afeccea..c42cea3 100644
--- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
+++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
@@ -90,7 +90,7 @@
      */
     void initDisplayList(uirenderer::RenderNode* renderNode, int width, int height);
 
-    PaintCoW&& filterBitmap(PaintCoW&& paint, sk_sp<SkColorFilter> colorSpaceFilter);
+    PaintCoW&& filterBitmap(PaintCoW&& paint);
 };
 
 }  // namespace skiapipeline
diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java
index dc3041f..820d82d 100644
--- a/media/java/android/media/AudioAttributes.java
+++ b/media/java/android/media/AudioAttributes.java
@@ -222,10 +222,10 @@
     public final static int SUPPRESSIBLE_MEDIA = 5;
     /**
      * @hide
-     * Denotes a usage for all other sounds not caught in SUPPRESSIBLE_NOTIFICATION,
+     * Denotes a usage for sounds not caught in SUPPRESSIBLE_NOTIFICATION,
      * SUPPRESSIBLE_CALL,SUPPRESSIBLE_NEVER, SUPPRESSIBLE_ALARM or SUPPRESSIBLE_MEDIA.
-     * This includes system, sonification and unknown sounds.
-     * These will be muted when the Zen priority mode doesn't allow sytem sounds
+     * This includes sonification sounds.
+     * These will be muted when the Zen priority mode doesn't allow system sounds
      * @see #SUPPRESSIBLE_USAGES
      */
     public final static int SUPPRESSIBLE_SYSTEM = 6;
@@ -248,6 +248,7 @@
         SUPPRESSIBLE_USAGES.put(USAGE_NOTIFICATION_EVENT,                SUPPRESSIBLE_NOTIFICATION);
         SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_ACCESSIBILITY,          SUPPRESSIBLE_NEVER);
         SUPPRESSIBLE_USAGES.put(USAGE_VOICE_COMMUNICATION,               SUPPRESSIBLE_NEVER);
+        SUPPRESSIBLE_USAGES.put(USAGE_VOICE_COMMUNICATION_SIGNALLING,    SUPPRESSIBLE_NEVER);
         SUPPRESSIBLE_USAGES.put(USAGE_ALARM,                             SUPPRESSIBLE_ALARM);
         SUPPRESSIBLE_USAGES.put(USAGE_MEDIA,                             SUPPRESSIBLE_MEDIA);
         SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,    SUPPRESSIBLE_MEDIA);
@@ -255,7 +256,6 @@
         SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANT,                         SUPPRESSIBLE_MEDIA);
         /** default volume assignment is STREAM_MUSIC, handle unknown usage as media */
         SUPPRESSIBLE_USAGES.put(USAGE_UNKNOWN,                           SUPPRESSIBLE_MEDIA);
-        SUPPRESSIBLE_USAGES.put(USAGE_VOICE_COMMUNICATION_SIGNALLING,    SUPPRESSIBLE_SYSTEM);
         SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_SONIFICATION,           SUPPRESSIBLE_SYSTEM);
     }
 
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 2541982..2d6cd24 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -4274,38 +4274,6 @@
         }
     }
 
-     /**
-     * Indicate A2DP source or sink active device change and eventually suppress
-     * the {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY} intent.
-     * This operation is asynchronous but its execution will still be sequentially scheduled
-     * relative to calls to {@link #setBluetoothHearingAidDeviceConnectionState(BluetoothDevice,
-     * int, boolean, int)} and
-     * {@link #handleBluetoothA2dpDeviceConfigChange(BluetoothDevice)}.
-     * @param device Bluetooth device connected/disconnected
-     * @param state  new connection state (BluetoothProfile.STATE_xxx)
-     * @param profile profile for the A2DP device
-     * (either {@link android.bluetooth.BluetoothProfile.A2DP} or
-     * {@link android.bluetooth.BluetoothProfile.A2DP_SINK})
-     * @param a2dpVolume New volume for the connecting device. Does nothing if
-     * disconnecting. Pass value -1 in case you want this field to be ignored
-     * @param suppressNoisyIntent if true the
-     * {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY} intent will not be sent.
-     * @return a delay in ms that the caller should wait before broadcasting
-     * BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED intent.
-     * {@hide}
-     */
-    public void handleBluetoothA2dpActiveDeviceChange(
-                BluetoothDevice device, int state, int profile,
-                boolean suppressNoisyIntent, int a2dpVolume) {
-        final IAudioService service = getService();
-        try {
-            service.handleBluetoothA2dpActiveDeviceChange(device,
-                state, profile, suppressNoisyIntent, a2dpVolume);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
     /** {@hide} */
     public IRingtonePlayer getRingtonePlayer() {
         try {
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 987db8b..fde0e64 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -990,9 +990,9 @@
     public static native int setMasterBalance(float balance);
 
     // helpers for android.media.AudioManager.getProperty(), see description there for meaning
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 112561552)
+    @UnsupportedAppUsage(trackingBug = 134049522)
     public static native int getPrimaryOutputSamplingRate();
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 112561552)
+    @UnsupportedAppUsage(trackingBug = 134049522)
     public static native int getPrimaryOutputFrameCount();
     @UnsupportedAppUsage
     public static native int getOutputLatency(int stream);
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index d9d614f..e29e569 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -101,6 +101,20 @@
     public static final int PLAYSTATE_PAUSED  = 2;  // matches SL_PLAYSTATE_PAUSED
     /** indicates AudioTrack state is playing */
     public static final int PLAYSTATE_PLAYING = 3;  // matches SL_PLAYSTATE_PLAYING
+    /**
+      * @hide
+      * indicates AudioTrack state is stopping waiting for NATIVE_EVENT_STREAM_END to
+      * transition to PLAYSTATE_STOPPED.
+      * Only valid for offload mode.
+      */
+    private static final int PLAYSTATE_STOPPING = 4;
+    /**
+      * @hide
+      * indicates AudioTrack state is paused from stopping state. Will transition to
+      * PLAYSTATE_STOPPING if play() is called.
+      * Only valid for offload mode.
+      */
+    private static final int PLAYSTATE_PAUSED_STOPPING = 5;
 
     // keep these values in sync with android_media_AudioTrack.cpp
     /**
@@ -303,6 +317,14 @@
      * One of PLAYSTATE_STOPPED, PLAYSTATE_PAUSED, or PLAYSTATE_PLAYING.
      */
     private int mPlayState = PLAYSTATE_STOPPED;
+
+    /**
+     * Indicates that we are expecting an end of stream callback following a call
+     * to setOffloadEndOfStream() in a gapless track transition context. The native track
+     * will be restarted automatically.
+     */
+    private boolean mOffloadEosPending = false;
+
     /**
      * Lock to ensure mPlayState updates reflect the actual state of the object.
      */
@@ -1073,6 +1095,10 @@
      * Declares that the last write() operation on this track provided the last buffer of this
      * stream.
      * After the end of stream, previously set padding and delay values are ignored.
+     * Can only be called only if the AudioTrack is opened in offload mode
+     * {@see Builder#setOffloadedPlayback(boolean)}.
+     * Can only be called only if the AudioTrack is in state {@link #PLAYSTATE_PLAYING}
+     * {@see #getPlaystate()}.
      * Use this method in the same thread as any write() operation.
      */
     public void setOffloadEndOfStream() {
@@ -1082,7 +1108,20 @@
         if (mState == STATE_UNINITIALIZED) {
             throw new IllegalStateException("Uninitialized track");
         }
-        native_set_eos();
+        if (mPlayState != PLAYSTATE_PLAYING) {
+            throw new IllegalStateException("EOS not supported if not playing");
+        }
+        synchronized (mStreamEventCbLock) {
+            if (mStreamEventCbInfoList.size() == 0) {
+                throw new IllegalStateException("EOS not supported without StreamEventCallback");
+            }
+        }
+
+        synchronized (mPlayStateLock) {
+            native_stop();
+            mOffloadEosPending = true;
+            mPlayState = PLAYSTATE_STOPPING;
+        }
     }
 
     /**
@@ -1366,7 +1405,11 @@
         }
         baseRelease();
         native_release();
-        mState = STATE_UNINITIALIZED;
+        synchronized (mPlayStateLock) {
+            mState = STATE_UNINITIALIZED;
+            mPlayState = PLAYSTATE_STOPPED;
+            mPlayStateLock.notify();
+        }
     }
 
     @Override
@@ -1525,7 +1568,14 @@
      */
     public int getPlayState() {
         synchronized (mPlayStateLock) {
-            return mPlayState;
+            switch (mPlayState) {
+                case PLAYSTATE_STOPPING:
+                    return PLAYSTATE_PLAYING;
+                case PLAYSTATE_PAUSED_STOPPING:
+                    return PLAYSTATE_PAUSED;
+                default:
+                    return mPlayState;
+            }
         }
     }
 
@@ -2260,7 +2310,12 @@
         synchronized(mPlayStateLock) {
             baseStart();
             native_start();
-            mPlayState = PLAYSTATE_PLAYING;
+            if (mPlayState == PLAYSTATE_PAUSED_STOPPING) {
+                mPlayState = PLAYSTATE_STOPPING;
+            } else {
+                mPlayState = PLAYSTATE_PLAYING;
+                mOffloadEosPending = false;
+            }
         }
     }
 
@@ -2282,9 +2337,15 @@
         synchronized(mPlayStateLock) {
             native_stop();
             baseStop();
-            mPlayState = PLAYSTATE_STOPPED;
-            mAvSyncHeader = null;
-            mAvSyncBytesRemaining = 0;
+            if (mOffloaded && mPlayState != PLAYSTATE_PAUSED_STOPPING) {
+                mPlayState = PLAYSTATE_STOPPING;
+            } else {
+                mPlayState = PLAYSTATE_STOPPED;
+                mOffloadEosPending = false;
+                mAvSyncHeader = null;
+                mAvSyncBytesRemaining = 0;
+                mPlayStateLock.notify();
+            }
         }
     }
 
@@ -2305,7 +2366,11 @@
         synchronized(mPlayStateLock) {
             native_pause();
             basePause();
-            mPlayState = PLAYSTATE_PAUSED;
+            if (mPlayState == PLAYSTATE_STOPPING) {
+                mPlayState = PLAYSTATE_PAUSED_STOPPING;
+            } else {
+                mPlayState = PLAYSTATE_PAUSED;
+            }
         }
     }
 
@@ -2434,6 +2499,9 @@
             return ERROR_BAD_VALUE;
         }
 
+        if (!blockUntilOffloadDrain(writeMode)) {
+            return 0;
+        }
 
         final int ret = native_write_byte(audioData, offsetInBytes, sizeInBytes, mAudioFormat,
                 writeMode == WRITE_BLOCKING);
@@ -2544,6 +2612,10 @@
             return ERROR_BAD_VALUE;
         }
 
+        if (!blockUntilOffloadDrain(writeMode)) {
+            return 0;
+        }
+
         final int ret = native_write_short(audioData, offsetInShorts, sizeInShorts, mAudioFormat,
                 writeMode == WRITE_BLOCKING);
 
@@ -2632,6 +2704,10 @@
             return ERROR_BAD_VALUE;
         }
 
+        if (!blockUntilOffloadDrain(writeMode)) {
+            return 0;
+        }
+
         final int ret = native_write_float(audioData, offsetInFloats, sizeInFloats, mAudioFormat,
                 writeMode == WRITE_BLOCKING);
 
@@ -2706,6 +2782,10 @@
             return ERROR_BAD_VALUE;
         }
 
+        if (!blockUntilOffloadDrain(writeMode)) {
+            return 0;
+        }
+
         int ret = 0;
         if (audioData.isDirect()) {
             ret = native_write_native_bytes(audioData,
@@ -2790,6 +2870,10 @@
             return ERROR_BAD_VALUE;
         }
 
+        if (!blockUntilOffloadDrain(writeMode)) {
+            return 0;
+        }
+
         // create timestamp header if none exists
         if (mAvSyncHeader == null) {
             mAvSyncHeader = ByteBuffer.allocate(mOffset);
@@ -2859,6 +2943,25 @@
         return native_reload_static();
     }
 
+    /**
+     * When an AudioTrack in offload mode is in STOPPING play state, wait until event STREAM_END is
+     * received if blocking write or return with 0 frames written if non blocking mode.
+     */
+    private boolean blockUntilOffloadDrain(int writeMode) {
+        synchronized (mPlayStateLock) {
+            while (mPlayState == PLAYSTATE_STOPPING || mPlayState == PLAYSTATE_PAUSED_STOPPING) {
+                if (writeMode == WRITE_NON_BLOCKING) {
+                    return false;
+                }
+                try {
+                    mPlayStateLock.wait();
+                } catch (InterruptedException e) {
+                }
+            }
+            return true;
+        }
+    }
+
     //--------------------------------------------------------------------------
     // Audio effects management
     //--------------------
@@ -3293,6 +3396,22 @@
         public void handleMessage(Message msg) {
             final LinkedList<StreamEventCbInfo> cbInfoList;
             synchronized (mStreamEventCbLock) {
+                if (msg.what == NATIVE_EVENT_STREAM_END) {
+                    synchronized (mPlayStateLock) {
+                        if (mPlayState == PLAYSTATE_STOPPING) {
+                            if (mOffloadEosPending) {
+                                native_start();
+                                mPlayState = PLAYSTATE_PLAYING;
+                            } else {
+                                mAvSyncHeader = null;
+                                mAvSyncBytesRemaining = 0;
+                                mPlayState = PLAYSTATE_STOPPED;
+                            }
+                            mOffloadEosPending = false;
+                            mPlayStateLock.notify();
+                        }
+                    }
+                }
                 if (mStreamEventCbInfoList.size() == 0) {
                     return;
                 }
@@ -3560,7 +3679,6 @@
     private native int native_getPortId();
 
     private native void native_set_delay_padding(int delayInFrames, int paddingInFrames);
-    private native void native_set_eos();
 
     //---------------------------------------------------------
     // Utility methods
diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java
index 58c6be9..5645ba5 100644
--- a/media/java/android/media/ExifInterface.java
+++ b/media/java/android/media/ExifInterface.java
@@ -1794,15 +1794,13 @@
             // Set thumbnail image offset and length
             setThumbnailData(inputStream);
             mIsSupportedFile = true;
-        } catch (IOException e) {
+        } catch (IOException | OutOfMemoryError e) {
             // Ignore exceptions in order to keep the compatibility with the old versions of
             // ExifInterface.
             mIsSupportedFile = false;
-            if (DEBUG) {
-                Log.d(TAG, "Invalid image: ExifInterface got an unsupported image format file"
-                        + "(ExifInterface supports JPEG and some RAW image formats only) "
-                        + "or a corrupted JPEG file to ExifInterface.", e);
-            }
+            Log.w(TAG, "Invalid image: ExifInterface got an unsupported image format file"
+                    + "(ExifInterface supports JPEG and some RAW image formats only) "
+                    + "or a corrupted JPEG file to ExifInterface.", e);
         } finally {
             addDefaultValuesForCompatibility();
 
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index a790441..71f52a1 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -178,9 +178,6 @@
 
     void handleBluetoothA2dpDeviceConfigChange(in BluetoothDevice device);
 
-    void handleBluetoothA2dpActiveDeviceChange(in BluetoothDevice device,
-            int state, int profile, boolean suppressNoisyIntent, int a2dpVolume);
-
     @UnsupportedAppUsage
     AudioRoutesInfo startWatchingRoutes(in IAudioRoutesObserver observer);
 
diff --git a/media/java/android/media/MediaFile.java b/media/java/android/media/MediaFile.java
index 710b503..0b1ae34 100644
--- a/media/java/android/media/MediaFile.java
+++ b/media/java/android/media/MediaFile.java
@@ -220,7 +220,6 @@
             case "audio/mpegurl":
             case "application/x-mpegurl":
             case "application/vnd.apple.mpegurl":
-            case "video/x-ms-asf":
             case "audio/x-scpls":
                 return true;
             default:
diff --git a/media/java/android/media/MediaHTTPConnection.java b/media/java/android/media/MediaHTTPConnection.java
index b5c4cca..8ee929e 100644
--- a/media/java/android/media/MediaHTTPConnection.java
+++ b/media/java/android/media/MediaHTTPConnection.java
@@ -38,6 +38,7 @@
 import java.net.UnknownServiceException;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /** @hide */
 public class MediaHTTPConnection extends IMediaHTTPConnection.Stub {
@@ -83,6 +84,10 @@
     private final static int HTTP_TEMP_REDIRECT = 307;
     private final static int MAX_REDIRECTS = 20;
 
+    // The number of threads that are currently running disconnect() (possibly
+    // not yet holding the synchronized lock).
+    private final AtomicInteger mNumDisconnectingThreads = new AtomicInteger(0);
+
     @UnsupportedAppUsage
     public MediaHTTPConnection() {
         CookieHandler cookieHandler = CookieHandler.getDefault();
@@ -155,19 +160,24 @@
     @Override
     @UnsupportedAppUsage
     public void disconnect() {
-        HttpURLConnection connectionToDisconnect = mConnection;
-        // Call disconnect() before blocking for the lock in order to ensure that any
-        // other thread that is blocked in readAt() will return quickly.
-        if (connectionToDisconnect != null) {
-            connectionToDisconnect.disconnect();
-        }
-        synchronized (this) {
-            // It's unlikely but possible that while we were waiting to acquire the lock, another
-            // thread concurrently started a new connection; if so, we're disconnecting that one
-            // here, too.
-            teardownConnection();
-            mHeaders = null;
-            mURL = null;
+        mNumDisconnectingThreads.incrementAndGet();
+        try {
+            HttpURLConnection connectionToDisconnect = mConnection;
+            // Call disconnect() before blocking for the lock in order to ensure that any
+            // other thread that is blocked in readAt() will return quickly.
+            if (connectionToDisconnect != null) {
+                connectionToDisconnect.disconnect();
+            }
+            synchronized (this) {
+                // It's possible that while we were waiting to acquire the lock, another thread
+                // concurrently started a new connection; if so, we're disconnecting that one
+                // here, too.
+                teardownConnection();
+                mHeaders = null;
+                mURL = null;
+            }
+        } finally {
+            mNumDisconnectingThreads.decrementAndGet();
         }
     }
 
@@ -224,11 +234,36 @@
             boolean noProxy = isLocalHost(url);
 
             while (true) {
+                // If another thread is concurrently disconnect()ing, there's a race
+                // between them and us. Therefore, we check mNumDisconnectingThreads shortly
+                // (not atomically) before & after writing mConnection. This guarantees that
+                // we won't "lose" a disconnect by creating a new connection that might
+                // miss the disconnect.
+                //
+                // Note that throwing an instanceof IOException is also what this thread
+                // would have done if another thread disconnect()ed the connection while
+                // this thread was blocked reading from that connection further down in this
+                // loop.
+                if (mNumDisconnectingThreads.get() > 0) {
+                    throw new IOException("concurrently disconnecting");
+                }
                 if (noProxy) {
                     mConnection = (HttpURLConnection)url.openConnection(Proxy.NO_PROXY);
                 } else {
                     mConnection = (HttpURLConnection)url.openConnection();
                 }
+                // If another thread is concurrently disconnecting, throwing IOException will
+                // cause us to release the lock, giving the other thread a chance to acquire
+                // it. It also ensures that the catch block will run, which will tear down
+                // the connection even if the other thread happens to already be on its way
+                // out of disconnect().
+                if (mNumDisconnectingThreads.get() > 0) {
+                    throw new IOException("concurrently disconnecting");
+                }
+                // If we get here without having thrown, we know that other threads
+                // will see our write to mConnection. Any disconnect() on that mConnection
+                // instance will cause our read from/write to that connection instance below
+                // to encounter an instanceof IOException.
                 mConnection.setConnectTimeout(CONNECT_TIMEOUT_MS);
 
                 // handle redirects ourselves if we do not allow cross-domain redirect
diff --git a/media/java/android/media/MediaMetadataRetriever.java b/media/java/android/media/MediaMetadataRetriever.java
index 112ce1d..c43b78c 100644
--- a/media/java/android/media/MediaMetadataRetriever.java
+++ b/media/java/android/media/MediaMetadataRetriever.java
@@ -869,7 +869,7 @@
      * This key retrieves the location information, if available.
      * The location should be specified according to ISO-6709 standard, under
      * a mp4/3gp box "@xyz". Location with longitude of -90 degrees and latitude
-     * of 180 degrees will be retrieved as "-90.0000+180.0000", for instance.
+     * of 180 degrees will be retrieved as "+180.0000-90.0000/", for instance.
      */
     public static final int METADATA_KEY_LOCATION        = 23;
     /**
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/CameraTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/CameraTest.java
index 7f23ba5..9b643ad 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/CameraTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/CameraTest.java
@@ -25,7 +25,6 @@
 import android.hardware.Camera.PreviewCallback;
 import android.hardware.Camera.ShutterCallback;
 import android.os.ConditionVariable;
-import android.os.Environment;
 import android.os.Looper;
 import android.test.ActivityInstrumentationTestCase;
 import android.test.suitebuilder.annotation.LargeTest;
@@ -159,7 +158,7 @@
                 if (rawData != null) {
                     int rawDataLength = rawData.length;
                     File rawoutput = new File(
-                            Environment.getExternalStorageDirectory().toString(), "/test.bmp");
+                            mContext.getExternalFilesDir(null).getPath(), "/test.bmp");
                     FileOutputStream outstream = new FileOutputStream(rawoutput);
                     outstream.write(rawData);                   
                     Log.v(TAG, "JpegPictureCallback rawDataLength = " + rawDataLength);
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestHelper.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestHelper.java
index 84153d60..bd236a6 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestHelper.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestHelper.java
@@ -22,10 +22,11 @@
 import android.hardware.Camera.Parameters;
 import android.hardware.Camera.PictureCallback;
 import android.hardware.Camera.ShutterCallback;
-import android.os.Environment;
 import android.util.Log;
 import android.view.SurfaceHolder;
 
+import androidx.test.InstrumentationRegistry;
+
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
@@ -70,7 +71,8 @@
             try {
                 Log.v(TAG, "JPEG picture taken");
                 fos = new FileOutputStream(String.format("%s/%s/%s-%d.jpg",
-                        Environment.getExternalStorageDirectory(), CAMERA_STRESS_IMAGES_DIRECTORY,
+                        InstrumentationRegistry.getInstrumentation().getTargetContext()
+                        .getExternalFilesDir(null).getPath(), CAMERA_STRESS_IMAGES_DIRECTORY,
                         CAMERA_STRESS_IMAGES_PREFIX, System.currentTimeMillis()));
                 fos.write(data);
             } catch (FileNotFoundException e) {
@@ -95,7 +97,8 @@
     public void setupCameraTest() {
         // Create the test images directory if it doesn't exist
         File stressImagesDirectory = new File(String.format("%s/%s",
-                Environment.getExternalStorageDirectory(), CAMERA_STRESS_IMAGES_DIRECTORY));
+                InstrumentationRegistry.getInstrumentation().getTargetContext()
+                .getExternalFilesDir(null).getPath(), CAMERA_STRESS_IMAGES_DIRECTORY));
         if (!stressImagesDirectory.exists()) {
             stressImagesDirectory.mkdir();
         }
@@ -129,7 +132,8 @@
     public void cleanupTestImages() {
         try {
             File stressImagesDirectory = new File(String.format("%s/%s",
-                    Environment.getExternalStorageDirectory(), CAMERA_STRESS_IMAGES_DIRECTORY));
+                    InstrumentationRegistry.getInstrumentation().getTargetContext()
+                    .getExternalFilesDir(null).getPath(), CAMERA_STRESS_IMAGES_DIRECTORY));
             File[] stressImages = stressImagesDirectory.listFiles();
             for (File f : stressImages) {
                 f.delete();
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestUtils.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestUtils.java
index 0340cec..0ae640d 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestUtils.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestUtils.java
@@ -54,7 +54,6 @@
 import android.media.ImageReader;
 import android.media.ImageWriter;
 import android.os.Build;
-import android.os.Environment;
 import android.os.Handler;
 import android.util.Log;
 import android.util.Pair;
@@ -63,6 +62,8 @@
 import android.view.Surface;
 import android.view.WindowManager;
 
+import androidx.test.InstrumentationRegistry;
+
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.lang.reflect.Array;
@@ -128,7 +129,8 @@
     private static final Location sTestLocation2 = new Location(LocationManager.NETWORK_PROVIDER);
 
     protected static final String DEBUG_FILE_NAME_BASE =
-            Environment.getExternalStorageDirectory().getPath();
+            InstrumentationRegistry.getInstrumentation().getTargetContext()
+            .getExternalFilesDir(null).getPath();
 
     static {
         sTestLocation0.setTime(1199145600L);
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/Camera2SwitchPreviewTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/Camera2SwitchPreviewTest.java
index 11327ca..a26ee2d 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/Camera2SwitchPreviewTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/Camera2SwitchPreviewTest.java
@@ -104,7 +104,6 @@
     private static final double AE_COMPENSATION_ERROR_TOLERANCE = 0.2;
     // 5 percent error margin for resulting metering regions
     private static final float METERING_REGION_ERROR_PERCENT_DELTA = 0.05f;
-    private final String VIDEO_FILE_PATH = Environment.getExternalStorageDirectory().getPath();
 
     private static final boolean DEBUG_DUMP = Log.isLoggable(TAG, Log.DEBUG);
     private static final int RECORDING_DURATION_MS = 3000;
@@ -137,10 +136,12 @@
     private int mVideoFrameRate;
     private Size mVideoSize;
     private long mRecordingStartTime;
+    private String mVideoFilePath;
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
+        mVideoFilePath = mContext.getExternalFilesDir(null).getPath();
     }
 
     @Override
@@ -371,9 +372,9 @@
         }
 
         // Configure preview and recording surfaces.
-        mOutMediaFileName = VIDEO_FILE_PATH + "/test_video.mp4";
+        mOutMediaFileName = mVideoFilePath + "/test_video.mp4";
         if (DEBUG_DUMP) {
-            mOutMediaFileName = VIDEO_FILE_PATH + "/test_video_" + cameraId + "_"
+            mOutMediaFileName = mVideoFilePath + "/test_video_" + cameraId + "_"
                     + videoSz.toString() + ".mp4";
         }
 
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/CameraStressTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/CameraStressTest.java
index d1193de..74244b9 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/CameraStressTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/CameraStressTest.java
@@ -28,7 +28,6 @@
 import java.util.List;
 
 import android.hardware.Camera.Parameters;
-import android.os.Environment;
 import android.os.Handler;
 import android.os.Looper;
 import android.test.ActivityInstrumentationTestCase2;
@@ -36,6 +35,8 @@
 import android.util.Log;
 import android.view.SurfaceHolder;
 
+import androidx.test.InstrumentationRegistry;
+
 /**
  * Junit / Instrumentation test case for the following camera APIs:
  *  - camera zoom
@@ -85,7 +86,8 @@
 
         mCameraTestHelper = new CameraTestHelper();
         File stressOutFile = new File(String.format("%s/%s",
-                Environment.getExternalStorageDirectory(), CAMERA_STRESS_OUTPUT));
+                InstrumentationRegistry.getInstrumentation().getTargetContext()
+                .getExternalFilesDir(null).getPath(), CAMERA_STRESS_OUTPUT));
         mOutput = new BufferedWriter(new FileWriter(stressOutFile, true));
         mOutput.write(this.getName() + "\n");
     }
diff --git a/native/android/surface_control.cpp b/native/android/surface_control.cpp
index 7ee2f0a..8fe4fec 100644
--- a/native/android/surface_control.cpp
+++ b/native/android/surface_control.cpp
@@ -384,6 +384,9 @@
     transaction->setCrop(surfaceControl, static_cast<const Rect&>(source));
     transaction->setFrame(surfaceControl, static_cast<const Rect&>(destination));
     transaction->setTransform(surfaceControl, transform);
+    bool transformToInverseDisplay = (NATIVE_WINDOW_TRANSFORM_INVERSE_DISPLAY & transform) ==
+            NATIVE_WINDOW_TRANSFORM_INVERSE_DISPLAY;
+    transaction->setTransformToDisplayInverse(surfaceControl, transformToInverseDisplay);
 }
 
 void ASurfaceTransaction_setBufferTransparency(ASurfaceTransaction* aSurfaceTransaction,
diff --git a/packages/CaptivePortalLogin/OWNERS b/packages/CaptivePortalLogin/OWNERS
index d3836d4..52193eb 100644
--- a/packages/CaptivePortalLogin/OWNERS
+++ b/packages/CaptivePortalLogin/OWNERS
@@ -1,8 +1,5 @@
 set noparent
 
-codewiz@google.com
-jchalard@google.com
-junyulai@google.com
 lorenzo@google.com
-reminv@google.com
-satk@google.com
+baligh@google.com
+delphij@google.com
diff --git a/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java b/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java
index bda5743..23fb7b6 100644
--- a/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java
+++ b/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java
@@ -485,7 +485,10 @@
             if (request.isForMainFrame()) {
                 mMainFrameUrl = request.getUrl().toString();
             }
-            return false;
+            // Be careful that two shouldOverrideUrlLoading methods are overridden, but
+            // shouldOverrideUrlLoading(WebView view, String url) was deprecated in API level 24.
+            // TODO: delete deprecated one ??
+            return shouldOverrideUrlLoading(view, mMainFrameUrl);
         }
 
         // A web page consisting of a large broken lock icon to indicate SSL failure.
diff --git a/packages/CarSystemUI/res/layout/car_ongoing_privacy_chip.xml b/packages/CarSystemUI/res/layout/car_ongoing_privacy_chip.xml
deleted file mode 100644
index 918abd9..0000000
--- a/packages/CarSystemUI/res/layout/car_ongoing_privacy_chip.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-<?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.
--->
-
-<com.android.systemui.statusbar.car.privacy.OngoingPrivacyChip
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/car_privacy_chip"
-    android:layout_width="wrap_content"
-    android:layout_height="match_parent"
-    android:layout_margin="@dimen/ongoing_appops_chip_margin"
-    android:layout_toStartOf="@+id/clock_container"
-    android:focusable="true"
-    android:gravity="center_vertical|end"
-    android:orientation="horizontal"
-    android:paddingEnd="@dimen/ongoing_appops_chip_side_padding"
-    android:paddingStart="@dimen/ongoing_appops_chip_side_padding"
-    android:visibility="visible">
-
-    <LinearLayout
-        android:id="@+id/icons_container"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:gravity="center_vertical|start"/>
-</com.android.systemui.statusbar.car.privacy.OngoingPrivacyChip>
\ No newline at end of file
diff --git a/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml b/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml
index cae89c1..925ccb4 100644
--- a/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml
+++ b/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml
@@ -65,8 +65,6 @@
             />
         </FrameLayout>
 
-        <include layout="@layout/car_ongoing_privacy_chip"/>
-
         <FrameLayout
             android:id="@+id/clock_container"
             android:layout_width="wrap_content"
diff --git a/packages/CarSystemUI/res/layout/notification_center_activity.xml b/packages/CarSystemUI/res/layout/notification_center_activity.xml
index 55b0d87..0af74c4 100644
--- a/packages/CarSystemUI/res/layout/notification_center_activity.xml
+++ b/packages/CarSystemUI/res/layout/notification_center_activity.xml
@@ -38,7 +38,7 @@
         android:layout_width="0dp"
         android:layout_height="0dp"
         android:orientation="vertical"
-        android:paddingStart="@dimen/notification_shade_list_padding_bottom"
+        android:paddingBottom="@dimen/notification_shade_list_padding_bottom"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
diff --git a/packages/CarSystemUI/res/values/dimens.xml b/packages/CarSystemUI/res/values/dimens.xml
index 7027ce3..0358357b 100644
--- a/packages/CarSystemUI/res/values/dimens.xml
+++ b/packages/CarSystemUI/res/values/dimens.xml
@@ -59,16 +59,6 @@
     <dimen name="car_keyline_1">24dp</dimen>
     <dimen name="car_keyline_2">96dp</dimen>
     <dimen name="car_keyline_3">128dp</dimen>
-    <dimen name="privacy_chip_icon_height">36dp</dimen>
-    <dimen name="privacy_chip_icon_padding_left">0dp</dimen>
-    <dimen name="privacy_chip_icon_padding_right">0dp</dimen>
-    <dimen name="privacy_chip_icon_padding_top">0dp</dimen>
-    <dimen name="privacy_chip_icon_padding_bottom">0dp</dimen>
-
-    <dimen name="privacy_chip_text_padding_left">0dp</dimen>
-    <dimen name="privacy_chip_text_padding_right">0dp</dimen>
-    <dimen name="privacy_chip_text_padding_top">0dp</dimen>
-    <dimen name="privacy_chip_text_padding_bottom">0dp</dimen>
 
     <dimen name="privacy_chip_icon_max_height">100dp</dimen>
 
@@ -86,16 +76,6 @@
     <dimen name="ongoing_appops_chip_bg_padding">4dp</dimen>
     <!-- Radius of Ongoing App Ops chip corners -->
     <dimen name="ongoing_appops_chip_bg_corner_radius">12dp</dimen>
-    <!-- Start padding for the app icon displayed in the dialog -->
-    <dimen name="privacy_dialog_app_icon_padding_start">40dp</dimen>
-    <!-- End padding for the app opps icon displayed in the dialog -->
-    <dimen name="privacy_dialog_app_ops_icon_padding_end">40dp</dimen>
-    <!-- Top padding for the list of application displayed in the dialog -->
-    <dimen name="privacy_dialog_app_list_padding_top">20dp</dimen>
-    <!-- Top padding for the dialog container-->
-    <dimen name="privacy_dialog_container_padding_top">10dp</dimen>
-    <!-- Top padding for the dialog title-->
-    <dimen name="privacy_dialog_title_padding_start">10dp</dimen>
 
     <!-- Car volume dimens. -->
     <dimen name="car_volume_item_height">@*android:dimen/car_single_line_list_item_height</dimen>
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
index b6b34c7..7fbdc2e 100644
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
@@ -389,22 +389,18 @@
                         }
                     }
                 });
+
+        // Attached to the Handle bar to close the notification shade
+        GestureDetector handleBarCloseNotificationGestureDetector = new GestureDetector(mContext,
+                new HandleBarCloseNotificationGestureListener());
+
         mNavBarNotificationTouchListener =
                 (v, event) -> {
                     boolean consumed = navBarCloseNotificationGestureDetector.onTouchEvent(event);
                     if (consumed) {
                         return true;
                     }
-                    if (event.getActionMasked() == MotionEvent.ACTION_UP
-                            && mNotificationView.getVisibility() == View.VISIBLE) {
-                        if (mSettleClosePercentage < mPercentageFromBottom) {
-                            animateNotificationPanel(
-                                    DEFAULT_FLING_VELOCITY, false);
-                        } else {
-                            animateNotificationPanel(DEFAULT_FLING_VELOCITY,
-                                    true);
-                        }
-                    }
+                    maybeCompleteAnimation(event);
                     return true;
                 };
 
@@ -418,15 +414,7 @@
                     if (consumed) {
                         return true;
                     }
-                    if (event1.getActionMasked() == MotionEvent.ACTION_UP
-                            && mNotificationView.getVisibility() == View.VISIBLE) {
-                        if (mSettleOpenPercentage > mPercentageFromBottom) {
-                            animateNotificationPanel(DEFAULT_FLING_VELOCITY, true);
-                        } else {
-                            animateNotificationPanel(
-                                    DEFAULT_FLING_VELOCITY, false);
-                        }
-                    }
+                    maybeCompleteAnimation(event1);
                     return true;
                 }
         );
@@ -498,6 +486,13 @@
             }
             return false;
         });
+
+        mHandleBar.setOnTouchListener((v, event) -> {
+            handleBarCloseNotificationGestureDetector.onTouchEvent(event);
+            maybeCompleteAnimation(event);
+            return true;
+        });
+
         mNotificationList = mNotificationView.findViewById(R.id.notifications);
         mNotificationList.addOnScrollListener(new RecyclerView.OnScrollListener() {
             @Override
@@ -612,6 +607,17 @@
         setPanelExpanded(false);
     }
 
+    private void maybeCompleteAnimation(MotionEvent event) {
+        if (event.getActionMasked() == MotionEvent.ACTION_UP
+                && mNotificationView.getVisibility() == View.VISIBLE) {
+            if (mSettleClosePercentage < mPercentageFromBottom) {
+                animateNotificationPanel(DEFAULT_FLING_VELOCITY, false);
+            } else {
+                animateNotificationPanel(DEFAULT_FLING_VELOCITY, true);
+            }
+        }
+    }
+
     /**
      * Animates the notification shade from one position to other. This is used to either open or
      * close the notification shade completely with a velocity. If the animation is to close the
@@ -1054,8 +1060,10 @@
     private static final int SWIPE_MAX_OFF_PATH = 75;
     private static final int SWIPE_THRESHOLD_VELOCITY = 200;
 
-    // Only responsible for open hooks. Since once the panel opens it covers all elements
-    // there is no need to merge with close.
+    /**
+     * Only responsible for open hooks. Since once the panel opens it covers all elements
+     * there is no need to merge with close.
+     */
     private abstract class OpenNotificationGestureListener extends
             GestureDetector.SimpleOnGestureListener {
 
@@ -1098,7 +1106,9 @@
         protected abstract void openNotification();
     }
 
-    // to be installed on the open panel notification panel
+    /**
+     * To be installed on the open panel notification panel
+     */
     private abstract class CloseNotificationGestureListener extends
             GestureDetector.SimpleOnGestureListener {
 
@@ -1172,7 +1182,9 @@
         protected abstract void close();
     }
 
-    // To be installed on the nav bars.
+    /**
+     * To be installed on the nav bars.
+     */
     private abstract class NavBarCloseNotificationGestureListener extends
             CloseNotificationGestureListener {
         @Override
@@ -1201,7 +1213,28 @@
     }
 
     /**
-     * SystemUi version onf the notification manager that overrides methods such that the
+     * To be installed on the handle bar.
+     */
+    private class HandleBarCloseNotificationGestureListener extends
+            GestureDetector.SimpleOnGestureListener {
+
+        @Override
+        public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
+                float distanceY) {
+            calculatePercentageFromBottom(event2.getRawY());
+            // To prevent the jump in the clip bounds while closing the notification shade using
+            // the handle bar we should calculate the height using the diff of event1 and event2.
+            // This will help the notification shade to clip smoothly as the event2 value changes
+            // as event1 value will be fixed.
+            int clipHeight =
+                    mNotificationView.getHeight() - (int) (event1.getRawY() - event2.getRawY());
+            setNotificationViewClipBounds(clipHeight);
+            return true;
+        }
+    }
+
+    /**
+     * SystemUi version of the notification manager that overrides methods such that the
      * notifications end up in the status bar layouts instead of a standalone window.
      */
     private class CarSystemUIHeadsUpNotificationManager extends CarHeadsUpNotificationManager {
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/OngoingPrivacyChip.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/OngoingPrivacyChip.java
deleted file mode 100644
index ead1de2..0000000
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/OngoingPrivacyChip.java
+++ /dev/null
@@ -1,228 +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.car.privacy;
-
-import android.app.ActivityManager;
-import android.app.AppOpsManager;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-
-import com.android.systemui.Dependency;
-import com.android.systemui.R;
-import com.android.systemui.appops.AppOpItem;
-import com.android.systemui.appops.AppOpsController;
-import com.android.systemui.plugins.ActivityStarter;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-/**
- * Layout defining the privacy chip that will be displayed in CarStatusRar with the information for
- * which applications are using AppOpps permission fpr camera, mic and location.
- */
-public class OngoingPrivacyChip extends LinearLayout implements View.OnClickListener {
-
-    private Context mContext;
-
-    private LinearLayout mIconsContainer;
-    private List<PrivacyItem> mPrivacyItems;
-    private static AppOpsController sAppOpsController;
-    private UserManager mUserManager;
-    private int mCurrentUser;
-    private List<Integer> mCurrentUserIds;
-    private boolean mListening = false;
-    PrivacyDialogBuilder mPrivacyDialogBuilder;
-    private LinearLayout mPrivacyChip;
-    private ActivityStarter mActivityStarter;
-
-    protected static final int[] OPS = new int[]{
-            AppOpsManager.OP_CAMERA,
-            AppOpsManager.OP_RECORD_AUDIO,
-            AppOpsManager.OP_COARSE_LOCATION,
-            AppOpsManager.OP_FINE_LOCATION
-    };
-
-    public OngoingPrivacyChip(Context context) {
-        super(context, null);
-        init(context);
-    }
-
-    public OngoingPrivacyChip(Context context, AttributeSet attr) {
-        super(context, attr);
-        init(context);
-    }
-
-    public OngoingPrivacyChip(Context context, AttributeSet attr, int defStyle) {
-        super(context, attr, defStyle);
-        init(context);
-    }
-
-    public OngoingPrivacyChip(Context context, AttributeSet attr, int defStyle, int a) {
-        super(context, attr, defStyle, a);
-        init(context);
-    }
-
-    private void init(Context context) {
-        mContext = context;
-        mPrivacyItems = new ArrayList<>();
-        sAppOpsController = Dependency.get(AppOpsController.class);
-        mUserManager = mContext.getSystemService(UserManager.class);
-        mActivityStarter = Dependency.get(ActivityStarter.class);
-        mCurrentUser = ActivityManager.getCurrentUser();
-        mCurrentUserIds = mUserManager.getProfiles(mCurrentUser).stream().map(
-                userInfo -> userInfo.id).collect(Collectors.toList());
-
-        mPrivacyDialogBuilder = new PrivacyDialogBuilder(context, mPrivacyItems);
-    }
-
-    private AppOpsController.Callback mCallback = new AppOpsController.Callback() {
-
-        @Override
-        public void onActiveStateChanged(int code, int uid, String packageName, boolean active) {
-            int userId = UserHandle.getUserId(uid);
-            if (mCurrentUserIds.contains(userId)) {
-                updatePrivacyList();
-            }
-        }
-    };
-
-    @Override
-    public void onFinishInflate() {
-        mIconsContainer = findViewById(R.id.icons_container);
-        mPrivacyChip = (LinearLayout) findViewById(R.id.car_privacy_chip);
-        if (mPrivacyChip != null) {
-            mPrivacyChip.setOnClickListener(this);
-            setListening(true);
-        }
-    }
-
-    @Override
-    public void onDetachedFromWindow() {
-        if (mPrivacyChip != null) {
-            setListening(false);
-        }
-        super.onDetachedFromWindow();
-    }
-
-    @Override
-    public void onClick(View v) {
-        updatePrivacyList();
-        Handler mUiHandler = new Handler(Looper.getMainLooper());
-        mUiHandler.post(() -> {
-            mActivityStarter.postStartActivityDismissingKeyguard(
-                    new Intent(Intent.ACTION_REVIEW_ONGOING_PERMISSION_USAGE), 0);
-        });
-    }
-
-    private void setListening(boolean listen) {
-        if (mListening == listen) {
-            return;
-        }
-        mListening = listen;
-        if (mListening) {
-            sAppOpsController.addCallback(OPS, mCallback);
-            updatePrivacyList();
-        } else {
-            sAppOpsController.removeCallback(OPS, mCallback);
-        }
-    }
-
-    private void updatePrivacyList() {
-        mPrivacyItems = mCurrentUserIds.stream()
-                .flatMap(item -> sAppOpsController.getActiveAppOpsForUser(item).stream())
-                .filter(Objects::nonNull)
-                .map(item -> toPrivacyItem(item))
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList());
-        mPrivacyDialogBuilder = new PrivacyDialogBuilder(mContext, mPrivacyItems);
-
-        Handler refresh = new Handler(Looper.getMainLooper());
-        refresh.post(new Runnable() {
-            @Override
-            public void run() {
-                updateView();
-            }
-        });
-    }
-
-    private PrivacyItem toPrivacyItem(AppOpItem appOpItem) {
-        PrivacyType type;
-        switch (appOpItem.getCode()) {
-            case AppOpsManager.OP_CAMERA:
-                type = PrivacyType.TYPE_CAMERA;
-                break;
-            case AppOpsManager.OP_COARSE_LOCATION:
-                type = PrivacyType.TYPE_LOCATION;
-                break;
-            case AppOpsManager.OP_FINE_LOCATION:
-                type = PrivacyType.TYPE_LOCATION;
-                break;
-            case AppOpsManager.OP_RECORD_AUDIO:
-                type = PrivacyType.TYPE_MICROPHONE;
-                break;
-            default:
-                return null;
-        }
-        PrivacyApplication app = new PrivacyApplication(appOpItem.getPackageName(), mContext);
-        return new PrivacyItem(type, app, appOpItem.getTimeStarted());
-    }
-
-    // Should only be called if the mPrivacyDialogBuilder icons or app changed
-    private void updateView() {
-        if (mPrivacyItems.isEmpty()) {
-            mPrivacyChip.setVisibility(GONE);
-            return;
-        }
-        mPrivacyChip.setVisibility(VISIBLE);
-        setIcons(mPrivacyDialogBuilder);
-
-        requestLayout();
-    }
-
-    private void setIcons(PrivacyDialogBuilder dialogBuilder) {
-        mIconsContainer.removeAllViews();
-        dialogBuilder.generateIcons().forEach(item -> {
-            int size = mContext.getResources().getDimensionPixelSize(
-                    R.dimen.privacy_chip_icon_height);
-            ImageView image = new ImageView(mContext);
-            image.setImageDrawable(item);
-            LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(size, size);
-
-            int leftPadding = mContext.getResources().getDimensionPixelSize(
-                    R.dimen.privacy_chip_icon_padding_left);
-            int topPadding = mContext.getResources().getDimensionPixelSize(
-                    R.dimen.privacy_chip_icon_padding_top);
-            int rightPadding = mContext.getResources().getDimensionPixelSize(
-                    R.dimen.privacy_chip_icon_padding_right);
-            int bottomPadding = mContext.getResources().getDimensionPixelSize(
-                    R.dimen.privacy_chip_icon_padding_bottom);
-            image.setLayoutParams(layoutParams);
-            image.setPadding(leftPadding, topPadding, rightPadding, bottomPadding);
-            mIconsContainer.addView(image);
-        });
-    }
-}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyApplication.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyApplication.java
deleted file mode 100644
index 5ec7a77..0000000
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyApplication.java
+++ /dev/null
@@ -1,62 +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.car.privacy;
-
-import android.car.userlib.CarUserManagerHelper;
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.graphics.drawable.Drawable;
-import android.util.Log;
-
-/**
- * Class to hold the data for the applications that are using the AppOps permissions.
- */
-public class PrivacyApplication {
-    private static final String TAG = "PrivacyApplication";
-
-    private Drawable mIcon;
-    private String mApplicationName;
-
-    public PrivacyApplication(String packageName, Context context) {
-        try {
-            CarUserManagerHelper carUserManagerHelper = new CarUserManagerHelper(context);
-            ApplicationInfo app = context.getPackageManager()
-                    .getApplicationInfoAsUser(packageName, 0,
-                            carUserManagerHelper.getCurrentForegroundUserId());
-            mIcon = context.getPackageManager().getApplicationIcon(app);
-            mApplicationName = context.getPackageManager().getApplicationLabel(app).toString();
-        } catch (PackageManager.NameNotFoundException e) {
-            mApplicationName = packageName;
-            Log.e(TAG, "Failed to to find package name", e);
-        }
-    }
-
-    /**
-     * Gets the application name.
-     */
-    public Drawable getIcon() {
-        return mIcon;
-    }
-
-    /**
-     * Gets the application name.
-     */
-    public String getApplicationName() {
-        return mApplicationName;
-    }
-}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyDialogBuilder.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyDialogBuilder.java
deleted file mode 100644
index 3b83e7c..0000000
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyDialogBuilder.java
+++ /dev/null
@@ -1,59 +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.car.privacy;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-/**
- * Helper class to build the {@link OngoingPrivacyDialog}
- */
-public class PrivacyDialogBuilder {
-
-    private Map<PrivacyType, List<PrivacyItem>> mItemsByType;
-    private PrivacyApplication mApplication;
-    private Context mContext;
-
-    public PrivacyDialogBuilder(Context context, List<PrivacyItem> itemsList) {
-        mContext = context;
-        mItemsByType = itemsList.stream().filter(Objects::nonNull).collect(
-                Collectors.groupingBy(PrivacyItem::getPrivacyType));
-        List<PrivacyApplication> apps = itemsList.stream().filter(Objects::nonNull).map(
-                PrivacyItem::getPrivacyApplication).distinct().collect(Collectors.toList());
-        mApplication = apps.size() == 1 ? apps.get(0) : null;
-    }
-
-    /**
-     * Gets the icon id for all the {@link PrivacyItem} in the same order as of itemList.
-     */
-    public List<Drawable> generateIcons() {
-        return mItemsByType.keySet().stream().map(item -> item.getIconId(mContext)).collect(
-                Collectors.toList());
-    }
-
-    /**
-     * Gets the application object.
-     */
-    public PrivacyApplication getApplication() {
-        return mApplication;
-    }
-}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyItem.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyItem.java
deleted file mode 100644
index fca1373..0000000
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyItem.java
+++ /dev/null
@@ -1,46 +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.car.privacy;
-
-/**
- * Class for holding the data of each privacy item displayed in {@link OngoingPrivacyDialog}
- */
-public class PrivacyItem {
-
-    private PrivacyType mPrivacyType;
-    private PrivacyApplication mPrivacyApplication;
-
-    public PrivacyItem(PrivacyType privacyType, PrivacyApplication privacyApplication,
-            long timeStarted) {
-        this.mPrivacyType = privacyType;
-        this.mPrivacyApplication = privacyApplication;
-    }
-
-    /**
-     * Gets the application object.
-     */
-    public PrivacyApplication getPrivacyApplication() {
-        return mPrivacyApplication;
-    }
-
-    /**
-     * Gets the privacy type for the application.
-     */
-    public PrivacyType getPrivacyType() {
-        return mPrivacyType;
-    }
-}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyType.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyType.java
deleted file mode 100644
index 8955c87..0000000
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyType.java
+++ /dev/null
@@ -1,53 +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.car.privacy;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-
-import com.android.systemui.R;
-
-/**
- * Enum for storing data for camera, mic and location.
- */
-public enum PrivacyType {
-    TYPE_CAMERA(R.string.privacy_type_camera, com.android.internal.R.drawable.ic_camera),
-    TYPE_LOCATION(R.string.privacy_type_location, R.drawable.stat_sys_location),
-    TYPE_MICROPHONE(R.string.privacy_type_microphone, R.drawable.ic_mic_white);
-
-    private int mNameId;
-    private int mIconId;
-
-    PrivacyType(int nameId, int iconId) {
-        mNameId = nameId;
-        mIconId = iconId;
-    }
-
-    /**
-     * Get the icon Id.
-     */
-    public Drawable getIconId(Context context) {
-        return context.getResources().getDrawable(mIconId, null);
-    }
-
-    /**
-     * Get the name Id.
-     */
-    public String getNameId(Context context) {
-        return context.getResources().getString(mNameId);
-    }
-}
diff --git a/packages/ExtServices/OWNERS b/packages/ExtServices/OWNERS
new file mode 100644
index 0000000..bf52f35
--- /dev/null
+++ b/packages/ExtServices/OWNERS
@@ -0,0 +1,4 @@
+set noparent
+
+baligh@google.com
+delphij@google.com
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index da3416b..1b27b52 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -638,7 +638,7 @@
                     final String docId = DocumentsContract.getDocumentId(documentUri);
                     try {
                         final Bundle out = new Bundle();
-                        final Uri uri = Uri.fromFile(getFileForDocId(docId));
+                        final Uri uri = Uri.fromFile(getFileForDocId(docId, true));
                         out.putParcelable(DocumentsContract.EXTRA_URI, uri);
                         return out;
                     } catch (FileNotFoundException e) {
diff --git a/packages/NetworkPermissionConfig/OWNERS b/packages/NetworkPermissionConfig/OWNERS
new file mode 100644
index 0000000..52193eb
--- /dev/null
+++ b/packages/NetworkPermissionConfig/OWNERS
@@ -0,0 +1,5 @@
+set noparent
+
+lorenzo@google.com
+baligh@google.com
+delphij@google.com
diff --git a/packages/NetworkStack/OWNERS b/packages/NetworkStack/OWNERS
index 0e1e65d..52193eb 100644
--- a/packages/NetworkStack/OWNERS
+++ b/packages/NetworkStack/OWNERS
@@ -1,6 +1,5 @@
-codewiz@google.com
-jchalard@google.com
-junyulai@google.com
+set noparent
+
 lorenzo@google.com
-reminv@google.com
-satk@google.com
+baligh@google.com
+delphij@google.com
diff --git a/packages/NetworkStack/src/com/android/server/NetworkStackService.java b/packages/NetworkStack/src/com/android/server/NetworkStackService.java
index 2fae0c7..9bf4457 100644
--- a/packages/NetworkStack/src/com/android/server/NetworkStackService.java
+++ b/packages/NetworkStack/src/com/android/server/NetworkStackService.java
@@ -195,6 +195,7 @@
         @Override
         public void makeNetworkMonitor(Network network, String name, INetworkMonitorCallbacks cb)
                 throws RemoteException {
+            checkNetworkStackCallingPermission();
             updateSystemAidlVersion(cb.getInterfaceVersion());
             final SharedLog log = addValidationLogs(network, name);
             final NetworkMonitor nm = new NetworkMonitor(mContext, cb, network, log);
@@ -203,6 +204,7 @@
 
         @Override
         public void makeIpClient(String ifName, IIpClientCallbacks cb) throws RemoteException {
+            checkNetworkStackCallingPermission();
             updateSystemAidlVersion(cb.getInterfaceVersion());
             final IpClient ipClient = new IpClient(mContext, ifName, cb, mObserverRegistry, this);
 
@@ -228,6 +230,7 @@
         @Override
         public void fetchIpMemoryStore(@NonNull final IIpMemoryStoreCallbacks cb)
                 throws RemoteException {
+            checkNetworkStackCallingPermission();
             updateSystemAidlVersion(cb.getInterfaceVersion());
             cb.onIpMemoryStoreFetched(mIpMemoryStoreService);
         }
diff --git a/packages/NetworkStack/src/com/android/server/util/PermissionUtil.java b/packages/NetworkStack/src/com/android/server/util/PermissionUtil.java
index 6fbeead..6701384 100644
--- a/packages/NetworkStack/src/com/android/server/util/PermissionUtil.java
+++ b/packages/NetworkStack/src/com/android/server/util/PermissionUtil.java
@@ -16,30 +16,56 @@
 
 package com.android.server.util;
 
+import static android.os.Binder.getCallingPid;
 import static android.os.Binder.getCallingUid;
 
 import android.os.Process;
 import android.os.UserHandle;
 
+import java.util.concurrent.atomic.AtomicInteger;
+
 /**
  * Utility class to check calling permissions on the network stack.
  */
 public final class PermissionUtil {
+    private static final AtomicInteger sSystemPid = new AtomicInteger(-1);
 
     /**
      * Check that the caller is allowed to communicate with the network stack.
      * @throws SecurityException The caller is not allowed to communicate with the network stack.
      */
     public static void checkNetworkStackCallingPermission() {
-        // TODO: check that the calling PID is the system server.
         final int caller = getCallingUid();
-        if (caller != Process.SYSTEM_UID
-                && UserHandle.getAppId(caller) != Process.BLUETOOTH_UID
-                && UserHandle.getAppId(caller) != Process.PHONE_UID) {
+        if (caller == Process.SYSTEM_UID) {
+            checkConsistentSystemPid();
+            return;
+        }
+
+        if (UserHandle.getAppId(caller) != Process.BLUETOOTH_UID) {
             throw new SecurityException("Invalid caller: " + caller);
         }
     }
 
+    private static void checkConsistentSystemPid() {
+        // Apart from the system server process, no process with a system UID should try to
+        // communicate with the network stack. This is to ensure that the network stack does not
+        // need to maintain behavior for clients it was not designed to work with.
+        // Checking that all calls from a system UID originate from the same PID loosely enforces
+        // this restriction as if another system process calls the network stack first, the system
+        // server would lose access to the network stack and cause obvious failures. If the system
+        // server calls the network stack first, other clients would lose access as expected.
+        final int systemPid = getCallingPid();
+        if (sSystemPid.compareAndSet(-1, systemPid)) {
+            // sSystemPid was unset (-1): this was the first call
+            return;
+        }
+
+        if (sSystemPid.get() != systemPid) {
+            throw new SecurityException("Invalid PID for the system server, expected "
+                    + sSystemPid.get() + " but was called from " + systemPid);
+        }
+    }
+
     /**
      * Check that the caller is allowed to dump the network stack, e.g. dumpsys.
      * @throws SecurityException The caller is not allowed to dump the network stack.
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 4c72f48..0e91839 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -578,6 +578,8 @@
     <string name="wifi_display_certification">Wireless display certification</string>
     <!-- Setting Checkbox title whether to enable WiFi Verbose Logging. [CHAR LIMIT=40] -->
     <string name="wifi_verbose_logging">Enable Wi\u2011Fi Verbose Logging</string>
+    <!-- Setting Checkbox title whether to disable WiFi Scan Throttling. [CHAR LIMIT=40] -->
+    <string name="wifi_scan_throttling">Wi\u2011Fi scan throttling</string>
     <!-- Setting Checkbox title whether to always keep mobile data active. [CHAR LIMIT=80] -->
     <string name="mobile_data_always_on">Mobile data always active</string>
     <!-- Setting Checkbox title whether to enable hardware acceleration for tethering. [CHAR LIMIT=80] -->
@@ -633,6 +635,8 @@
     <string name="wifi_display_certification_summary">Show options for wireless display certification</string>
     <!-- Setting Checkbox summary whether to enable Wifi verbose Logging [CHAR LIMIT=80] -->
     <string name="wifi_verbose_logging_summary">Increase Wi\u2011Fi logging level, show per SSID RSSI in Wi\u2011Fi Picker</string>
+    <!-- Setting Checkbox summary whether to disable Wifi scan throttling [CHAR LIMIT=NONE] -->
+    <string name="wifi_scan_throttling_summary">Reduces battery drain &amp; improves network performance</string>
     <!-- Label indicating network has been manually marked as metered -->
     <string name="wifi_metered_label">Metered</string>
     <!-- Label indicating network has been manually marked as unmetered -->
diff --git a/packages/SettingsLib/src/com/android/settingslib/net/SignalStrengthUtil.java b/packages/SettingsLib/src/com/android/settingslib/net/SignalStrengthUtil.java
new file mode 100644
index 0000000..246f2ce
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/net/SignalStrengthUtil.java
@@ -0,0 +1,34 @@
+/*
+ * 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.settingslib.net;
+
+import android.content.Context;
+import android.telephony.SubscriptionManager;
+
+/**
+ * Utilities for dealing with signal strength.
+ */
+public class SignalStrengthUtil {
+    /**
+     * @return whether we should artificially inflate the signal strength and number of levels by 1
+     * bar for the subscription with the given id
+     */
+    public static boolean shouldInflateSignalStrength(Context context, int subscriptionId) {
+        return SubscriptionManager.getResourcesForSubId(context, subscriptionId)
+                .getBoolean(com.android.internal.R.bool.config_inflateSignalStrength);
+    }
+}
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index f7132e3..d07bc32 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -3043,15 +3043,16 @@
             // Increment the generation first, so observers always see the new value
             mGenerationRegistry.incrementGeneration(key);
 
-            if (isGlobalSettingsKey(key)) {
+            if (isGlobalSettingsKey(key) || isConfigSettingsKey(key)) {
                 final long token = Binder.clearCallingIdentity();
                 try {
-                    if (Global.LOCATION_GLOBAL_KILL_SWITCH.equals(name)) {
+                    if (Global.LOCATION_GLOBAL_KILL_SWITCH.equals(name)
+                            && isGlobalSettingsKey(key)) {
                         // When the global kill switch is updated, send the
                         // change notification for the location setting.
                         notifyLocationChangeForRunningUsers();
                     }
-                    notifyGlobalSettingChangeForRunningUsers(key, name);
+                    notifySettingChangeForRunningUsers(key, name);
                 } finally {
                     Binder.restoreCallingIdentity(token);
                 }
@@ -3091,7 +3092,7 @@
             }
         }
 
-        private void notifyGlobalSettingChangeForRunningUsers(int key, String name) {
+        private void notifySettingChangeForRunningUsers(int key, String name) {
             // Important: No need to update generation for each user as there
             // is a singleton generation entry for the global settings which
             // is already incremented be the caller.
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index a245086..da139d7 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -206,8 +206,9 @@
     <uses-permission android:name="android.permission.INTERACT_ACROSS_PROFILES"/>
 
     <application android:label="@string/app_label"
-                 android:defaultToDeviceProtectedStorage="true"
-                 android:directBootAware="true">
+                android:theme="@android:style/Theme.DeviceDefault.DayNight"
+                android:defaultToDeviceProtectedStorage="true"
+                android:directBootAware="true">
         <provider
             android:name="androidx.core.content.FileProvider"
             android:authorities="com.android.shell"
@@ -232,7 +233,6 @@
 
         <activity
             android:name=".BugreportWarningActivity"
-            android:theme="@android:style/Theme.DeviceDefault.Light.Dialog.Alert"
             android:finishOnCloseSystemDialogs="true"
             android:excludeFromRecents="true"
             android:exported="false" />
diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java
index 1060c7b..e57a5de 100644
--- a/packages/Shell/src/com/android/shell/BugreportProgressService.java
+++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java
@@ -99,6 +99,7 @@
 import android.util.Pair;
 import android.util.Patterns;
 import android.util.SparseArray;
+import android.view.ContextThemeWrapper;
 import android.view.IWindowManager;
 import android.view.View;
 import android.view.WindowManager;
@@ -1467,11 +1468,13 @@
         void initialize(final Context context, BugreportInfo info) {
             final String dialogTitle =
                     context.getString(R.string.bugreport_info_dialog_title, info.id);
+            final Context themedContext = new ContextThemeWrapper(
+                    context, com.android.internal.R.style.Theme_DeviceDefault_DayNight);
             // First initializes singleton.
             if (mDialog == null) {
                 @SuppressLint("InflateParams")
                 // It's ok pass null ViewRoot on AlertDialogs.
-                final View view = View.inflate(context, R.layout.dialog_bugreport_info, null);
+                final View view = View.inflate(themedContext, R.layout.dialog_bugreport_info, null);
 
                 mInfoName = (EditText) view.findViewById(R.id.name);
                 mInfoTitle = (EditText) view.findViewById(R.id.title);
@@ -1488,7 +1491,7 @@
                     }
                 });
 
-                mDialog = new AlertDialog.Builder(context)
+                mDialog = new AlertDialog.Builder(themedContext)
                         .setView(view)
                         .setTitle(dialogTitle)
                         .setCancelable(true)
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 9425941..91a8ab5 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -58,6 +58,7 @@
         "androidx.arch.core_core-runtime",
         "androidx.lifecycle_lifecycle-extensions",
         "androidx.dynamicanimation_dynamicanimation",
+        "androidx-constraintlayout_constraintlayout",
         "iconloader_base",
         "SystemUI-tags",
         "SystemUI-proto",
@@ -111,6 +112,7 @@
         "androidx.arch.core_core-runtime",
         "androidx.lifecycle_lifecycle-extensions",
         "androidx.dynamicanimation_dynamicanimation",
+        "androidx-constraintlayout_constraintlayout",
         "SystemUI-tags",
         "SystemUI-proto",
         "metrics-helper-lib",
diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS
index c071b8b..83acfa0 100644
--- a/packages/SystemUI/OWNERS
+++ b/packages/SystemUI/OWNERS
@@ -12,6 +12,7 @@
 dupin@google.com
 ethibodeau@google.com
 evanlaird@google.com
+hyunyoungs@google.com
 jmonk@google.com
 jaggies@google.com
 jjaggi@google.com
@@ -24,6 +25,8 @@
 lynhan@google.com
 madym@google.com
 mankoff@google.com
+mrcasey@google.com
+mrenouf@google.com
 nbenbernou@google.com
 nesciosquid@google.com
 ngmatthew@google.com
diff --git a/packages/SystemUI/res/color/notification_guts_priority_button_bg_fill.xml b/packages/SystemUI/res/color/notification_guts_priority_button_bg_fill.xml
new file mode 100644
index 0000000..8ef1bf9c
--- /dev/null
+++ b/packages/SystemUI/res/color/notification_guts_priority_button_bg_fill.xml
@@ -0,0 +1,21 @@
+<!--
+  ~ 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
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true"
+          android:color="@color/notification_guts_priority_button_bg_fill_color_selected" />
+    <item android:color="@color/notification_guts_priority_button_bg_fill_color" />
+</selector>
diff --git a/packages/SystemUI/res/color/notification_guts_priority_button_bg_stroke.xml b/packages/SystemUI/res/color/notification_guts_priority_button_bg_stroke.xml
new file mode 100644
index 0000000..7964609
--- /dev/null
+++ b/packages/SystemUI/res/color/notification_guts_priority_button_bg_stroke.xml
@@ -0,0 +1,21 @@
+<!--
+  ~ 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
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true"
+          android:color="@color/notification_guts_priority_button_bg_stroke_color_selected" />
+    <item android:color="@color/notification_guts_priority_button_bg_stroke_color" />
+</selector>
diff --git a/packages/SystemUI/res/color/notification_guts_priority_contents.xml b/packages/SystemUI/res/color/notification_guts_priority_contents.xml
new file mode 100644
index 0000000..56c43f0
--- /dev/null
+++ b/packages/SystemUI/res/color/notification_guts_priority_contents.xml
@@ -0,0 +1,21 @@
+<!--
+  ~ 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.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true"
+          android:color="@color/notification_guts_priority_button_content_color_selected" />
+    <item android:color="@color/notification_guts_priority_button_content_color" />
+</selector>
diff --git a/packages/SystemUI/res/drawable/button_border_selected.xml b/packages/SystemUI/res/drawable/button_border_selected.xml
deleted file mode 100644
index 01e7099..0000000
--- a/packages/SystemUI/res/drawable/button_border_selected.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?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.
-  -->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-       android:shape="rectangle">
-    <solid
-        android:color="@color/notification_guts_selection_bg" />
-    <stroke
-        android:width="1dp"
-        android:color="@color/GM2_grey_300"/>
-    <corners android:radius="@dimen/rect_button_radius" />
-</shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/button_border_unselected.xml b/packages/SystemUI/res/drawable/button_border_unselected.xml
deleted file mode 100644
index b9c4ced..0000000
--- a/packages/SystemUI/res/drawable/button_border_unselected.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?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.
-  -->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-       android:shape="rectangle"
-       android:color="@color/notification_guts_selection_bg">
-    <stroke
-        android:width="1dp"
-        android:color="@color/GM2_grey_300"/>
-
-    <corners android:radius="@dimen/rect_button_radius" />
-</shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/button_ripple_radius.xml b/packages/SystemUI/res/drawable/button_ripple_radius.xml
deleted file mode 100644
index 5c2857a..0000000
--- a/packages/SystemUI/res/drawable/button_ripple_radius.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?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.
-  -->
-<ripple
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:color="?android:attr/colorControlHighlight">
-    <item android:id="@android:id/mask">
-        <shape android:shape="rectangle">
-            <solid android:color="@color/notification_guts_selection_bg" />
-            <corners android:radius="@dimen/rect_button_radius" />
-        </shape>
-    </item>
-</ripple>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/corner_gesture_hint.xml b/packages/SystemUI/res/drawable/corner_gesture_hint.xml
deleted file mode 100644
index 3f4abb0..0000000
--- a/packages/SystemUI/res/drawable/corner_gesture_hint.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<!--
-    Copyright (C) 2019 The Android Open Source Project
-
-    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:height="12dp"
-    android:width="12dp"
-    android:viewportWidth="12"
-    android:viewportHeight="12">
-
-    <path android:fillColor="#00000000"
-          android:pathData="M 1.18 10.65 C 1.18 5.58 5.41 1.18 10.65 1.18"
-          android:strokeColor="#000"
-          android:strokeLineCap="round"
-          android:strokeWidth="1.3" />
-</vector>
diff --git a/packages/SystemUI/res/drawable/ic_emergency_star.xml b/packages/SystemUI/res/drawable/ic_emergency_star.xml
new file mode 100644
index 0000000..148f250
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_emergency_star.xml
@@ -0,0 +1,28 @@
+<?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
+  -->
+
+<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="M9.321,2l0,5.359l-4.641,-2.68l-2.68,4.641l4.641,2.679l-4.641,2.68l2.68,4.641l4.641,-2.68l0,5.359l5.359,0l0,-5.359l4.641,2.68l2.68,-4.641l-4.641,-2.68l4.641,-2.679l-2.68,-4.641l-4.641,2.679l0,-5.359z"
+        android:strokeColor="#00000000"
+        android:fillColor="#EA4335"
+        android:strokeWidth="1"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_notifications_alert.xml b/packages/SystemUI/res/drawable/ic_notifications_alert.xml
index eb7b8ee..d53d698 100644
--- a/packages/SystemUI/res/drawable/ic_notifications_alert.xml
+++ b/packages/SystemUI/res/drawable/ic_notifications_alert.xml
@@ -14,11 +14,11 @@
     limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="24.0dp"
-        android:height="24.0dp"
-        android:viewportWidth="24.0"
-        android:viewportHeight="24.0">
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
     <path
-        android:pathData="M7.58 4.08L6.15 2.65C3.75 4.48 2.17 7.3 2.03 10.5h2c.15-2.65 1.51-4.97 3.55-6.42zm12.39 6.42h2c-.15-3.2-1.73-6.02-4.12-7.85l-1.42 1.43c2.02 1.45 3.39 3.77 3.54 6.42zM18 11c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2v-5zm-6 11c.14 0 .27-.01.4-.04.65-.14 1.18-.58 1.44-1.18.1-.24.15-.5.15-.78h-4c.01 1.1.9 2 2.01 2z"
-        android:fillColor="#FF000000"/>
-</vector>
+        android:fillColor="@android:color/white"
+        android:pathData="M18 17v-6c0-3.07-1.63-5.64-4.5-6.32V4c0-0.83-0.67-1.5-1.5-1.5s-1.5 0.67 -1.5 1.5v0.68C7.64 5.36 6 7.92 6 11v6H4v2h16v-2h-2zm-2 0H8v-6c0-2.48 1.51-4.5 4-4.5s4 2.02 4 4.5v6zm-6 3h4c0 1.1-0.9 2-2 2s-2-0.9-2-2zm12-9h-2c0-2.74-1.23-5.19-3.16-6.84l1.41-1.41C20.54 4.77 22 7.71 22 11zM5.75 2.75l1.41 1.41C5.23 5.81 4 8.26 4 11H2c0-3.29 1.46-6.23 3.75-8.25z" />
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_notifications_silence.xml b/packages/SystemUI/res/drawable/ic_notifications_silence.xml
index ff136eb..a6cc81b 100644
--- a/packages/SystemUI/res/drawable/ic_notifications_silence.xml
+++ b/packages/SystemUI/res/drawable/ic_notifications_silence.xml
@@ -14,15 +14,11 @@
     limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="24.0dp"
-        android:height="24.0dp"
-        android:viewportWidth="24.0"
-        android:viewportHeight="24.0">
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
     <path
-        android:pathData="M0 0h24v24H0z"
-    />
-    <path
-        android:pathData="M20 18.69L7.84 6.14 5.27 3.49 4 4.76l2.8 2.8v.01c-.52.99-.8 2.16-.8 3.42v5l-2 2v1h13.73l2 2L21 19.72l-1-1.03zM12 22c1.11 0 2-.89 2-2h-4c0 1.11.89 2 2 2zm6-7.32V11c0-3.08-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68c-.15.03-.29.08-.42.12-.1.03-.2.07-.3.11h-.01c-.01 0-.01 0-.02.01-.23.09-.46.2-.68.31 0 0-.01 0-.01.01L18 14.68z"
-        android:fillColor="#FF000000"
-    />
-</vector>
+        android:fillColor="@android:color/white"
+        android:pathData="M12 22c1.1 0 2-0.9 2-2h-4c0 1.1 0.9 2 2 2zm4-6L2.81 2.81 1.39 4.22l4.85 4.85C6.09 9.68 6 10.33 6 11v6H4v2h12.17l3.61 3.61 1.41-1.41L16 16zm-8 1l0.01-6.16L14.17 17H8zm4-10.5c2.49 0 4 2.02 4 4.5v2.17l2 2V11c0-3.07-1.63-5.64-4.5-6.32V4c0-0.83-0.67-1.5-1.5-1.5s-1.5 0.67 -1.5 1.5v0.68c-0.78 0.18 -1.45 0.52 -2.04 0.95 L9.93 7.1c0.58-0.37 1.27-0.6 2.07-0.6z" />
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_volume_media.xml b/packages/SystemUI/res/drawable/ic_volume_media.xml
index c8fa3fb..99be7b6 100644
--- a/packages/SystemUI/res/drawable/ic_volume_media.xml
+++ b/packages/SystemUI/res/drawable/ic_volume_media.xml
@@ -22,6 +22,6 @@
 
     <path
         android:fillColor="#FFFFFFFF"
-        android:pathData="M12,3l0.01,10.55c-0.59,-0.34 -1.27,-0.55 -2,-0.55C7.79,13 6,14.79 6,17c0,2.21 1.79,4 4.01,4S14,19.21 14,17V7h4V3H12zM10.01,19c-1.1,0 -2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2C12.01,18.1 11.11,19 10.01,19z"/>
+        android:pathData="M12 3l0.01 10.55c-0.59-0.34-1.27-0.55-2-0.55C7.79 13 6 14.79 6 17s1.79 4 4.01 4S14 19.21 14 17V7h4V3h-6z" />
 
 </vector>
diff --git a/packages/SystemUI/res/drawable/ic_volume_media_bt.xml b/packages/SystemUI/res/drawable/ic_volume_media_bt.xml
index 9f7744e..23cb206 100644
--- a/packages/SystemUI/res/drawable/ic_volume_media_bt.xml
+++ b/packages/SystemUI/res/drawable/ic_volume_media_bt.xml
@@ -14,14 +14,12 @@
      limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24.0dp"
-    android:height="24.0dp"
-    android:viewportWidth="24.0"
-    android:viewportHeight="24.0"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
     android:tint="?android:attr/colorControlNormal">
-
     <path
-        android:fillColor="#FFFFFFFF"
-        android:pathData="M9,3l0.01,10.55C8.41,13.21 7.73,13 7.01,13C4.79,13 3,14.79 3,17c0,2.21 1.79,4 4.01,4S11,19.21 11,17V7h4V3H9zM7.01,19c-1.1,0 -2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2C9.01,18.1 8.11,19 7.01,19zM21,12.43L17.57,9h-0.6v4.55l-2.75,-2.75l-0.85,0.85L16.73,15l-3.35,3.35l0.85,0.85l2.75,-2.75V21h0.6L21,17.57L18.42,15L21,12.43zM18.17,11.3l1.13,1.13l-1.13,1.13V11.3zM19.3,17.57l-1.13,1.13v-2.26L19.3,17.57z"/>
-
-</vector>
+        android:fillColor="@android:color/white"
+        android:pathData="M9 3l0.01 10.55c-0.6-0.34-1.28-0.55-2-0.55C4.79 13 3 14.79 3 17s1.79 4 4.01 4S11 19.21 11 17V7h4V3H9zm12 9.43L17.57 9h-0.6v4.55l-2.75-2.75-0.85 0.85 L16.73 15l-3.35 3.35 0.85 0.85 2.75-2.75V21h0.6L21 17.57 18.42 15 21 12.43zm-2.83-1.13l1.13 1.13-1.13 1.13V11.3zm1.13 6.27l-1.13 1.13v-2.26l1.13 1.13z" />
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_volume_media_bt_mute.xml b/packages/SystemUI/res/drawable/ic_volume_media_bt_mute.xml
index 12e0f2e..2469ddc 100644
--- a/packages/SystemUI/res/drawable/ic_volume_media_bt_mute.xml
+++ b/packages/SystemUI/res/drawable/ic_volume_media_bt_mute.xml
@@ -14,14 +14,12 @@
      limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:height="24dp"
-    android:viewportHeight="24.0"
-    android:viewportWidth="24.0"
     android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
     android:tint="?android:attr/colorControlNormal">
-
     <path
-        android:fillColor="#FFFFFFFF"
-        android:pathData="M9,6.17L9,3h6v4h-4v1.17L9,6.17zM19.42,15L22,17.57l-0.8,0.8l-6.78,-6.78l0.8,-0.8l2.75,2.75V9h0.6L22,12.43L19.42,15zM19.17,13.55l1.13,-1.13l-1.13,-1.13V13.55zM17.21,17.21l3.98,3.98l-1.41,1.41l-3.98,-3.98l-0.58,0.58l-0.85,-0.85l0.58,-0.58L11,13.83V17c0,2.21 -1.78,4 -3.99,4S3,19.21 3,17c0,-2.21 1.79,-4 4.01,-4c0.73,0 1.41,0.21 2,0.55l0,-1.72L1.39,4.22l1.41,-1.41l13.56,13.56L17.21,17.21zM9.01,17c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2c0,1.1 0.9,2 2,2S9.01,18.1 9.01,17z"/>
-
-</vector>
+        android:fillColor="@android:color/white"
+        android:pathData="M9 6.17V3h6v4h-4v1.17l-2-2zM19.42 15L22 17.57l-0.8 0.8 -6.78-6.78 0.8 -0.8 2.75 2.75V9h0.6L22 12.43 19.42 15zm-0.25-1.45l1.13-1.13-1.13-1.13v2.26zm-1.96 3.66l3.98 3.98-1.41 1.41-3.98-3.98-0.58 0.58 -0.85-0.85 0.58 -0.58L11 13.83V17c0 2.21-1.78 4-3.99 4S3 19.21 3 17s1.79-4 4.01-4c0.73 0 1.41 0.21 2 0.55v-1.72L1.39 4.22 2.8 2.81l13.56 13.56 0.85 0.84z" />
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_volume_media_mute.xml b/packages/SystemUI/res/drawable/ic_volume_media_mute.xml
index 45b5b87..3a495963 100644
--- a/packages/SystemUI/res/drawable/ic_volume_media_mute.xml
+++ b/packages/SystemUI/res/drawable/ic_volume_media_mute.xml
@@ -22,9 +22,6 @@
 
     <path
         android:fillColor="#FFFFFFFF"
-        android:pathData="M21.19,21.19L14,14l-2,-2l-9.2,-9.2L1.39,4.22l8.79,8.79c-0.06,0 -0.12,-0.01 -0.18,-0.01C7.79,13 6,14.79 6,17c0,2.21 1.79,4 4.01,4S14,19.21 14,17v-0.17l5.78,5.78L21.19,21.19zM10.01,19c-1.1,0 -2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2C12.01,18.1 11.11,19 10.01,19z"/>
-    <path
-        android:fillColor="#FFffffff"
-        android:pathData="M14,11.17l0,-4.17l4,0l0,-4l-6,0l0,6.17z"/>
+        android:pathData="M21.19 21.19L14 14l-2-2-9.2-9.2-1.41 1.42 8.79 8.79c-0.06 0-0.12-0.01-0.18-0.01-2.21 0-4 1.79-4 4s1.79 4 4.01 4S14 19.21 14 17v-0.17l5.78 5.78 1.41-1.42zM14 11.17V7h4V3h-6v6.17z" />
 
 </vector>
diff --git a/packages/SystemUI/res/drawable/notification_guts_priority_button_bg.xml b/packages/SystemUI/res/drawable/notification_guts_priority_button_bg.xml
new file mode 100644
index 0000000..a0e025f
--- /dev/null
+++ b/packages/SystemUI/res/drawable/notification_guts_priority_button_bg.xml
@@ -0,0 +1,27 @@
+<?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
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle" >
+    <solid
+        android:color="@color/notification_guts_priority_button_bg_fill" />
+
+    <stroke
+        android:width="1dp"
+        android:color="@color/notification_guts_priority_button_bg_stroke" />
+
+    <corners android:radius="@dimen/rect_button_radius" />
+</shape>
diff --git a/packages/SystemUI/res/layout-land/global_actions_column.xml b/packages/SystemUI/res/layout-land/global_actions_column.xml
index 99a4e13..98cb0d0 100644
--- a/packages/SystemUI/res/layout-land/global_actions_column.xml
+++ b/packages/SystemUI/res/layout-land/global_actions_column.xml
@@ -42,10 +42,10 @@
             android:orientation="horizontal"
             android:layout_marginTop="@dimen/global_actions_grid_side_margin"
             android:translationZ="@dimen/global_actions_translate"
-            android:paddingLeft="@dimen/global_actions_grid_horizontal_padding"
-            android:paddingRight="@dimen/global_actions_grid_horizontal_padding"
-            android:paddingTop="@dimen/global_actions_grid_vertical_padding"
-            android:paddingBottom="@dimen/global_actions_grid_vertical_padding"
+            android:paddingLeft="@dimen/global_actions_grid_vertical_padding"
+            android:paddingRight="@dimen/global_actions_grid_vertical_padding"
+            android:paddingTop="@dimen/global_actions_grid_horizontal_padding"
+            android:paddingBottom="@dimen/global_actions_grid_horizontal_padding"
             android:background="?android:attr/colorBackgroundFloating"
         />
         <!-- For separated items-->
@@ -55,10 +55,10 @@
             android:layout_height="wrap_content"
             android:layout_marginLeft="@dimen/global_actions_grid_side_margin"
             android:layout_marginTop="@dimen/global_actions_grid_side_margin"
-            android:paddingLeft="@dimen/global_actions_grid_horizontal_padding"
-            android:paddingRight="@dimen/global_actions_grid_horizontal_padding"
-            android:paddingTop="@dimen/global_actions_grid_vertical_padding"
-            android:paddingBottom="@dimen/global_actions_grid_vertical_padding"
+            android:paddingLeft="@dimen/global_actions_grid_vertical_padding"
+            android:paddingRight="@dimen/global_actions_grid_vertical_padding"
+            android:paddingTop="@dimen/global_actions_grid_horizontal_padding"
+            android:paddingBottom="@dimen/global_actions_grid_horizontal_padding"
             android:orientation="horizontal"
             android:background="?android:attr/colorBackgroundFloating"
             android:translationZ="@dimen/global_actions_translate"
diff --git a/packages/SystemUI/res/layout-land/global_actions_column_seascape.xml b/packages/SystemUI/res/layout-land/global_actions_column_seascape.xml
index 0f86131..5e7604c 100644
--- a/packages/SystemUI/res/layout-land/global_actions_column_seascape.xml
+++ b/packages/SystemUI/res/layout-land/global_actions_column_seascape.xml
@@ -41,10 +41,10 @@
             android:orientation="horizontal"
             android:layout_marginBottom="@dimen/global_actions_grid_side_margin"
             android:translationZ="@dimen/global_actions_translate"
-            android:paddingLeft="@dimen/global_actions_grid_horizontal_padding"
-            android:paddingRight="@dimen/global_actions_grid_horizontal_padding"
-            android:paddingTop="@dimen/global_actions_grid_vertical_padding"
-            android:paddingBottom="@dimen/global_actions_grid_vertical_padding"
+            android:paddingLeft="@dimen/global_actions_grid_vertical_padding"
+            android:paddingRight="@dimen/global_actions_grid_vertical_padding"
+            android:paddingTop="@dimen/global_actions_grid_horizontal_padding"
+            android:paddingBottom="@dimen/global_actions_grid_horizontal_padding"
             android:background="?android:attr/colorBackgroundFloating"
         />
         <!-- For separated items-->
@@ -55,10 +55,10 @@
             android:layout_height="wrap_content"
             android:layout_marginRight="@dimen/global_actions_grid_side_margin"
             android:layout_marginBottom="@dimen/global_actions_grid_side_margin"
-            android:paddingLeft="@dimen/global_actions_grid_horizontal_padding"
-            android:paddingRight="@dimen/global_actions_grid_horizontal_padding"
-            android:paddingTop="@dimen/global_actions_grid_vertical_padding"
-            android:paddingBottom="@dimen/global_actions_grid_vertical_padding"
+            android:paddingLeft="@dimen/global_actions_grid_vertical_padding"
+            android:paddingRight="@dimen/global_actions_grid_vertical_padding"
+            android:paddingTop="@dimen/global_actions_grid_horizontal_padding"
+            android:paddingBottom="@dimen/global_actions_grid_horizontal_padding"
             android:orientation="horizontal"
             android:background="?android:attr/colorBackgroundFloating"
             android:translationZ="@dimen/global_actions_translate"
diff --git a/packages/SystemUI/res/layout/global_actions_grid.xml b/packages/SystemUI/res/layout/global_actions_grid.xml
index 3928062..456553d 100644
--- a/packages/SystemUI/res/layout/global_actions_grid.xml
+++ b/packages/SystemUI/res/layout/global_actions_grid.xml
@@ -1,73 +1,95 @@
 <?xml version="1.0" encoding="utf-8"?>
-<com.android.systemui.globalactions.GlobalActionsGridLayout
+<androidx.constraintlayout.widget.ConstraintLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@id/global_actions_view"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/global_actions_grid_root"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:orientation="horizontal"
-    android:theme="@style/qs_theme"
-    android:gravity="bottom | center_horizontal"
     android:clipChildren="false"
     android:clipToPadding="false"
-    android:paddingBottom="@dimen/global_actions_grid_container_shadow_offset"
     android:layout_marginBottom="@dimen/global_actions_grid_container_negative_shadow_offset"
 >
-    <LinearLayout
+
+    <FrameLayout
+        android:id="@+id/global_actions_panel_container"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/global_actions_view"
+    />
+
+    <com.android.systemui.globalactions.GlobalActionsGridLayout
+        android:id="@id/global_actions_view"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_width="wrap_content"
-        android:layoutDirection="ltr"
+        android:orientation="horizontal"
+        android:theme="@style/qs_theme"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        android:gravity="bottom | center_horizontal"
         android:clipChildren="false"
         android:clipToPadding="false"
-        android:layout_marginBottom="@dimen/global_actions_grid_container_bottom_margin"
+        android:paddingBottom="@dimen/global_actions_grid_container_shadow_offset"
+        android:layout_marginBottom="@dimen/global_actions_grid_container_negative_shadow_offset"
     >
-        <!-- For separated items-->
         <LinearLayout
-            android:id="@+id/separated_button"
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:layout_marginLeft="@dimen/global_actions_grid_side_margin"
-            android:layout_marginRight="@dimen/global_actions_grid_side_margin"
-            android:paddingLeft="@dimen/global_actions_grid_horizontal_padding"
-            android:paddingRight="@dimen/global_actions_grid_horizontal_padding"
-            android:paddingTop="@dimen/global_actions_grid_vertical_padding"
-            android:paddingBottom="@dimen/global_actions_grid_vertical_padding"
-            android:orientation="vertical"
-            android:gravity="center"
-            android:translationZ="@dimen/global_actions_translate"
-        />
-        <!-- Grid of action items -->
-        <com.android.systemui.globalactions.ListGridLayout
-            android:id="@android:id/list"
-            android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:orientation="vertical"
-            android:gravity="right"
-            android:layout_marginRight="@dimen/global_actions_grid_side_margin"
-            android:translationZ="@dimen/global_actions_translate"
-            android:paddingLeft="@dimen/global_actions_grid_horizontal_padding"
-            android:paddingRight="@dimen/global_actions_grid_horizontal_padding"
-            android:paddingTop="@dimen/global_actions_grid_vertical_padding"
-            android:paddingBottom="@dimen/global_actions_grid_vertical_padding"
+            android:layout_width="wrap_content"
+            android:layoutDirection="ltr"
+            android:clipChildren="false"
+            android:clipToPadding="false"
+            android:layout_marginBottom="@dimen/global_actions_grid_container_bottom_margin"
         >
+            <!-- For separated items-->
             <LinearLayout
+                android:id="@+id/separated_button"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:layout_marginLeft="@dimen/global_actions_grid_side_margin"
+                android:layout_marginRight="@dimen/global_actions_grid_side_margin"
+                android:paddingLeft="@dimen/global_actions_grid_horizontal_padding"
+                android:paddingRight="@dimen/global_actions_grid_horizontal_padding"
+                android:paddingTop="@dimen/global_actions_grid_vertical_padding"
+                android:paddingBottom="@dimen/global_actions_grid_vertical_padding"
+                android:orientation="vertical"
+                android:gravity="center"
+                android:translationZ="@dimen/global_actions_translate"
+            />
+            <!-- Grid of action items -->
+            <com.android.systemui.globalactions.ListGridLayout
+                android:id="@android:id/list"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:visibility="gone"
-                android:layoutDirection="locale"
-            />
-            <LinearLayout
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:visibility="gone"
-                android:layoutDirection="locale"
-            />
-            <LinearLayout
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:visibility="gone"
-                android:layoutDirection="locale"
-            />
-        </com.android.systemui.globalactions.ListGridLayout>
-    </LinearLayout>
+                android:orientation="vertical"
+                android:gravity="right"
+                android:layout_marginRight="@dimen/global_actions_grid_side_margin"
+                android:translationZ="@dimen/global_actions_translate"
+                android:paddingLeft="@dimen/global_actions_grid_horizontal_padding"
+                android:paddingRight="@dimen/global_actions_grid_horizontal_padding"
+                android:paddingTop="@dimen/global_actions_grid_vertical_padding"
+                android:paddingBottom="@dimen/global_actions_grid_vertical_padding"
+            >
+                <LinearLayout
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:visibility="gone"
+                    android:layoutDirection="locale"
+                />
+                <LinearLayout
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:visibility="gone"
+                    android:layoutDirection="locale"
+                />
+                <LinearLayout
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:visibility="gone"
+                    android:layoutDirection="locale"
+                />
+            </com.android.systemui.globalactions.ListGridLayout>
+        </LinearLayout>
 
-</com.android.systemui.globalactions.GlobalActionsGridLayout>
+    </com.android.systemui.globalactions.GlobalActionsGridLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/invocation_lights.xml b/packages/SystemUI/res/layout/invocation_lights.xml
new file mode 100644
index 0000000..ff78670
--- /dev/null
+++ b/packages/SystemUI/res/layout/invocation_lights.xml
@@ -0,0 +1,23 @@
+<?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
+  -->
+
+<com.android.systemui.assist.ui.InvocationLightsView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_gravity="bottom"
+    android:visibility="gone"/>
diff --git a/packages/SystemUI/res/layout/notification_info.xml b/packages/SystemUI/res/layout/notification_info.xml
index 4b672ee0..87de9d4 100644
--- a/packages/SystemUI/res/layout/notification_info.xml
+++ b/packages/SystemUI/res/layout/notification_info.xml
@@ -219,106 +219,127 @@
             android:gravity="center"
             android:orientation="vertical">
 
-            <RelativeLayout
+            <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
                 android:id="@+id/alert"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:padding="@dimen/notification_importance_button_padding"
                 android:clickable="true"
-                android:focusable="true">
-                <ImageView
-                    android:id="@+id/alert_icon"
-                    android:src="@drawable/ic_notifications_alert"
-                    android:background="@android:color/transparent"
-                    android:layout_gravity="center"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:clickable="false"
-                    android:focusable="false"/>
-                <TextView
-                    android:id="@+id/alert_label"
+                android:focusable="true"
+                android:background="@drawable/notification_guts_priority_button_bg"
+                android:orientation="vertical">
+                <LinearLayout
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
-                    android:ellipsize="end"
-                    android:maxLines="1"
-                    android:clickable="false"
-                    android:focusable="false"
-                    android:layout_toEndOf="@id/alert_icon"
-                    android:layout_marginStart="@dimen/notification_importance_drawable_padding"
-                    android:textAppearance="@style/TextAppearance.NotificationImportanceButton.Unselected"
-                    android:text="@string/notification_alert_title"/>
+                    android:orientation="horizontal"
+                    android:gravity="center"
+                    >
+                    <ImageView
+                        android:id="@+id/alert_icon"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:src="@drawable/ic_notifications_alert"
+                        android:background="@android:color/transparent"
+                        android:tint="@color/notification_guts_priority_contents"
+                        android:clickable="false"
+                        android:focusable="false"/>
+                    <TextView
+                        android:id="@+id/alert_label"
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content"
+                        android:layout_marginStart="@dimen/notification_importance_drawable_padding"
+                        android:layout_weight="1"
+                        android:ellipsize="end"
+                        android:maxLines="1"
+                        android:clickable="false"
+                        android:focusable="false"
+                        android:textAppearance="@style/TextAppearance.NotificationImportanceButton"
+                        android:text="@string/notification_alert_title"/>
+                </LinearLayout>
                 <TextView
                     android:id="@+id/alert_summary"
-                    android:visibility="gone"
-                    android:paddingTop="@dimen/notification_importance_button_padding"
-                    android:text="@string/notification_channel_summary_default"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
+                    android:layout_marginTop="@dimen/notification_importance_button_description_top_margin"
+                    android:visibility="gone"
+                    android:text="@string/notification_channel_summary_default"
                     android:clickable="false"
                     android:focusable="false"
                     android:ellipsize="end"
                     android:maxLines="2"
-                    android:layout_below="@id/alert_icon"
                     android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
-            </RelativeLayout>
+            </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
 
-            <RelativeLayout
+            <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
                 android:id="@+id/silence"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:padding="@dimen/notification_importance_button_padding"
                 android:layout_marginTop="@dimen/notification_importance_button_separation"
+                android:padding="@dimen/notification_importance_button_padding"
                 android:clickable="true"
-                android:focusable="true">
-                <ImageView
-                    android:id="@+id/silence_icon"
-                    android:src="@drawable/ic_notifications_silence"
-                    android:background="@android:color/transparent"
-                    android:layout_gravity="center"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:clickable="false"
-                    android:focusable="false"/>
-                <TextView
-                    android:id="@+id/silence_label"
+                android:focusable="true"
+                android:background="@drawable/notification_guts_priority_button_bg"
+                android:orientation="vertical">
+                <LinearLayout
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
-                    android:ellipsize="end"
-                    android:maxLines="1"
-                    android:clickable="false"
-                    android:focusable="false"
-                    android:layout_toEndOf="@id/silence_icon"
-                    android:layout_marginStart="@dimen/notification_importance_drawable_padding"
-                    android:textAppearance="@style/TextAppearance.NotificationImportanceButton.Unselected"
-                    android:text="@string/notification_silence_title"/>
+                    android:orientation="horizontal"
+                    android:gravity="center"
+                    >
+                    <ImageView
+                        android:id="@+id/silence_icon"
+                        android:src="@drawable/ic_notifications_silence"
+                        android:background="@android:color/transparent"
+                        android:tint="@color/notification_guts_priority_contents"
+                        android:layout_gravity="center"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:clickable="false"
+                        android:focusable="false"/>
+                    <TextView
+                        android:id="@+id/silence_label"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:ellipsize="end"
+                        android:maxLines="1"
+                        android:clickable="false"
+                        android:focusable="false"
+                        android:layout_toEndOf="@id/silence_icon"
+                        android:layout_marginStart="@dimen/notification_importance_drawable_padding"
+                        android:textAppearance="@style/TextAppearance.NotificationImportanceButton"
+                        android:text="@string/notification_silence_title"/>
+                </LinearLayout>
                 <TextView
                     android:id="@+id/silence_summary"
-                    android:visibility="gone"
-                    android:paddingTop="@dimen/notification_importance_button_padding"
-                    android:text="@string/notification_channel_summary_default"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
+                    android:layout_marginTop="@dimen/notification_importance_button_description_top_margin"
+                    android:visibility="gone"
+                    android:text="@string/notification_channel_summary_low"
                     android:clickable="false"
                     android:focusable="false"
                     android:ellipsize="end"
                     android:maxLines="2"
-                    android:layout_below="@id/silence_icon"
                     android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
-            </RelativeLayout>
+            </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
 
         </LinearLayout>
 
         <RelativeLayout
             android:id="@+id/bottom_buttons"
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="@dimen/notification_guts_button_spacing" >
+            android:layout_height="60dp"
+            android:gravity="center_vertical"
+            android:paddingStart="4dp"
+            android:paddingEnd="4dp"
+            >
             <TextView
                 android:id="@+id/turn_off_notifications"
                 android:text="@string/inline_turn_off_notifications"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_alignParentStart="true"
+                android:gravity="start|center_vertical"
                 android:minWidth="@dimen/notification_importance_toggle_size"
                 android:minHeight="@dimen/notification_importance_toggle_size"
                 android:maxWidth="200dp"
@@ -329,10 +350,10 @@
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_alignParentEnd="true"
-                android:gravity="center_vertical|end"
-                android:maxWidth="125dp"
+                android:gravity="end|center_vertical"
                 android:minWidth="@dimen/notification_importance_toggle_size"
                 android:minHeight="@dimen/notification_importance_toggle_size"
+                android:maxWidth="125dp"
                 style="@style/TextAppearance.NotificationInfo.Button"/>
         </RelativeLayout>
 
diff --git a/packages/SystemUI/res/layout/quick_settings_header_info.xml b/packages/SystemUI/res/layout/quick_settings_header_info.xml
index c812489..683e867 100644
--- a/packages/SystemUI/res/layout/quick_settings_header_info.xml
+++ b/packages/SystemUI/res/layout/quick_settings_header_info.xml
@@ -21,6 +21,7 @@
     android:layout_below="@id/quick_status_bar_system_icons"
     android:paddingStart="@dimen/status_bar_padding_start"
     android:paddingEnd="@dimen/status_bar_padding_end"
+    android:visibility="invisible"
     android:theme="@style/QSHeaderTheme">
 
         <com.android.systemui.qs.QSHeaderInfoLayout
diff --git a/packages/SystemUI/res/layout/rounded_corners.xml b/packages/SystemUI/res/layout/rounded_corners.xml
index 02651a2..b409c8f 100644
--- a/packages/SystemUI/res/layout/rounded_corners.xml
+++ b/packages/SystemUI/res/layout/rounded_corners.xml
@@ -18,36 +18,30 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
+    <com.android.systemui.CornerHandleView
+        android:id="@+id/assist_hint_left"
+        android:layout_width="36dp"
+        android:layout_height="36dp"
+        android:layout_gravity="left|top"
+        android:visibility="gone"/>
+    <com.android.systemui.CornerHandleView
+        android:id="@+id/assist_hint_right"
+        android:layout_width="36dp"
+        android:layout_height="36dp"
+        android:layout_gravity="right|bottom"
+        android:visibility="gone"/>
     <ImageView
         android:id="@+id/left"
         android:layout_width="12dp"
         android:layout_height="12dp"
         android:layout_gravity="left|top"
         android:tint="#ff000000"
-        android:src="@drawable/rounded" />
+        android:src="@drawable/rounded"/>
     <ImageView
         android:id="@+id/right"
         android:layout_width="12dp"
         android:layout_height="12dp"
         android:tint="#ff000000"
         android:layout_gravity="right|bottom"
-        android:src="@drawable/rounded" />
-    <ImageView
-        android:id="@+id/assist_hint_left"
-        android:layout_width="32dp"
-        android:layout_height="32dp"
-        android:padding="6dp"
-        android:layout_gravity="left|top"
-        android:src="@drawable/corner_gesture_hint"
-        android:tint="#ffffffff"
-        android:visibility="gone" />
-    <ImageView
-        android:id="@+id/assist_hint_right"
-        android:layout_width="32dp"
-        android:layout_height="32dp"
-        android:padding="6dp"
-        android:layout_gravity="right|bottom"
-        android:src="@drawable/corner_gesture_hint"
-        android:tint="#ffffffff"
-        android:visibility="gone" />
+        android:src="@drawable/rounded"/>
 </com.android.systemui.RegionInterceptingFrameLayout>
diff --git a/packages/SystemUI/res/layout/start_contextual.xml b/packages/SystemUI/res/layout/start_contextual.xml
deleted file mode 100644
index e022c73..0000000
--- a/packages/SystemUI/res/layout/start_contextual.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-             xmlns:systemui="http://schemas.android.com/apk/res-auto"
-             android:id="@+id/start_menu_container"
-             android:layout_width="@dimen/navigation_key_width"
-             android:layout_height="match_parent"
-             android:importantForAccessibility="no"
-             android:focusable="false"
-             android:clipChildren="false"
-             android:clipToPadding="false"
-             >
-    <include layout="@layout/rotate_suggestion"
-             android:layout_width="match_parent"
-             android:layout_height="match_parent"
-             android:visibility="invisible"
-    />
-    <include layout="@layout/back"
-             android:layout_width="match_parent"
-             android:layout_height="match_parent"
-             android:visibility="invisible"
-    />
-</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/status_bar_notification_section_header.xml b/packages/SystemUI/res/layout/status_bar_notification_section_header.xml
index 0c3c597..d3eb9ae 100644
--- a/packages/SystemUI/res/layout/status_bar_notification_section_header.xml
+++ b/packages/SystemUI/res/layout/status_bar_notification_section_header.xml
@@ -44,6 +44,8 @@
             android:layout_height="wrap_content"
             android:layout_weight="1"
             android:layout_marginStart="@dimen/notification_section_header_padding_left"
+            android:gravity="start"
+            android:textAlignment="gravity"
             android:text="@string/notification_section_header_gentle"
             android:textSize="12sp"
             android:textColor="@color/notification_section_header_label_color"
diff --git a/packages/SystemUI/res/values-mcc310-mnc410/config.xml b/packages/SystemUI/res/values-mcc310-mnc410/config.xml
deleted file mode 100644
index 26b9192..0000000
--- a/packages/SystemUI/res/values-mcc310-mnc410/config.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** 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.
-*/
--->
-
-<!-- These resources are around just to allow their values to be customized
-     for different hardware and product builds. -->
-<resources>
-    <!-- Enable 5 bar signal strength icon -->
-    <bool name="config_inflateSignalStrength">true</bool>
-</resources>
-
diff --git a/packages/SystemUI/res/values-mcc311-mnc480/config.xml b/packages/SystemUI/res/values-mcc311-mnc480/config.xml
deleted file mode 100644
index 7dadae7..0000000
--- a/packages/SystemUI/res/values-mcc311-mnc480/config.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 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.
-*/
--->
-
-<!-- These resources are around just to allow their values to be customized
-     for different hardware and product builds. -->
-<resources>
-    <!-- Enable 5 bar signal strength icon -->
-    <bool name="config_inflateSignalStrength">true</bool>
-</resources>
-
diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml
index addc10a..7b55d18 100644
--- a/packages/SystemUI/res/values-night/colors.xml
+++ b/packages/SystemUI/res/values-night/colors.xml
@@ -31,7 +31,10 @@
     <color name="notification_divider_color">#212121</color>
 
     <!-- The background color of the notification shade -->
-    <color name="notification_shade_background_color">#181818</color>
+    <color name="notification_shade_background_color">@color/GM2_grey_900</color>
+
+    <!-- The color of the gear shown behind a notification -->
+    <color name="notification_gear_color">@color/GM2_grey_500</color>
 
     <!-- The color of the ripples on the untinted notifications -->
     <color name="notification_ripple_untinted_color">#30ffffff</color>
@@ -42,12 +45,16 @@
     <!-- The color of the text inside a notification -->
     <color name="notification_primary_text_color">@*android:color/notification_primary_text_color_dark</color>
 
-    <color name="notification_guts_selection_bg">@color/GM2_grey_800</color>
-    <color name="notification_guts_selection_border">@color/GM2_grey_700</color>
     <color name="notification_guts_link_icon_tint">@color/GM2_grey_500</color>
-    <color name="notification_guts_sub_text_color">@color/GM2_grey_200</color>
+    <color name="notification_guts_sub_text_color">@color/GM2_grey_300</color>
     <color name="notification_guts_header_text_color">@color/GM2_grey_200</color>
-    <color name="notification_guts_button_color">@color/GM2_blue_200</color>
+    <color name="notification_guts_info_button_color">@color/GM2_blue_300</color>
+    <color name="notification_guts_priority_button_content_color">@color/GM2_grey_500</color>
+    <color name="notification_guts_priority_button_content_color_selected">@color/GM2_blue_300</color>
+    <color name="notification_guts_priority_button_bg_fill_color">@color/transparent</color>
+    <color name="notification_guts_priority_button_bg_fill_color_selected">@color/GM2_grey_800</color>
+    <color name="notification_guts_priority_button_bg_stroke_color">@color/GM2_grey_700</color>
+    <color name="notification_guts_priority_button_bg_stroke_color_selected">@color/GM2_blue_300</color>
 
     <color name="notification_section_header_label_color">@color/GM2_grey_200</color>
     <color name="notification_section_clear_all_btn_color">@color/GM2_grey_500</color>
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index f415661..e7a1a66 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -74,7 +74,7 @@
     <color name="notification_divider_color">#FF616161</color>
 
     <!-- The background color of the notification shade -->
-    <color name="notification_shade_background_color">#ffeeeeee</color>
+    <color name="notification_shade_background_color">@color/GM2_grey_200</color>
 
     <!-- The color of the ripples on the untinted notifications -->
     <color name="notification_ripple_untinted_color">#28000000</color>
@@ -83,7 +83,7 @@
     <color name="notification_ripple_tinted_color">#30ffffff</color>
 
     <!-- The color of the gear shown behind a notification -->
-    <color name="notification_gear_color">#ff757575</color>
+    <color name="notification_gear_color">@color/GM2_grey_700</color>
 
     <!-- The color of the text inside a notification -->
     <color name="notification_primary_text_color">@*android:color/notification_primary_text_color_light</color>
@@ -91,14 +91,18 @@
     <!-- The "inside" of a notification, reached via longpress -->
     <color name="notification_guts_bg_color">@color/GM2_grey_50</color>
 
-    <color name="notification_guts_selection_bg">#FFFFFF</color>
-    <color name="notification_guts_selection_border">#4285F4</color>
     <color name="notification_guts_link_icon_tint">@color/GM2_grey_700</color>
     <color name="notification_guts_sub_text_color">@color/GM2_grey_700</color>
     <color name="notification_guts_header_text_color">@color/GM2_grey_900</color>
     <color name="notification_silence_color">#FF32c1de</color>
     <color name="notification_alert_color">#FFF87B2B</color>
-    <color name="notification_guts_button_color">@color/GM2_blue_700</color>
+    <color name="notification_guts_info_button_color">@color/GM2_blue_700</color>
+    <color name="notification_guts_priority_button_content_color">@color/GM2_grey_700</color>
+    <color name="notification_guts_priority_button_content_color_selected">@color/GM2_blue_700</color>
+    <color name="notification_guts_priority_button_bg_fill_color">@color/transparent</color>
+    <color name="notification_guts_priority_button_bg_fill_color_selected">#FFFFFF</color>
+    <color name="notification_guts_priority_button_bg_stroke_color">@color/GM2_grey_300</color>
+    <color name="notification_guts_priority_button_bg_stroke_color_selected">@color/GM2_blue_600</color>
 
     <color name="notification_section_header_label_color">@color/GM2_grey_900</color>
     <color name="notification_section_clear_all_btn_color">@color/GM2_grey_700</color>
@@ -162,14 +166,17 @@
     <color name="smart_reply_button_stroke">#ffdadce0</color>
 
     <!-- Biometric dialog colors -->
-    <color name="biometric_dialog_dim_color">#80000000</color> <!-- 50% black -->
+    <color name="biometric_dialog_dim_color">#80000000</color>              <!-- 50% black -->
     <color name="biometric_dialog_gray">#ff757575</color>
-    <color name="biometric_dialog_accent">#ff008577</color> <!-- dark teal -->
-    <color name="biometric_dialog_error">#ffd93025</color> <!-- red 600 -->
+    <color name="biometric_dialog_accent">#ff008577</color>                 <!-- dark teal -->
+    <color name="biometric_dialog_error">#ffd93025</color>                  <!-- red 600 -->
 
     <!-- Logout button -->
     <color name="logout_button_bg_color">#ccffffff</color>
 
+    <!-- Color for the Assistant invocation lights -->
+    <color name="default_invocation_lights_color">#ffffffff</color>         <!-- white -->
+
     <!-- GM2 colors -->
     <color name="GM2_grey_50">#F8F9FA</color>
     <color name="GM2_grey_100">#F1F3F4</color>
@@ -188,6 +195,8 @@
     <color name="GM2_red_700">#C5221F</color>
 
     <color name="GM2_blue_200">#AECBFA</color>
+    <color name="GM2_blue_300">#8AB4F8</color>
+    <color name="GM2_blue_600">#1A73E8</color>
     <color name="GM2_blue_700">#1967D2</color>
 
     <color name="GM2_yellow_500">#FFFBBC04</color>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index d774c55..19e7b73 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -331,7 +331,7 @@
     <!-- Nav bar button default ordering/layout -->
     <string name="config_navBarLayout" translatable="false">left[.5W],back[1WC];home;recent[1WC],right[.5W]</string>
     <string name="config_navBarLayoutQuickstep" translatable="false">back[1.7WC];home;contextual[1.7WC]</string>
-    <string name="config_navBarLayoutHandle" translatable="false">start_contextual[40AC];home_handle;ime_switcher[40AC]</string>
+    <string name="config_navBarLayoutHandle" translatable="false">back[40AC];home_handle;ime_switcher[40AC]</string>
 
     <bool name="quick_settings_show_full_alarm">false</bool>
 
@@ -417,10 +417,6 @@
          it has been expanded to reveal its children. -->
     <bool name="config_showGroupNotificationBgWhenExpanded">false</bool>
 
-    <!-- Whether to artificially interpret all signal strengths as
-         one bar higher than they actually are -->
-    <bool name="config_inflateSignalStrength">false</bool>
-
     <!-- Should we vibrate on an icon animation of the shelf. This should only be active if the
      vibrator is capable of subtle vibrations -->
     <bool name="config_vibrateOnIconAnimation">false</bool>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index b27836d..c5e4662 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -38,7 +38,6 @@
     <dimen name="navigation_handle_sample_horizontal_margin">10dp</dimen>
     <dimen name="navigation_home_handle_width">72dp</dimen>
 
-
     <!-- Size of the nav bar edge panels, should be greater to the
          edge sensitivity + the drag threshold -->
     <dimen name="navigation_edge_panel_width">70dp</dimen>
@@ -57,6 +56,9 @@
     <!-- Luminance change threshold that allows applying new value if difference was exceeded -->
     <item name="navigation_luminance_change_threshold" type="dimen" format="float">0.05</item>
 
+    <dimen name="floating_rotation_button_diameter">40dp</dimen>
+    <dimen name="floating_rotation_button_min_margin">4dp</dimen>
+
     <!-- Height of notification icons in the status bar -->
     <dimen name="status_bar_icon_size">@*android:dimen/status_bar_icon_size</dimen>
 
@@ -218,7 +220,7 @@
     <dimen name="notification_guts_option_horizontal_padding">15dp</dimen>
 
     <!-- The vertical space between items in the alert selections in the inline settings -->
-    <dimen name="notification_guts_option_vertical_padding">20dp</dimen>
+    <dimen name="notification_guts_option_vertical_padding">16dp</dimen>
 
     <!-- The vertical space between the alert selections in the inline settings -->
     <dimen name="notification_guts_option_vertical_margin">6dp</dimen>
@@ -227,7 +229,7 @@
     <dimen name="notification_importance_toggle_marginTop">28dp</dimen>
     <dimen name="notification_importance_toggle_marginBottom">28dp</dimen>
     <dimen name="notification_importance_text_marginTop">20dp</dimen>
-    <dimen name="notification_importance_button_separation">16dp</dimen>
+    <dimen name="notification_importance_button_separation">8dp</dimen>
     <dimen name="notification_importance_button_width">178dp</dimen>
     <dimen name="notification_importance_button_horiz_padding">28dp</dimen>
     <dimen name="notification_importance_drawable_padding">8dp</dimen>
@@ -236,8 +238,9 @@
     <dimen name="notification_importance_description_text">14sp</dimen>
     <dimen name="notification_importance_channel_text">16sp</dimen>
     <dimen name="notification_importance_channel_group_text">14sp</dimen>
-    <dimen name="notification_importance_button_text">16sp</dimen>
-    <dimen name="notification_importance_button_padding">14dp</dimen>
+    <dimen name="notification_importance_button_text">14sp</dimen>
+    <dimen name="notification_importance_button_padding">16dp</dimen>
+    <dimen name="notification_importance_button_description_top_margin">12dp</dimen>
     <dimen name="rect_button_radius">8dp</dimen>
 
     <!-- The minimum height for the snackbar shown after the snooze option has been chosen. -->
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 9831250..3fe2492 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -495,6 +495,9 @@
     <!-- Content description of the battery level icon for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
     <string name="accessibility_battery_level">Battery <xliff:g id="number">%d</xliff:g> percent.</string>
 
+    <!-- Content description of the battery level icon for accessibility, including the estimated time remaining before the phone runs out of battery (not shown on the screen). [CHAR LIMIT=NONE] -->
+    <string name="accessibility_battery_level_with_estimate">Battery <xliff:g id="percentage" example="95%">%1$s</xliff:g> percent, about <xliff:g id="time" example="Until 3:15pm">%2$s</xliff:g> left based on your usage</string>
+
     <!-- Content description of the battery level icon for accessibility while the device is charging (not shown on the screen). [CHAR LIMIT=NONE] -->
     <string name="accessibility_battery_level_charging">Battery charging, <xliff:g id="battery_percentage">%d</xliff:g> percent.</string>
 
@@ -1677,25 +1680,16 @@
     <string name="inline_keep_showing_app">Keep showing notifications from this app?</string>
 
     <!-- [CHAR LIMIT=100] Notification Importance title -->
-    <string name="notification_silence_title">Gentle</string>
+    <string name="notification_silence_title">Silent</string>
 
     <!-- [CHAR LIMIT=100] Notification Importance title -->
-    <string name="notification_alert_title">Prioritized</string>
+    <string name="notification_alert_title">Alerting</string>
 
     <!-- [CHAR LIMIT=150] Notification Importance title: low importance level summary -->
-    <string name="notification_channel_summary_low">Helps you focus with notifications only in the pull-down shade. Always silent.</string>
-
-    <!-- [CHAR LIMIT=150] Notification Importance title: low importance level summary -->
-    <string name="notification_channel_summary_low_status">Displays below priority notifications. Always silent.</string>
-
-    <!-- [CHAR LIMIT=150] Notification Importance title: low importance level summary -->
-    <string name="notification_channel_summary_low_lock">Displays below priority notifications. Always silent.</string>
-
-    <!-- [CHAR LIMIT=150] Notification Importance title: low importance level summary -->
-    <string name="notification_channel_summary_low_status_lock">Displays below priority notifications. Always silent.</string>
+    <string name="notification_channel_summary_low">Helps you focus without sound or vibration.</string>
 
     <!-- [CHAR LIMIT=150] Notification Importance title: normal importance level summary -->
-    <string name="notification_channel_summary_default">Gets your attention with sound &amp; a status bar icon. Shows on lock screen.</string>
+    <string name="notification_channel_summary_default">Gets your attention with sound or vibration.</string>
 
     <!-- Notification: Control panel: Label that displays when the app's notifications cannot be blocked. -->
     <string name="notification_unblockable_desc">These notifications can\'t be modified.</string>
@@ -2468,4 +2462,11 @@
     <string name="bubble_accessibility_action_move_bottom_right">Move bottom right</string>
     <!-- Text used for the bubble dismiss area. Bubbles dragged to, or flung towards, this area will go away. [CHAR LIMIT=20] -->
     <string name="bubble_dismiss_text">Dismiss</string>
+
+    <!-- Notification content text when the system navigation mode changes as a result of changing the default launcher [CHAR LIMIT=NONE] -->
+    <string name="notification_content_system_nav_changed">System navigation updated. To make changes, go to Settings.</string>
+
+    <!-- Notification content text when switching to a default launcher that supports gesture navigation [CHAR LIMIT=NONE] -->
+    <string name="notification_content_gesture_nav_available">Go to Settings to update system navigation</string>
+
 </resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index a2b9299..b387793 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -460,9 +460,9 @@
     </style>
 
     <style name="TextAppearance.NotificationInfo.Button">
-        <item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item>
-        <item name="android:textSize">16sp</item>
-        <item name="android:textColor">@color/notification_guts_button_color</item>
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
+        <item name="android:textSize">14sp</item>
+        <item name="android:textColor">@color/notification_guts_info_button_color</item>
         <item name="android:background">@drawable/btn_borderless_rect</item>
         <item name="android:gravity">center_vertical</item>
         <item name="android:focusable">true</item>
@@ -470,21 +470,21 @@
 
     <style name="TextAppearance.NotificationImportanceChannel">
         <item name="android:textSize">@dimen/notification_importance_channel_text</item>
-        <item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item>
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
         <item name="android:textColor">@color/notification_guts_header_text_color</item>
         <item name="android:textSize">@dimen/notification_importance_channel_text</item>
     </style>
 
     <style name="TextAppearance.NotificationImportanceChannelGroup">
         <item name="android:textSize">@dimen/notification_importance_channel_group_text</item>
-        <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
         <item name="android:textColor">@color/notification_guts_sub_text_color</item>
         <item name="android:textSize">@dimen/notification_importance_channel_group_text</item>
     </style>
 
     <style name="TextAppearance.NotificationImportanceHeader">
         <item name="android:textSize">@dimen/notification_importance_header_text</item>
-        <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
         <item name="android:textColor">@color/notification_guts_header_text_color</item>
     </style>
 
@@ -497,18 +497,11 @@
 
     <style name="TextAppearance.NotificationImportanceButton">
         <item name="android:textSize">@dimen/notification_importance_button_text</item>
-        <item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item>
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
+        <item name="android:textColor">@color/notification_guts_priority_contents</item>
         <item name="android:gravity">center</item>
     </style>
 
-    <style name="TextAppearance.NotificationImportanceButton.Selected" parent="TextAppearance.NotificationImportanceButton">
-        <item name="android:textColor">?android:attr/colorAccent</item>
-    </style>
-
-    <style name="TextAppearance.NotificationImportanceButton.Unselected" parent="TextAppearance.NotificationImportanceButton">
-        <item name="android:textColor">?android:attr/textColorPrimary</item>
-    </style>
-
     <style name="TextAppearance.HeadsUpStatusBarText"
            parent="@*android:style/TextAppearance.DeviceDefault.Notification.Info">
     </style>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputChannelCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputChannelCompat.java
index 6567b6a..2b1fce8 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputChannelCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputChannelCompat.java
@@ -18,7 +18,6 @@
 
 import android.os.Bundle;
 import android.os.Looper;
-import android.util.Pair;
 import android.view.BatchedInputEventReceiver;
 import android.view.Choreographer;
 import android.view.InputChannel;
@@ -42,19 +41,6 @@
     }
 
     /**
-     * Creates a dispatcher and receiver pair to better handle events across threads.
-     */
-    public static Pair<InputEventDispatcher, InputEventReceiver> createPair(String name,
-            Looper looper, Choreographer choreographer, InputEventListener listener) {
-        InputChannel[] channels = InputChannel.openInputChannelPair(name);
-
-        InputEventDispatcher dispatcher = new InputEventDispatcher(channels[0], looper);
-        InputEventReceiver receiver = new InputEventReceiver(channels[1], looper, choreographer,
-                listener);
-        return Pair.create(dispatcher, receiver);
-    }
-
-    /**
      * Creates a dispatcher from the extras received as part on onInitialize
      */
     public static InputEventReceiver fromBundle(Bundle params, String key,
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SystemGestureExclusionListenerCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/SystemGestureExclusionListenerCompat.java
new file mode 100644
index 0000000..9fdecfb
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/SystemGestureExclusionListenerCompat.java
@@ -0,0 +1,85 @@
+/**
+ * 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.shared.system;
+
+import android.graphics.Region;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.ISystemGestureExclusionListener;
+import android.view.WindowManagerGlobal;
+
+/**
+ * Utility class to listen for exclusion rect changes.
+ */
+public abstract class SystemGestureExclusionListenerCompat {
+
+    private static final String TAG = "SGEListenerCompat";
+
+    private final int mDisplayId;
+
+    private ISystemGestureExclusionListener mGestureExclusionListener =
+            new ISystemGestureExclusionListener.Stub() {
+                @Override
+                public void onSystemGestureExclusionChanged(int displayId,
+                        Region systemGestureExclusion) {
+                    if (displayId == mDisplayId) {
+                        onExclusionChanged(systemGestureExclusion);
+                    }
+                }
+            };
+    private boolean mRegistered;
+
+    public SystemGestureExclusionListenerCompat(int displayId) {
+        mDisplayId = displayId;
+    }
+
+    /**
+     * Called when the exclusion region has changed
+     */
+    public abstract void onExclusionChanged(Region systemGestureExclusion);
+
+    /**
+     * Registers the listener for getting exclusion rect changes.
+     */
+    public void register() {
+        if (!mRegistered) {
+            try {
+                WindowManagerGlobal.getWindowManagerService()
+                        .registerSystemGestureExclusionListener(
+                                mGestureExclusionListener, mDisplayId);
+                mRegistered = true;
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed to register window manager callbacks", e);
+            }
+        }
+    }
+
+    /**
+     * Unregisters the receiver if previously registered
+     */
+    public void unregister() {
+        if (mRegistered) {
+            try {
+                WindowManagerGlobal.getWindowManagerService()
+                        .unregisterSystemGestureExclusionListener(
+                                mGestureExclusionListener, mDisplayId);
+                mRegistered = false;
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed to unregister window manager callbacks", e);
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java
index de4c798..f66463e 100644
--- a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java
+++ b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java
@@ -322,9 +322,6 @@
         mCharging = pluggedIn;
         mLevel = level;
         updatePercentText();
-        setContentDescription(
-                getContext().getString(charging ? R.string.accessibility_battery_level_charging
-                        : R.string.accessibility_battery_level, level));
     }
 
     @Override
@@ -358,6 +355,9 @@
                 mBatteryController.getEstimatedTimeRemainingString((String estimate) -> {
                     if (estimate != null) {
                         mBatteryPercentView.setText(estimate);
+                        setContentDescription(getContext().getString(
+                                R.string.accessibility_battery_level_with_estimate,
+                                mLevel, estimate));
                     } else {
                         setPercentTextAtCurrentLevel();
                     }
@@ -371,6 +371,9 @@
     private void setPercentTextAtCurrentLevel() {
         mBatteryPercentView.setText(
                 NumberFormat.getPercentInstance().format(mLevel / 100f));
+        setContentDescription(
+                getContext().getString(mCharging ? R.string.accessibility_battery_level_charging
+                        : R.string.accessibility_battery_level, mLevel));
     }
 
     private void updateShowPercent() {
diff --git a/packages/SystemUI/src/com/android/systemui/CornerHandleView.java b/packages/SystemUI/src/com/android/systemui/CornerHandleView.java
new file mode 100644
index 0000000..691c3f9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/CornerHandleView.java
@@ -0,0 +1,160 @@
+/*
+ * 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;
+
+import android.animation.ArgbEvaluator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.os.SystemProperties;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.ContextThemeWrapper;
+import android.view.View;
+
+import com.android.settingslib.Utils;
+
+/**
+ * CornerHandleView draws an inset arc intended to be displayed within the screen decoration
+ * corners.
+ */
+public class CornerHandleView extends View {
+    private static final boolean ALLOW_TUNING = false;
+    private static final int ANGLE_DEGREES = 50;
+    private static final float STROKE_DP = 2.5f;
+    // Radius to use if none is available.
+    private static final int FALLBACK_RADIUS_DP = 15;
+
+    private Paint mPaint;
+    private int mLightColor;
+    private int mDarkColor;
+    private RectF mOval;
+
+
+    public CornerHandleView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        mPaint = new Paint();
+        mPaint.setAntiAlias(true);
+        mPaint.setStyle(Paint.Style.STROKE);
+        mPaint.setStrokeCap(Paint.Cap.ROUND);
+        mPaint.setStrokeWidth(getStrokePx());
+
+        final int dualToneDarkTheme = Utils.getThemeAttr(mContext,
+                R.attr.darkIconTheme);
+        final int dualToneLightTheme = Utils.getThemeAttr(mContext,
+                R.attr.lightIconTheme);
+        Context lightContext = new ContextThemeWrapper(mContext, dualToneLightTheme);
+        Context darkContext = new ContextThemeWrapper(mContext, dualToneDarkTheme);
+        mLightColor = Utils.getColorAttrDefaultColor(lightContext,
+                R.attr.singleToneColor);
+        mDarkColor = Utils.getColorAttrDefaultColor(darkContext,
+                R.attr.singleToneColor);
+
+        updateOval();
+    }
+
+    /**
+     * Receives an intensity from 0 (lightest) to 1 (darkest) and sets the handle color
+     * appropriately. Intention is to match the home handle color.
+     */
+    public void updateDarkness(float darkIntensity) {
+        mPaint.setColor((int) ArgbEvaluator.getInstance().evaluate(darkIntensity,
+                mLightColor,
+                mDarkColor));
+        invalidate();
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        if (ALLOW_TUNING) {
+            mPaint.setStrokeWidth(getStrokePx());
+            updateOval();
+        }
+
+        canvas.drawArc(mOval, 180 + ((90 - getAngle()) / 2), getAngle(), false,
+                mPaint);
+    }
+
+    // TODO(b/133834204): Remove tweaking of corner handles
+    private static float convertDpToPixel(float dp, Context context) {
+        return dp * ((float) context.getResources().getDisplayMetrics().densityDpi
+                / DisplayMetrics.DENSITY_DEFAULT);
+    }
+
+    private void updateOval() {
+        mOval = new RectF(getMarginPx() - (getStrokePx() / 2.f),
+                getMarginPx() - (getStrokePx() / 2.f),
+                getMarginPx() + (2 * (getRadiusPx()) + (getStrokePx() / 2.f)),
+                getMarginPx() + 2 * getRadiusPx() + (getStrokePx() / 2.f));
+    }
+
+    private int getAngle() {
+        if (ALLOW_TUNING) {
+            return SystemProperties.getInt("CORNER_HANDLE_ANGLE_DEGREES", ANGLE_DEGREES);
+        } else {
+            return ANGLE_DEGREES;
+        }
+    }
+
+    private int getMarginPx() {
+        // Hand-derived function to map radiusPx to the margin amount.
+        // https://www.wolframalpha.com/input/?i=0.001402+*+x+%5E2%E2%88%920.08661+*+x%2B17.20+from+40+to+180
+        int radius = getRadiusPx();
+        int marginPx = (int) (0.001402f * radius * radius - 0.08661f * radius + 17.2f);
+        if (ALLOW_TUNING) {
+            return SystemProperties.getInt("CORNER_HANDLE_MARGIN_PX", marginPx);
+        } else {
+            return marginPx;
+        }
+    }
+
+    private int getRadiusPx() {
+        // Attempt to get the bottom corner radius, otherwise fall back on the generic or top
+        // values. If none are available, use the FALLBACK_RADIUS_DP.
+        int radius = getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.rounded_corner_radius_bottom);
+        if (radius == 0) {
+            radius = getResources().getDimensionPixelSize(
+                    com.android.internal.R.dimen.rounded_corner_radius);
+        }
+        if (radius == 0) {
+            radius = getResources().getDimensionPixelSize(
+                    com.android.internal.R.dimen.rounded_corner_radius_top);
+        }
+        if (radius == 0) {
+            radius = (int) convertDpToPixel(FALLBACK_RADIUS_DP, mContext);
+        }
+        if (ALLOW_TUNING) {
+            return SystemProperties.getInt("CORNER_HANDLE_RADIUS_PX", radius);
+        } else {
+            return radius;
+        }
+    }
+
+    private int getStrokePx() {
+        if (ALLOW_TUNING) {
+            return SystemProperties.getInt("CORNER_HANDLE_STROKE_PX",
+                    (int) convertDpToPixel(STROKE_DP, getContext()));
+        } else {
+            return (int) convertDpToPixel(STROKE_DP, getContext());
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index 390a9e6..4aaf85a 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -60,6 +60,12 @@
 import android.view.ViewGroup.LayoutParams;
 import android.view.ViewTreeObserver;
 import android.view.WindowManager;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.OvershootInterpolator;
+import android.view.animation.PathInterpolator;
+import android.view.animation.ScaleAnimation;
+import android.view.animation.TranslateAnimation;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 
@@ -72,6 +78,7 @@
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.qs.SecureSetting;
 import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
+import com.android.systemui.statusbar.phone.NavigationBarTransitions;
 import com.android.systemui.statusbar.phone.StatusBar;
 import com.android.systemui.tuner.TunablePadding;
 import com.android.systemui.tuner.TunerService;
@@ -85,7 +92,8 @@
  * An overlay that draws screen decorations in software (e.g for rounded corners or display cutout)
  * for antialiasing and emulation purposes.
  */
-public class ScreenDecorations extends SystemUI implements Tunable {
+public class ScreenDecorations extends SystemUI implements Tunable,
+        NavigationBarTransitions.DarkIntensityListener {
     private static final boolean DEBUG = false;
     private static final String TAG = "ScreenDecorations";
 
@@ -93,13 +101,17 @@
     public static final String PADDING = "sysui_rounded_content_padding";
     private static final boolean DEBUG_SCREENSHOT_ROUNDED_CORNERS =
             SystemProperties.getBoolean("debug.screenshot_rounded_corners", false);
+    private static final boolean VERBOSE = false;
 
     private DisplayManager mDisplayManager;
     private DisplayManager.DisplayListener mDisplayListener;
 
-    @VisibleForTesting protected int mRoundedDefault;
-    @VisibleForTesting protected int mRoundedDefaultTop;
-    @VisibleForTesting protected int mRoundedDefaultBottom;
+    @VisibleForTesting
+    protected int mRoundedDefault;
+    @VisibleForTesting
+    protected int mRoundedDefaultTop;
+    @VisibleForTesting
+    protected int mRoundedDefaultBottom;
     private View mOverlay;
     private View mBottomOverlay;
     private float mDensity;
@@ -111,6 +123,8 @@
     private SecureSetting mColorInversionSetting;
     private boolean mPendingRotationChange;
     private Handler mHandler;
+    private boolean mAssistHintBlocked = false;
+    private boolean mIsReceivingNavBarColor = false;
 
     /**
      * Converts a set of {@link Rect}s into a {@link Region}
@@ -137,15 +151,32 @@
         putComponent(ScreenDecorations.class, this);
     }
 
-    private void fade(View view, boolean fadeIn) {
+    private void fade(View view, boolean fadeIn, boolean isLeft) {
         if (fadeIn) {
             view.animate().cancel();
-            view.setAlpha(0f);
+            view.setAlpha(1f);
             view.setVisibility(View.VISIBLE);
-            view.animate().alpha(1f);
+
+            AnimationSet anim = new AnimationSet(true);
+            anim.setDuration(900);
+
+            Animation scaleAnimation = new ScaleAnimation(2f, 1f, 2f, 1f,
+                    ScaleAnimation.RELATIVE_TO_SELF, 0.5f, ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
+            anim.addAnimation(scaleAnimation);
+            anim.setInterpolator(new PathInterpolator(0.02f, 0.44f, 0.67f, 1.00f));
+
+            Animation translateAnimation = new TranslateAnimation(
+                    TranslateAnimation.RELATIVE_TO_SELF, isLeft ? -0.2f : 0.2f,
+                    TranslateAnimation.RELATIVE_TO_SELF,
+                    0f,
+                    TranslateAnimation.RELATIVE_TO_SELF, 0.2f, TranslateAnimation.RELATIVE_TO_SELF,
+                    0f);
+            anim.addAnimation(translateAnimation);
+            anim.setInterpolator(new OvershootInterpolator());
+            view.startAnimation(anim);
         } else {
             view.animate().cancel();
-            view.animate().alpha(0f).withEndAction(() -> view.setVisibility(View.INVISIBLE));
+            view.animate().setDuration(400).alpha(0f);
         }
 
     }
@@ -161,35 +192,59 @@
             return;
         }
 
+        if (mAssistHintBlocked && visible) {
+            if (VERBOSE) {
+                Log.v(TAG, "Assist hint blocked, cannot make it visible");
+            }
+            return;
+        }
+
         if (mAssistHintVisible != visible) {
             mAssistHintVisible = visible;
 
-            View assistHintTopLeft = mOverlay.findViewById(R.id.assist_hint_left);
-            View assistHintTopRight = mOverlay.findViewById(R.id.assist_hint_right);
-            View assistHintBottomLeft = mBottomOverlay.findViewById(R.id.assist_hint_left);
-            View assistHintBottomRight = mBottomOverlay.findViewById(R.id.assist_hint_right);
+            CornerHandleView assistHintTopLeft = mOverlay.findViewById(R.id.assist_hint_left);
+            CornerHandleView assistHintTopRight = mOverlay.findViewById(R.id.assist_hint_right);
+            CornerHandleView assistHintBottomLeft = mBottomOverlay.findViewById(
+                    R.id.assist_hint_left);
+            CornerHandleView assistHintBottomRight = mBottomOverlay.findViewById(
+                    R.id.assist_hint_right);
 
             switch (mRotation) {
                 case RotationUtils.ROTATION_NONE:
-                    fade(assistHintBottomLeft, mAssistHintVisible);
-                    fade(assistHintBottomRight, mAssistHintVisible);
+                    fade(assistHintBottomLeft, mAssistHintVisible, /* isLeft = */ true);
+                    fade(assistHintBottomRight, mAssistHintVisible, /* isLeft = */ false);
                     break;
                 case RotationUtils.ROTATION_LANDSCAPE:
-                    fade(assistHintTopRight, mAssistHintVisible);
-                    fade(assistHintBottomRight, mAssistHintVisible);
+                    fade(assistHintTopRight, mAssistHintVisible, /* isLeft = */ true);
+                    fade(assistHintBottomRight, mAssistHintVisible, /* isLeft = */ false);
                     break;
                 case RotationUtils.ROTATION_SEASCAPE:
-                    fade(assistHintTopLeft, mAssistHintVisible);
-                    fade(assistHintBottomLeft, mAssistHintVisible);
+                    fade(assistHintTopLeft, mAssistHintVisible, /* isLeft = */ false);
+                    fade(assistHintBottomLeft, mAssistHintVisible,  /* isLeft = */ true);
                     break;
                 case RotationUtils.ROTATION_UPSIDE_DOWN:
-                    fade(assistHintTopLeft, mAssistHintVisible);
-                    fade(assistHintTopRight, mAssistHintVisible);
+                    fade(assistHintTopLeft, mAssistHintVisible, /* isLeft = */ false);
+                    fade(assistHintTopRight, mAssistHintVisible, /* isLeft = */ true);
                     break;
             }
         }
     }
 
+    /**
+     * Prevents the assist hint from becoming visible even if `mAssistHintVisible` is true.
+     */
+    public void setAssistHintBlocked(boolean blocked) {
+        if (!mHandler.getLooper().isCurrentThread()) {
+            mHandler.post(() -> setAssistHintBlocked(blocked));
+            return;
+        }
+
+        mAssistHintBlocked = blocked;
+        if (mAssistHintVisible && mAssistHintBlocked) {
+            setAssistHintVisible(false);
+        }
+    }
+
     @VisibleForTesting
     Handler startHandlerThread() {
         HandlerThread thread = new HandlerThread("ScreenDecorations");
@@ -253,12 +308,12 @@
                 .inflate(R.layout.rounded_corners, null);
         mCutoutTop = new DisplayCutoutView(mContext, true,
                 this::updateWindowVisibilities, this);
-        ((ViewGroup)mOverlay).addView(mCutoutTop);
+        ((ViewGroup) mOverlay).addView(mCutoutTop);
         mBottomOverlay = LayoutInflater.from(mContext)
                 .inflate(R.layout.rounded_corners, null);
         mCutoutBottom = new DisplayCutoutView(mContext, false,
                 this::updateWindowVisibilities, this);
-        ((ViewGroup)mBottomOverlay).addView(mCutoutBottom);
+        ((ViewGroup) mBottomOverlay).addView(mCutoutBottom);
 
         mOverlay.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
         mOverlay.setAlpha(0);
@@ -378,10 +433,26 @@
             if (mOverlay != null) {
                 updateLayoutParams();
                 updateViews();
+                if (mAssistHintVisible) {
+                    // If assist handles are visible, hide them without animation and then make them
+                    // show once again (with corrected rotation).
+                    hideAssistHandles();
+                    setAssistHintVisible(true);
+                }
             }
         }
     }
 
+    private void hideAssistHandles() {
+        if (mOverlay != null && mBottomOverlay != null) {
+            mOverlay.findViewById(R.id.assist_hint_left).setVisibility(View.GONE);
+            mOverlay.findViewById(R.id.assist_hint_right).setVisibility(View.GONE);
+            mBottomOverlay.findViewById(R.id.assist_hint_left).setVisibility(View.GONE);
+            mBottomOverlay.findViewById(R.id.assist_hint_right).setVisibility(View.GONE);
+            mAssistHintVisible = false;
+        }
+    }
+
     private void updateRoundedCornerRadii() {
         final int newRoundedDefault = mContext.getResources().getDimensionPixelSize(
                 com.android.internal.R.dimen.rounded_corner_radius);
@@ -416,7 +487,7 @@
         } else if (mRotation == RotationUtils.ROTATION_LANDSCAPE) {
             updateView(topLeft, Gravity.TOP | Gravity.LEFT, 0);
             updateView(topRight, Gravity.BOTTOM | Gravity.LEFT, 270);
-            updateView(bottomLeft, Gravity.TOP | Gravity.RIGHT, 90);;
+            updateView(bottomLeft, Gravity.TOP | Gravity.RIGHT, 90);
             updateView(bottomRight, Gravity.BOTTOM | Gravity.RIGHT, 180);
         } else if (mRotation == RotationUtils.ROTATION_UPSIDE_DOWN) {
             updateView(topLeft, Gravity.BOTTOM | Gravity.LEFT, 270);
@@ -477,7 +548,7 @@
     }
 
     private void updateView(View v, int gravity, int rotation) {
-        ((FrameLayout.LayoutParams)v.getLayoutParams()).gravity = gravity;
+        ((FrameLayout.LayoutParams) v.getLayoutParams()).gravity = gravity;
         v.setRotation(rotation);
     }
 
@@ -613,6 +684,10 @@
                 setSize(mOverlay.findViewById(R.id.right), sizeTop);
                 setSize(mBottomOverlay.findViewById(R.id.left), sizeBottom);
                 setSize(mBottomOverlay.findViewById(R.id.right), sizeBottom);
+                setSize(mOverlay.findViewById(R.id.assist_hint_left), sizeTop * 2);
+                setSize(mOverlay.findViewById(R.id.assist_hint_right), sizeTop * 2);
+                setSize(mBottomOverlay.findViewById(R.id.assist_hint_left), sizeBottom * 2);
+                setSize(mBottomOverlay.findViewById(R.id.assist_hint_right), sizeBottom * 2);
             }
         });
     }
@@ -624,6 +699,27 @@
         view.setLayoutParams(params);
     }
 
+    @Override
+    public void onDarkIntensity(float darkIntensity) {
+        if (mOverlay != null) {
+            CornerHandleView assistHintTopLeft = mOverlay.findViewById(R.id.assist_hint_left);
+            CornerHandleView assistHintTopRight = mOverlay.findViewById(R.id.assist_hint_right);
+
+            assistHintTopLeft.updateDarkness(darkIntensity);
+            assistHintTopRight.updateDarkness(darkIntensity);
+        }
+
+        if (mBottomOverlay != null) {
+            CornerHandleView assistHintBottomLeft = mBottomOverlay.findViewById(
+                    R.id.assist_hint_left);
+            CornerHandleView assistHintBottomRight = mBottomOverlay.findViewById(
+                    R.id.assist_hint_right);
+
+            assistHintBottomLeft.updateDarkness(darkIntensity);
+            assistHintBottomRight.updateDarkness(darkIntensity);
+        }
+    }
+
     @VisibleForTesting
     static class TunablePaddingTagListener implements FragmentListener {
 
diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
index 3fc6689..49bd5bd 100644
--- a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
@@ -100,6 +100,13 @@
         } else {
             mAppOps.stopWatchingActive(this);
             mAppOps.stopWatchingNoted(this);
+            mBGHandler.removeCallbacksAndMessages(null); // null removes all
+            synchronized (mActiveItems) {
+                mActiveItems.clear();
+            }
+            synchronized (mNotedItems) {
+                mNotedItems.clear();
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehavior.java b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehavior.java
index e2c7313..2eda3d7 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehavior.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehavior.java
@@ -16,31 +16,10 @@
 
 package com.android.systemui.assist;
 
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.systemui.assist.AssistHandleBehaviorController.BehaviorController;
-
 public enum AssistHandleBehavior {
 
-    TEST(new AssistHandleOffBehavior()),
-    OFF(new AssistHandleOffBehavior()),
-    LIKE_HOME(new AssistHandleLikeHomeBehavior()),
-    REMINDER_EXP(new AssistHandleReminderExpBehavior());
-
-    private BehaviorController mController;
-
-    AssistHandleBehavior(BehaviorController controller) {
-        mController = controller;
-    }
-
-    BehaviorController getController() {
-        return mController;
-    }
-
-    @VisibleForTesting
-    void setTestController(BehaviorController controller) {
-        if (this.equals(TEST)) {
-            mController = controller;
-        }
-    }
+    TEST,
+    OFF,
+    LIKE_HOME,
+    REMINDER_EXP;
 }
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehaviorController.java b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehaviorController.java
index 0203d9e..01deb03 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehaviorController.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehaviorController.java
@@ -16,24 +16,27 @@
 
 package com.android.systemui.assist;
 
-import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Build;
 import android.os.Handler;
 import android.os.SystemClock;
-import android.os.SystemProperties;
+import android.provider.DeviceConfig;
 import android.util.Log;
 
+import androidx.annotation.Nullable;
+
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.AssistUtils;
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
+import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.Dependency;
 import com.android.systemui.ScreenDecorations;
 import com.android.systemui.SysUiServiceProvider;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.phone.NavigationModeController;
 
-import java.util.Locale;
+import java.util.EnumMap;
+import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Supplier;
 
@@ -46,60 +49,76 @@
 public final class AssistHandleBehaviorController implements AssistHandleCallbacks {
 
     private static final String TAG = "AssistHandleBehavior";
-    private static final boolean IS_DEBUG_DEVICE =
-            Build.TYPE.toLowerCase(Locale.ROOT).contains("debug")
-                    || Build.TYPE.toLowerCase(Locale.ROOT).equals("eng");
 
-    private static final String SHOWN_FREQUENCY_THRESHOLD_KEY =
-            "ASSIST_HANDLES_SHOWN_FREQUENCY_THRESHOLD_MS";
     private static final long DEFAULT_SHOWN_FREQUENCY_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(10);
-    private static final String SHOW_AND_GO_DURATION_KEY = "ASSIST_HANDLES_SHOW_AND_GO_DURATION_MS";
     private static final long DEFAULT_SHOW_AND_GO_DURATION_MS = TimeUnit.SECONDS.toMillis(3);
-    private static final String BEHAVIOR_KEY = "behavior";
-    private static final String SET_BEHAVIOR_ACTION =
-            "com.android.systemui.SET_ASSIST_HANDLE_BEHAVIOR";
+
+    /**
+     * This is the default behavior that will be used once the system is up. It will be set once the
+     * behavior dependencies are available. This ensures proper behavior lifecycle.
+     */
+    private static final AssistHandleBehavior DEFAULT_BEHAVIOR = AssistHandleBehavior.REMINDER_EXP;
 
     private final Context mContext;
+    private final AssistUtils mAssistUtils;
     private final Handler mHandler;
     private final Runnable mHideHandles = this::hideHandles;
     private final Supplier<ScreenDecorations> mScreenDecorationsSupplier;
+    private final Map<AssistHandleBehavior, BehaviorController> mBehaviorMap =
+            new EnumMap<>(AssistHandleBehavior.class);
 
     private boolean mHandlesShowing = false;
     private long mHandlesLastHiddenAt;
+    /**
+     * This should always be initialized as {@link AssistHandleBehavior#OFF} to ensure proper
+     * behavior lifecycle.
+     */
     private AssistHandleBehavior mCurrentBehavior = AssistHandleBehavior.OFF;
     private boolean mInGesturalMode;
 
-    AssistHandleBehaviorController(Context context, Handler handler) {
-        this(context, handler, () ->
-                SysUiServiceProvider.getComponent(context, ScreenDecorations.class));
+    AssistHandleBehaviorController(Context context, AssistUtils assistUtils, Handler handler) {
+        this(
+                context,
+                assistUtils,
+                handler, () -> SysUiServiceProvider.getComponent(context, ScreenDecorations.class),
+                /* testBehavior = */ null);
     }
 
     @VisibleForTesting
     AssistHandleBehaviorController(
             Context context,
+            AssistUtils assistUtils,
             Handler handler,
-            Supplier<ScreenDecorations> screenDecorationsSupplier) {
+            Supplier<ScreenDecorations> screenDecorationsSupplier,
+            @Nullable BehaviorController testBehavior) {
         mContext = context;
+        mAssistUtils = assistUtils;
         mHandler = handler;
         mScreenDecorationsSupplier = screenDecorationsSupplier;
 
+        mBehaviorMap.put(AssistHandleBehavior.OFF, new AssistHandleOffBehavior());
+        mBehaviorMap.put(AssistHandleBehavior.LIKE_HOME, new AssistHandleLikeHomeBehavior());
+        mBehaviorMap.put(AssistHandleBehavior.REMINDER_EXP, new AssistHandleReminderExpBehavior());
+        if (testBehavior != null) {
+            mBehaviorMap.put(AssistHandleBehavior.TEST, testBehavior);
+        }
+
         mInGesturalMode = QuickStepContract.isGesturalMode(
                 Dependency.get(NavigationModeController.class)
                         .addListener(this::handleNavigationModeChange));
 
-        if (IS_DEBUG_DEVICE) {
-            context.registerReceiver(new BroadcastReceiver() {
-                @Override
-                public void onReceive(Context context, Intent intent) {
-                    String behaviorString = intent.getExtras().getString(BEHAVIOR_KEY);
-                    try {
-                        setBehavior(AssistHandleBehavior.valueOf(behaviorString));
-                    } catch (IllegalArgumentException e) {
-                        Log.e(TAG, "Invalid behavior identifier: " + behaviorString);
+        setBehavior(DeviceConfig.getString(
+                DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.ASSIST_HANDLES_BEHAVIOR_MODE,
+                DEFAULT_BEHAVIOR.toString()));
+        DeviceConfig.addOnPropertyChangedListener(
+                DeviceConfig.NAMESPACE_SYSTEMUI,
+                mHandler::post,
+                (namespace, name, value) -> {
+                    if (SystemUiDeviceConfigFlags.ASSIST_HANDLES_BEHAVIOR_MODE.equals(name)) {
+                        setBehavior(value);
                     }
-                }
-            }, new IntentFilter(SET_BEHAVIOR_ACTION));
-        }
+                });
     }
 
     @Override
@@ -123,26 +142,56 @@
         mHandler.post(() -> maybeShowHandles(/* ignoreThreshold = */ true));
     }
 
+    void onAssistantGesturePerformed() {
+        mBehaviorMap.get(mCurrentBehavior).onAssistantGesturePerformed();
+    }
+
     void setBehavior(AssistHandleBehavior behavior) {
         if (mCurrentBehavior == behavior) {
             return;
         }
 
+        if (!mBehaviorMap.containsKey(behavior)) {
+            Log.e(TAG, "Unsupported behavior requested: " + behavior.toString());
+            return;
+        }
+
         if (mInGesturalMode) {
-            mCurrentBehavior.getController().onModeDeactivated();
-            behavior.getController().onModeActivated(mContext, this);
+            mBehaviorMap.get(mCurrentBehavior).onModeDeactivated();
+            mBehaviorMap.get(behavior).onModeActivated(mContext, /* callbacks = */ this);
         }
 
         mCurrentBehavior = behavior;
     }
 
-    private static long getShownFrequencyThreshold() {
-        return SystemProperties.getLong(
-                SHOWN_FREQUENCY_THRESHOLD_KEY, DEFAULT_SHOWN_FREQUENCY_THRESHOLD_MS);
+    private void setBehavior(@Nullable String behavior) {
+        try {
+            setBehavior(AssistHandleBehavior.valueOf(behavior));
+        } catch (IllegalArgumentException | NullPointerException e) {
+            Log.e(TAG, "Invalid behavior: " + behavior, e);
+        }
     }
 
-    private static long getShowAndGoDuration() {
-        return SystemProperties.getLong(SHOW_AND_GO_DURATION_KEY, DEFAULT_SHOW_AND_GO_DURATION_MS);
+    private boolean handlesUnblocked(boolean ignoreThreshold) {
+        long timeSinceHidden = SystemClock.elapsedRealtime() - mHandlesLastHiddenAt;
+        boolean notThrottled = ignoreThreshold || timeSinceHidden > getShownFrequencyThreshold();
+        ComponentName assistantComponent =
+                mAssistUtils.getAssistComponentForUser(KeyguardUpdateMonitor.getCurrentUser());
+        return notThrottled && assistantComponent != null;
+    }
+
+    private long getShownFrequencyThreshold() {
+        return DeviceConfig.getLong(
+                DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOWN_FREQUENCY_THRESHOLD_MS,
+                DEFAULT_SHOWN_FREQUENCY_THRESHOLD_MS);
+    }
+
+    private long getShowAndGoDuration() {
+        return DeviceConfig.getLong(
+                DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOW_AND_GO_DURATION_MS,
+                DEFAULT_SHOW_AND_GO_DURATION_MS);
     }
 
     private void maybeShowHandles(boolean ignoreThreshold) {
@@ -150,8 +199,7 @@
             return;
         }
 
-        long timeSinceHidden = SystemClock.elapsedRealtime() - mHandlesLastHiddenAt;
-        if (ignoreThreshold || timeSinceHidden > getShownFrequencyThreshold()) {
+        if (handlesUnblocked(ignoreThreshold)) {
             mHandlesShowing = true;
             ScreenDecorations screenDecorations = mScreenDecorationsSupplier.get();
             if (screenDecorations == null) {
@@ -185,9 +233,9 @@
 
         mInGesturalMode = inGesturalMode;
         if (mInGesturalMode) {
-            mCurrentBehavior.getController().onModeActivated(mContext, this);
+            mBehaviorMap.get(mCurrentBehavior).onModeActivated(mContext, /* callbacks = */ this);
         } else {
-            mCurrentBehavior.getController().onModeDeactivated();
+            mBehaviorMap.get(mCurrentBehavior).onModeDeactivated();
             hide();
         }
     }
@@ -199,6 +247,7 @@
 
     interface BehaviorController {
         void onModeActivated(Context context, AssistHandleCallbacks callbacks);
-        void onModeDeactivated();
+        default void onModeDeactivated() {}
+        default void onAssistantGesturePerformed() {}
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleLikeHomeBehavior.java b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleLikeHomeBehavior.java
index e89e93c..05e504c 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleLikeHomeBehavior.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleLikeHomeBehavior.java
@@ -16,17 +16,15 @@
 
 package com.android.systemui.assist;
 
-import android.app.StatusBarManager;
 import android.content.Context;
 
 import androidx.annotation.Nullable;
 
 import com.android.systemui.Dependency;
-import com.android.systemui.SysUiServiceProvider;
 import com.android.systemui.assist.AssistHandleBehaviorController.BehaviorController;
-import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.NavigationBarController;
-import com.android.systemui.statusbar.phone.NavigationBarFragment;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.shared.system.QuickStepContract;
 
 /**
  * Assistant Handle behavior that makes Assistant handles show/hide when the home handle is
@@ -34,47 +32,68 @@
  */
 final class AssistHandleLikeHomeBehavior implements BehaviorController {
 
-    private final CommandQueue.Callbacks mCallbacks = new CommandQueue.Callbacks() {
+    private final StatusBarStateController.StateListener mStatusBarStateListener =
+            new StatusBarStateController.StateListener() {
+                @Override
+                public void onDozingChanged(boolean isDozing) {
+                    handleDozingChanged(isDozing);
+                }
+            };
+    private final OverviewProxyService.OverviewProxyListener mOverviewProxyListener =
+            new OverviewProxyService.OverviewProxyListener() {
         @Override
-        public void setWindowState(int displayId, int window, int state) {
-            if (mNavBarDisplayId == displayId
-                    && window == StatusBarManager.WINDOW_NAVIGATION_BAR) {
-                handleWindowStateChanged(state);
-            }
+        public void onSystemUiStateChanged(int sysuiStateFlags) {
+            handleSystemUiStateChange(sysuiStateFlags);
         }
     };
+    private final StatusBarStateController mStatusBarStateController;
+    private final OverviewProxyService mOverviewProxyService;
 
-    private CommandQueue mCommandQueue;
-    private int mNavBarDisplayId;
-    private boolean mIsNavBarWindowVisible;
+    private boolean mIsDozing;
+    private boolean mIsHomeHandleHiding;
 
     @Nullable private AssistHandleCallbacks mAssistHandleCallbacks;
 
+    AssistHandleLikeHomeBehavior() {
+        mStatusBarStateController = Dependency.get(StatusBarStateController.class);
+        mOverviewProxyService = Dependency.get(OverviewProxyService.class);
+    }
+
     @Override
     public void onModeActivated(Context context, AssistHandleCallbacks callbacks) {
         mAssistHandleCallbacks = callbacks;
-        NavigationBarFragment navigationBarFragment =
-                Dependency.get(NavigationBarController.class).getDefaultNavigationBarFragment();
-        mNavBarDisplayId = navigationBarFragment.mDisplayId;
-        mIsNavBarWindowVisible = navigationBarFragment.isNavBarWindowVisible();
-        mCommandQueue = SysUiServiceProvider.getComponent(context, CommandQueue.class);
-        mCommandQueue.addCallback(mCallbacks);
+        mIsDozing = mStatusBarStateController.isDozing();
+        mStatusBarStateController.addCallback(mStatusBarStateListener);
+        mOverviewProxyService.addCallback(mOverviewProxyListener);
         callbackForCurrentState();
     }
 
     @Override
     public void onModeDeactivated() {
         mAssistHandleCallbacks = null;
-        mCommandQueue.removeCallback(mCallbacks);
+        mOverviewProxyService.removeCallback(mOverviewProxyListener);
     }
 
-    private void handleWindowStateChanged(int state) {
-        boolean newVisibility = state == StatusBarManager.WINDOW_STATE_SHOWING;
-        if (mIsNavBarWindowVisible == newVisibility) {
+    private static boolean isHomeHandleHiding(int sysuiStateFlags) {
+        return (sysuiStateFlags & QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN) != 0;
+    }
+
+    private void handleDozingChanged(boolean isDozing) {
+        if (mIsDozing == isDozing) {
             return;
         }
 
-        mIsNavBarWindowVisible = newVisibility;
+        mIsDozing = isDozing;
+        callbackForCurrentState();
+    }
+
+    private void handleSystemUiStateChange(int sysuiStateFlags) {
+        boolean isHomeHandleHiding = isHomeHandleHiding(sysuiStateFlags);
+        if (mIsHomeHandleHiding == isHomeHandleHiding) {
+            return;
+        }
+
+        mIsHomeHandleHiding = isHomeHandleHiding;
         callbackForCurrentState();
     }
 
@@ -83,10 +102,10 @@
             return;
         }
 
-        if (mIsNavBarWindowVisible) {
-            mAssistHandleCallbacks.showAndStay();
-        } else {
+        if (mIsHomeHandleHiding || mIsDozing) {
             mAssistHandleCallbacks.hide();
+        } else {
+            mAssistHandleCallbacks.showAndStay();
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleOffBehavior.java b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleOffBehavior.java
index ebf15ae..f4130ae 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleOffBehavior.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleOffBehavior.java
@@ -27,9 +27,4 @@
     public void onModeActivated(Context context, AssistHandleCallbacks callbacks) {
         callbacks.hide();
     }
-
-    @Override
-    public void onModeDeactivated() {
-        // Do nothing
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleReminderExpBehavior.java b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleReminderExpBehavior.java
index 4a5e0e8..4b6a6dc 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleReminderExpBehavior.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleReminderExpBehavior.java
@@ -16,24 +16,27 @@
 
 package com.android.systemui.assist;
 
+import android.app.ActivityManager;
 import android.content.ComponentName;
 import android.content.Context;
-import android.graphics.Rect;
-import android.view.View;
-import android.view.WindowManager;
+import android.os.SystemClock;
+import android.provider.DeviceConfig;
+import android.provider.Settings;
 
 import androidx.annotation.Nullable;
 
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.systemui.Dependency;
-import com.android.systemui.SysUiServiceProvider;
 import com.android.systemui.assist.AssistHandleBehaviorController.BehaviorController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.TaskStackChangeListener;
-import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.StatusBarState;
 
+import java.util.concurrent.TimeUnit;
+
 /**
  * Assistant handle behavior that hides the handles when the phone is dozing or in immersive mode,
  * shows the handles when on lockscreen, and shows the handles temporarily when changing tasks or
@@ -41,6 +44,11 @@
  */
 final class AssistHandleReminderExpBehavior implements BehaviorController {
 
+    private static final String LEARNING_TIME_ELAPSED_KEY = "reminder_exp_learning_time_elapsed";
+    private static final String LEARNING_EVENT_COUNT_KEY = "reminder_exp_learning_event_count";
+    private static final long DEFAULT_LEARNING_TIME_MS = TimeUnit.DAYS.toMillis(3);
+    private static final int DEFAULT_LEARNING_COUNT = 3;
+
     private final StatusBarStateController.StateListener mStatusBarStateListener =
             new StatusBarStateController.StateListener() {
                 @Override
@@ -65,66 +73,95 @@
                     handleTaskStackTopChanged(taskId);
                 }
             };
-    private final CommandQueue.Callbacks mCallbacks = new CommandQueue.Callbacks() {
-        @Override
-        public void setSystemUiVisibility(int displayId, int vis,
-                int fullscreenStackVis, int dockedStackVis, int mask,
-                Rect fullscreenStackBounds, Rect dockedStackBounds,
-                boolean navbarColorManagedByIme) {
-            if (mStatusBarDisplayId == displayId) {
-                handleSystemUiVisibilityChange(vis, mask);
-            }
-        }
-    };
     private final OverviewProxyService.OverviewProxyListener mOverviewProxyListener =
             new OverviewProxyService.OverviewProxyListener() {
                 @Override
                 public void onOverviewShown(boolean fromHome) {
                     handleOverviewShown();
                 }
+
+                @Override
+                public void onSystemUiStateChanged(int sysuiStateFlags) {
+                    handleSystemUiStateChanged(sysuiStateFlags);
+                }
             };
 
-    private StatusBarStateController mStatusBarStateController;
-    private ActivityManagerWrapper mActivityManagerWrapper;
-    private OverviewProxyService mOverviewProxyService;
-    private int mStatusBarDisplayId;
-    private CommandQueue mCommandQueue;
+    private final StatusBarStateController mStatusBarStateController;
+    private final ActivityManagerWrapper mActivityManagerWrapper;
+    private final OverviewProxyService mOverviewProxyService;
+
     private boolean mOnLockscreen;
     private boolean mIsDozing;
     private int mRunningTaskId;
-    private boolean mIsImmersive;
+    private boolean mIsNavBarHidden;
 
+    /** Whether user has learned the gesture. */
+    private boolean mIsLearned;
+    private long mLastLearningTimestamp;
+    /** Uptime while in this behavior. */
+    private long mLearningTimeElapsed;
+    /** Number of successful Assistant invocations while in this behavior. */
+    private int mLearningCount;
+
+    @Nullable private Context mContext;
     @Nullable private AssistHandleCallbacks mAssistHandleCallbacks;
 
+    AssistHandleReminderExpBehavior() {
+        mStatusBarStateController = Dependency.get(StatusBarStateController.class);
+        mActivityManagerWrapper = ActivityManagerWrapper.getInstance();
+        mOverviewProxyService = Dependency.get(OverviewProxyService.class);
+    }
+
     @Override
     public void onModeActivated(Context context, AssistHandleCallbacks callbacks) {
+        mContext = context;
         mAssistHandleCallbacks = callbacks;
-        mStatusBarStateController = Dependency.get(StatusBarStateController.class);
         mOnLockscreen = onLockscreen(mStatusBarStateController.getState());
         mIsDozing = mStatusBarStateController.isDozing();
         mStatusBarStateController.addCallback(mStatusBarStateListener);
-        mActivityManagerWrapper = ActivityManagerWrapper.getInstance();
-        mRunningTaskId = mActivityManagerWrapper.getRunningTask().taskId;
+        ActivityManager.RunningTaskInfo runningTaskInfo = mActivityManagerWrapper.getRunningTask();
+        mRunningTaskId = runningTaskInfo == null ? 0 : runningTaskInfo.taskId;
         mActivityManagerWrapper.registerTaskStackListener(mTaskStackChangeListener);
-        mStatusBarDisplayId =
-                ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
-                        .getDefaultDisplay().getDisplayId();
-        mCommandQueue = SysUiServiceProvider.getComponent(context, CommandQueue.class);
-        mCommandQueue.addCallback(mCallbacks);
-        mOverviewProxyService = Dependency.get(OverviewProxyService.class);
         mOverviewProxyService.addCallback(mOverviewProxyListener);
-        callbackForCurrentState();
+
+        mLearningTimeElapsed = Settings.Secure.getLong(
+                context.getContentResolver(), LEARNING_TIME_ELAPSED_KEY, /* default = */ 0);
+        mLearningCount = Settings.Secure.getInt(
+                context.getContentResolver(), LEARNING_EVENT_COUNT_KEY, /* default = */ 0);
+        mLastLearningTimestamp = SystemClock.uptimeMillis();
+
+        callbackForCurrentState(/* justUnlocked = */ false);
     }
 
     @Override
     public void onModeDeactivated() {
         mAssistHandleCallbacks = null;
+        if (mContext != null) {
+            Settings.Secure.putLong(
+                    mContext.getContentResolver(), LEARNING_TIME_ELAPSED_KEY, mLearningTimeElapsed);
+            Settings.Secure.putInt(
+                    mContext.getContentResolver(), LEARNING_EVENT_COUNT_KEY, mLearningCount);
+            mContext = null;
+        }
         mStatusBarStateController.removeCallback(mStatusBarStateListener);
         mActivityManagerWrapper.unregisterTaskStackListener(mTaskStackChangeListener);
-        mCommandQueue.removeCallback(mCallbacks);
         mOverviewProxyService.removeCallback(mOverviewProxyListener);
     }
 
+    @Override
+    public void onAssistantGesturePerformed() {
+        if (mContext == null) {
+            return;
+        }
+
+        Settings.Secure.putLong(
+                mContext.getContentResolver(), LEARNING_EVENT_COUNT_KEY, ++mLearningCount);
+    }
+
+    private static boolean isNavBarHidden(int sysuiStateFlags) {
+        return (sysuiStateFlags & QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN) != 0;
+    }
+
     private void handleStatusBarStateChanged(int newState) {
         boolean onLockscreen = onLockscreen(newState);
         if (mOnLockscreen == onLockscreen) {
@@ -132,7 +169,7 @@
         }
 
         mOnLockscreen = onLockscreen;
-        callbackForCurrentState();
+        callbackForCurrentState(!onLockscreen);
     }
 
     private void handleDozingChanged(boolean isDozing) {
@@ -141,7 +178,7 @@
         }
 
         mIsDozing = isDozing;
-        callbackForCurrentState();
+        callbackForCurrentState(/* justUnlocked = */ false);
     }
 
     private void handleTaskStackTopChanged(int taskId) {
@@ -150,21 +187,21 @@
         }
 
         mRunningTaskId = taskId;
-        callbackForCurrentState();
+        callbackForCurrentState(/* justUnlocked = */ false);
     }
 
-    private void handleSystemUiVisibilityChange(int vis, int mask) {
-        boolean isImmersive = isImmersive(vis, mask);
-        if (mIsImmersive == isImmersive) {
+    private void handleSystemUiStateChanged(int sysuiStateFlags) {
+        boolean isNavBarHidden = isNavBarHidden(sysuiStateFlags);
+        if (mIsNavBarHidden == isNavBarHidden) {
             return;
         }
 
-        mIsImmersive = isImmersive;
-        callbackForCurrentState();
+        mIsNavBarHidden = isNavBarHidden;
+        callbackForCurrentState(/* justUnlocked = */ false);
     }
 
     private void handleOverviewShown() {
-        callbackForCurrentState();
+        callbackForCurrentState(/* justUnlocked = */ false);
     }
 
     private boolean onLockscreen(int statusBarState) {
@@ -172,17 +209,34 @@
                 || statusBarState == StatusBarState.SHADE_LOCKED;
     }
 
-    private boolean isImmersive(int vis, int mask) {
-        return ((vis & mask)
-                & (View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)) != 0;
+    private void callbackForCurrentState(boolean justUnlocked) {
+        updateLearningStatus();
+
+        if (mIsLearned) {
+            callbackForLearnedState(justUnlocked);
+        } else {
+            callbackForUnlearnedState();
+        }
     }
 
-    private void callbackForCurrentState() {
+    private void callbackForLearnedState(boolean justUnlocked) {
         if (mAssistHandleCallbacks == null) {
             return;
         }
 
-        if (mIsDozing || mIsImmersive) {
+        if (mIsDozing || mIsNavBarHidden || mOnLockscreen) {
+            mAssistHandleCallbacks.hide();
+        } else if (justUnlocked) {
+            mAssistHandleCallbacks.showAndGo();
+        }
+    }
+
+    private void callbackForUnlearnedState() {
+        if (mAssistHandleCallbacks == null) {
+            return;
+        }
+
+        if (mIsDozing || mIsNavBarHidden) {
             mAssistHandleCallbacks.hide();
         } else if (mOnLockscreen) {
             mAssistHandleCallbacks.showAndStay();
@@ -190,4 +244,33 @@
             mAssistHandleCallbacks.showAndGo();
         }
     }
+
+    private void updateLearningStatus() {
+        if (mContext == null) {
+            return;
+        }
+
+        long currentTimestamp = SystemClock.uptimeMillis();
+        mLearningTimeElapsed += currentTimestamp - mLastLearningTimestamp;
+        mLastLearningTimestamp = currentTimestamp;
+        Settings.Secure.putLong(
+                mContext.getContentResolver(), LEARNING_TIME_ELAPSED_KEY, mLearningTimeElapsed);
+
+        mIsLearned =
+                mLearningCount >= getLearningCount() || mLearningTimeElapsed >= getLearningTimeMs();
+    }
+
+    private long getLearningTimeMs() {
+        return DeviceConfig.getLong(
+                DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.ASSIST_HANDLES_LEARN_TIME_MS,
+                DEFAULT_LEARNING_TIME_MS);
+    }
+
+    private int getLearningCount() {
+        return DeviceConfig.getInt(
+                DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.ASSIST_HANDLES_LEARN_COUNT,
+                DEFAULT_LEARNING_COUNT);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
index 2c38e51..2fc79d6 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
@@ -40,8 +40,11 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.settingslib.applications.InterestingConfigChanges;
 import com.android.systemui.ConfigurationChangedReceiver;
+import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.assist.ui.DefaultUiController;
+import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 
@@ -50,6 +53,40 @@
  */
 public class AssistManager implements ConfigurationChangedReceiver {
 
+    /**
+     * Controls the UI for showing Assistant invocation progress.
+     */
+    public interface UiController {
+        /**
+         * Updates the invocation progress.
+         *
+         * @param type     one of INVOCATION_TYPE_GESTURE, INVOCATION_TYPE_ACTIVE_EDGE,
+         *                 INVOCATION_TYPE_VOICE, INVOCATION_TYPE_QUICK_SEARCH_BAR,
+         *                 INVOCATION_HOME_BUTTON_LONG_PRESS
+         * @param progress a float between 0 and 1 inclusive. 0 represents the beginning of the
+         *                 gesture; 1 represents the end.
+         */
+        void onInvocationProgress(int type, float progress);
+
+        /**
+         * Called when an invocation gesture completes.
+         *
+         * @param velocity the speed of the invocation gesture, in pixels per millisecond. For
+         *                 drags, this is 0.
+         */
+        void onGestureCompletion(float velocity);
+
+        /**
+         * Called with the Bundle from VoiceInteractionSessionListener.onSetUiHints.
+         */
+        void processBundle(Bundle hints);
+
+        /**
+         * Hides the UI.
+         */
+        void hide();
+    }
+
     private static final String TAG = "AssistManager";
 
     // Note that VERBOSE logging may leak PII (e.g. transcription contents).
@@ -76,6 +113,7 @@
     private final InterestingConfigChanges mInterestingConfigChanges;
     private final PhoneStateMonitor mPhoneStateMonitor;
     private final AssistHandleBehaviorController mHandleController;
+    private final UiController mUiController;
 
     private AssistOrbContainer mView;
     private final DeviceProvisionedController mDeviceProvisionedController;
@@ -85,16 +123,16 @@
     private IVoiceInteractionSessionShowCallback mShowCallback =
             new IVoiceInteractionSessionShowCallback.Stub() {
 
-        @Override
-        public void onFailed() throws RemoteException {
-            mView.post(mHideRunnable);
-        }
+                @Override
+                public void onFailed() throws RemoteException {
+                    mView.post(mHideRunnable);
+                }
 
-        @Override
-        public void onShown() throws RemoteException {
-            mView.post(mHideRunnable);
-        }
-    };
+                @Override
+                public void onShown() throws RemoteException {
+                    mView.post(mHideRunnable);
+                }
+            };
 
     private Runnable mHideRunnable = new Runnable() {
         @Override
@@ -111,7 +149,8 @@
         mAssistUtils = new AssistUtils(context);
         mAssistDisclosure = new AssistDisclosure(context, new Handler());
         mPhoneStateMonitor = new PhoneStateMonitor(context);
-        mHandleController = new AssistHandleBehaviorController(context, new Handler());
+        mHandleController =
+                new AssistHandleBehaviorController(context, mAssistUtils, new Handler());
 
         registerVoiceInteractionSessionListener();
         mInterestingConfigChanges = new InterestingConfigChanges(ActivityInfo.CONFIG_ORIENTATION
@@ -119,6 +158,23 @@
                 | ActivityInfo.CONFIG_SCREEN_LAYOUT | ActivityInfo.CONFIG_ASSETS_PATHS);
         onConfigurationChanged(context.getResources().getConfiguration());
         mShouldEnableOrb = !ActivityManager.isLowRamDeviceStatic();
+
+        mUiController = new DefaultUiController(mContext);
+
+        OverviewProxyService overviewProxy = Dependency.get(OverviewProxyService.class);
+        overviewProxy.addCallback(new OverviewProxyService.OverviewProxyListener() {
+            @Override
+            public void onAssistantProgress(float progress) {
+                // Progress goes from 0 to 1 to indicate how close the assist gesture is to
+                // completion.
+                onInvocationProgress(INVOCATION_TYPE_GESTURE, progress);
+            }
+
+            @Override
+            public void onAssistantGestureCompletion(float velocity) {
+                onGestureCompletion(velocity);
+            }
+        });
     }
 
     protected void registerVoiceInteractionSessionListener() {
@@ -191,26 +247,32 @@
         if (args == null) {
             args = new Bundle();
         }
+        int invocationType = args.getInt(INVOCATION_TYPE_KEY, 0);
+        if (invocationType == INVOCATION_TYPE_GESTURE) {
+            mHandleController.onAssistantGesturePerformed();
+        }
         args.putInt(INVOCATION_PHONE_STATE_KEY, mPhoneStateMonitor.getPhoneState());
         args.putLong(INVOCATION_TIME_MS_KEY, SystemClock.uptimeMillis());
         // Logs assistant start with invocation type.
         MetricsLogger.action(
                 new LogMaker(MetricsEvent.ASSISTANT)
-                    .setType(MetricsEvent.TYPE_OPEN).setSubtype(args.getInt(INVOCATION_TYPE_KEY)));
+                        .setType(MetricsEvent.TYPE_OPEN).setSubtype(
+                        invocationType));
         startAssistInternal(args, assistComponent, isService);
     }
 
     /** Called when the user is performing an assistant invocation action (e.g. Active Edge) */
     public void onInvocationProgress(int type, float progress) {
-        // intentional no-op, vendor's AssistManager implementation should override if needed.
+        mUiController.onInvocationProgress(type, progress);
     }
 
-    /** Called when the user has invoked the assistant with the incoming velocity, in pixels per
+    /**
+     * Called when the user has invoked the assistant with the incoming velocity, in pixels per
      * millisecond. For invocations without a velocity (e.g. slow drag), the velocity is set to
      * zero.
      */
-    public void onAssistantGestureCompletion(float velocity) {
-        // intentional no-op, vendor's AssistManager implementation should override if needed.
+    public void onGestureCompletion(float velocity) {
+        mUiController.onGestureCompletion(velocity);
     }
 
     public void hideAssist() {
@@ -264,7 +326,7 @@
                 Settings.Secure.ASSIST_STRUCTURE_ENABLED, 1, UserHandle.USER_CURRENT) != 0;
 
         final SearchManager searchManager =
-            (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
+                (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
         if (searchManager == null) {
             return;
         }
@@ -329,7 +391,7 @@
                 // Look for the search icon specified in the activity meta-data
                 Bundle metaData = isService
                         ? packageManager.getServiceInfo(
-                                component, PackageManager.GET_META_DATA).metaData
+                        component, PackageManager.GET_META_DATA).metaData
                         : packageManager.getActivityInfo(
                                 component, PackageManager.GET_META_DATA).metaData;
                 if (metaData != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/CircularCornerPathRenderer.java b/packages/SystemUI/src/com/android/systemui/assist/ui/CircularCornerPathRenderer.java
new file mode 100644
index 0000000..162e09e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/assist/ui/CircularCornerPathRenderer.java
@@ -0,0 +1,65 @@
+/*
+ * 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.assist.ui;
+
+import android.graphics.Path;
+
+/**
+ * Describes paths for circular rounded device corners.
+ */
+public final class CircularCornerPathRenderer extends CornerPathRenderer {
+
+    private final int mCornerRadiusBottom;
+    private final int mCornerRadiusTop;
+    private final int mHeight;
+    private final int mWidth;
+    private final Path mPath = new Path();
+
+    public CircularCornerPathRenderer(int cornerRadiusBottom, int cornerRadiusTop,
+            int width, int height) {
+        mCornerRadiusBottom = cornerRadiusBottom;
+        mCornerRadiusTop = cornerRadiusTop;
+        mHeight = height;
+        mWidth = width;
+    }
+
+    @Override // CornerPathRenderer
+    public Path getCornerPath(Corner corner) {
+        mPath.reset();
+        switch (corner) {
+            case BOTTOM_LEFT:
+                mPath.moveTo(0, mHeight - mCornerRadiusBottom);
+                mPath.arcTo(0, mHeight - mCornerRadiusBottom * 2, mCornerRadiusBottom * 2, mHeight,
+                        180, -90, true);
+                break;
+            case BOTTOM_RIGHT:
+                mPath.moveTo(mWidth - mCornerRadiusBottom, mHeight);
+                mPath.arcTo(mWidth - mCornerRadiusBottom * 2, mHeight - mCornerRadiusBottom * 2,
+                        mWidth, mHeight, 90, -90, true);
+                break;
+            case TOP_RIGHT:
+                mPath.moveTo(mWidth, mCornerRadiusTop);
+                mPath.arcTo(mWidth - mCornerRadiusTop, 0, mWidth, mCornerRadiusTop, 0, -90, true);
+                break;
+            case TOP_LEFT:
+                mPath.moveTo(mCornerRadiusTop, 0);
+                mPath.arcTo(0, 0, mCornerRadiusTop, mCornerRadiusTop, 270, -90, true);
+                break;
+        }
+        return mPath;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/CornerPathRenderer.java b/packages/SystemUI/src/com/android/systemui/assist/ui/CornerPathRenderer.java
new file mode 100644
index 0000000..2b40e65
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/assist/ui/CornerPathRenderer.java
@@ -0,0 +1,140 @@
+/*
+ * 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.assist.ui;
+
+import android.graphics.Path;
+import android.graphics.PointF;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Handles paths along device corners.
+ */
+public abstract class CornerPathRenderer {
+
+    // The maximum delta between the corner curve and points approximating the corner curve.
+    private static final float ACCEPTABLE_ERROR = 0.1f;
+
+    /**
+     * For convenience, labels the four device corners.
+     *
+     * Corners must be listed in CCW order, otherwise we'll break rotation.
+     */
+    public enum Corner {
+        BOTTOM_LEFT,
+        BOTTOM_RIGHT,
+        TOP_RIGHT,
+        TOP_LEFT
+    }
+
+    /**
+     * Returns the path along the inside of a corner (centered insetAmountPx from the corner's
+     * edge).
+     */
+    public Path getInsetPath(Corner corner, float insetAmountPx) {
+        return approximateInnerPath(getCornerPath(corner), -insetAmountPx);
+    }
+
+    /**
+     * Returns the path of a corner (centered on the exact corner). Must be implemented by extending
+     * classes, based on the device-specific rounded corners. A default implementation for circular
+     * corners is provided by CircularCornerPathRenderer.
+     */
+    public abstract Path getCornerPath(Corner corner);
+
+    private Path approximateInnerPath(Path input, float delta) {
+        List<PointF> points = shiftBy(getApproximatePoints(input), delta);
+        return toPath(points);
+    }
+
+    private ArrayList<PointF> getApproximatePoints(Path path) {
+        float[] rawInput = path.approximate(ACCEPTABLE_ERROR);
+
+        ArrayList<PointF> output = new ArrayList<>();
+
+        for (int i = 0; i < rawInput.length; i = i + 3) {
+            output.add(new PointF(rawInput[i + 1], rawInput[i + 2]));
+        }
+
+        return output;
+    }
+
+    private ArrayList<PointF> shiftBy(ArrayList<PointF> input, float delta) {
+        ArrayList<PointF> output = new ArrayList<>();
+
+        for (int i = 0; i < input.size(); i++) {
+            PointF point = input.get(i);
+            PointF normal = normalAt(input, i);
+            PointF shifted =
+                    new PointF(point.x + (normal.x * delta), point.y + (normal.y * delta));
+            output.add(shifted);
+        }
+        return output;
+    }
+
+    private Path toPath(List<PointF> points) {
+        Path path = new Path();
+        if (points.size() > 0) {
+            path.moveTo(points.get(0).x, points.get(0).y);
+            for (PointF point : points.subList(1, points.size())) {
+                path.lineTo(point.x, point.y);
+            }
+        }
+        return path;
+    }
+
+    private PointF normalAt(List<PointF> points, int index) {
+        PointF d1;
+        if (index == 0) {
+            d1 = new PointF(0, 0);
+        } else {
+            PointF point = points.get(index);
+            PointF previousPoint = points.get(index - 1);
+            d1 = new PointF((point.x - previousPoint.x), (point.y - previousPoint.y));
+        }
+
+        PointF d2;
+        if (index == (points.size() - 1)) {
+            d2 = new PointF(0, 0);
+        } else {
+            PointF point = points.get(index);
+            PointF nextPoint = points.get(index + 1);
+            d2 = new PointF((nextPoint.x - point.x), (nextPoint.y - point.y));
+        }
+
+        return rotate90Ccw(normalize(new PointF(d1.x + d2.x, d1.y + d2.y)));
+    }
+
+    private PointF rotate90Ccw(PointF input) {
+        return new PointF(-input.y, input.x);
+    }
+
+    private float magnitude(PointF point) {
+        return (float) Math.sqrt((point.x * point.x) + (point.y * point.y));
+    }
+
+    private PointF normalize(PointF point) {
+        float magnitude = magnitude(point);
+        if (magnitude == 0.f) {
+            return point;
+        }
+
+        float normal = 1 / magnitude;
+        return new PointF((point.x * normal), (point.y * normal));
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java b/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java
new file mode 100644
index 0000000..b1be811
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java
@@ -0,0 +1,187 @@
+/*
+ * 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.assist.ui;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.ColorInt;
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.metrics.LogMaker;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.WindowManager;
+import android.view.animation.PathInterpolator;
+import android.widget.FrameLayout;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.ScreenDecorations;
+import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.assist.AssistManager;
+
+/**
+ * Default UiController implementation. Shows white edge lights along the bottom of the phone,
+ * expanding from the corners to meet in the center.
+ */
+public class DefaultUiController implements AssistManager.UiController {
+
+    private static final String TAG = "DefaultUiController";
+
+    private static final long ANIM_DURATION_MS = 200;
+
+    protected final FrameLayout mRoot;
+
+    private final WindowManager mWindowManager;
+    private final WindowManager.LayoutParams mLayoutParams;
+    private final PathInterpolator mProgressInterpolator = new PathInterpolator(.83f, 0, .84f, 1);
+
+    private boolean mAttached = false;
+    private boolean mInvocationInProgress = false;
+    private float mLastInvocationProgress = 0;
+
+    private ValueAnimator mInvocationAnimator = new ValueAnimator();
+    private InvocationLightsView mInvocationLightsView;
+
+    public DefaultUiController(Context context) {
+        mRoot = new FrameLayout(context);
+        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+
+        mLayoutParams = new WindowManager.LayoutParams(
+                WindowManager.LayoutParams.MATCH_PARENT,
+                WindowManager.LayoutParams.WRAP_CONTENT, 0, 0,
+                WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
+                WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+                        | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
+                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+                PixelFormat.TRANSLUCENT);
+        mLayoutParams.privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
+        mLayoutParams.gravity = Gravity.BOTTOM;
+        mLayoutParams.setTitle("Assist");
+
+        mInvocationLightsView = (InvocationLightsView)
+                LayoutInflater.from(context).inflate(R.layout.invocation_lights, mRoot, false);
+        mRoot.addView(mInvocationLightsView);
+    }
+
+    @Override // AssistManager.UiController
+    public void processBundle(Bundle bundle) {
+        Log.e(TAG, "Bundle received but handling is not implemented; ignoring");
+    }
+
+    @Override // AssistManager.UiController
+    public void onInvocationProgress(int type, float progress) {
+        if (progress == 1) {
+            animateInvocationCompletion(type, 0);
+        } else if (progress == 0) {
+            hide();
+        } else {
+            if (!mInvocationInProgress) {
+                attach();
+                mInvocationInProgress = true;
+                updateAssistHandleVisibility();
+            }
+            setProgressInternal(type, progress);
+        }
+        mLastInvocationProgress = progress;
+
+        // Logs assistant invocation start.
+        if (!mInvocationInProgress && progress > 0.f) {
+            MetricsLogger.action(new LogMaker(MetricsEvent.ASSISTANT)
+                    .setType(MetricsEvent.TYPE_ACTION));
+        }
+        // Logs assistant invocation cancelled.
+        if (mInvocationInProgress && progress == 0f) {
+            MetricsLogger.action(new LogMaker(MetricsEvent.ASSISTANT)
+                    .setType(MetricsEvent.TYPE_DISMISS).setSubtype(0));
+        }
+    }
+
+    @Override // AssistManager.UiController
+    public void onGestureCompletion(float velocity) {
+        animateInvocationCompletion(AssistManager.INVOCATION_TYPE_GESTURE, velocity);
+    }
+
+    @Override // AssistManager.UiController
+    public void hide() {
+        Dependency.get(AssistManager.class).hideAssist();
+        detach();
+        if (mInvocationAnimator.isRunning()) {
+            mInvocationAnimator.cancel();
+        }
+        mInvocationLightsView.hide();
+        mInvocationInProgress = false;
+        updateAssistHandleVisibility();
+    }
+
+    /**
+     * Sets the colors of the four invocation lights, from left to right.
+     */
+    public void setInvocationColors(@ColorInt int color1, @ColorInt int color2,
+            @ColorInt int color3, @ColorInt int color4) {
+        mInvocationLightsView.setColors(color1, color2, color3, color4);
+    }
+
+    private void updateAssistHandleVisibility() {
+        ScreenDecorations decorations = SysUiServiceProvider.getComponent(mRoot.getContext(),
+                ScreenDecorations.class);
+        decorations.setAssistHintBlocked(mInvocationInProgress);
+    }
+
+    private void attach() {
+        if (!mAttached) {
+            mWindowManager.addView(mRoot, mLayoutParams);
+            mAttached = true;
+        }
+    }
+
+    private void detach() {
+        if (mAttached) {
+            mWindowManager.removeViewImmediate(mRoot);
+            mAttached = false;
+        }
+    }
+
+    private void setProgressInternal(int type, float progress) {
+        mInvocationLightsView.onInvocationProgress(
+                mProgressInterpolator.getInterpolation(progress));
+    }
+
+    private void animateInvocationCompletion(int type, float velocity) {
+        mInvocationAnimator = ValueAnimator.ofFloat(mLastInvocationProgress, 1);
+        mInvocationAnimator.setStartDelay(1);
+        mInvocationAnimator.setDuration(ANIM_DURATION_MS);
+        mInvocationAnimator.addUpdateListener(
+                animation -> setProgressInternal(type, (float) animation.getAnimatedValue()));
+        mInvocationAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                super.onAnimationEnd(animation);
+                mInvocationInProgress = false;
+                mLastInvocationProgress = 0;
+                hide();
+            }
+        });
+        mInvocationAnimator.start();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java b/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java
new file mode 100644
index 0000000..251229f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java
@@ -0,0 +1,128 @@
+/*
+ * 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.assist.ui;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+import android.view.Display;
+import android.view.Surface;
+
+/**
+ * Utility class for determining screen and corner dimensions.
+ */
+public class DisplayUtils {
+    /**
+     * Converts given distance from dp to pixels.
+     */
+    public static int convertDpToPx(float dp, Context context) {
+        Display d = context.getDisplay();
+
+        DisplayMetrics dm = new DisplayMetrics();
+        d.getRealMetrics(dm);
+
+        return (int) Math.ceil(dp * dm.density);
+    }
+
+    /**
+     * The width of the display.
+     *
+     * - Not affected by rotation.
+     * - Includes system decor.
+     */
+    public static int getWidth(Context context) {
+        Display d = context.getDisplay();
+
+        DisplayMetrics dm = new DisplayMetrics();
+        d.getRealMetrics(dm);
+
+        int rotation = d.getRotation();
+        if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
+            return dm.widthPixels;
+        } else {
+            return dm.heightPixels;
+        }
+    }
+
+    /**
+     * The height of the display.
+     *
+     * - Not affected by rotation.
+     * - Includes system decor.
+     */
+    public static int getHeight(Context context) {
+        Display d = context.getDisplay();
+
+        DisplayMetrics dm = new DisplayMetrics();
+        d.getRealMetrics(dm);
+
+        int rotation = d.getRotation();
+        if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
+            return dm.heightPixels;
+        } else {
+            return dm.widthPixels;
+        }
+    }
+
+    /**
+     * Returns the radius of the bottom corners (the distance from the true corner to the point
+     * where the curve ends), in pixels.
+     */
+    public static int getCornerRadiusBottom(Context context) {
+        int radius = 0;
+
+        int resourceId = context.getResources().getIdentifier("rounded_corner_radius_bottom",
+                "dimen", "android");
+        if (resourceId > 0) {
+            radius = context.getResources().getDimensionPixelSize(resourceId);
+        }
+
+        if (radius == 0) {
+            radius = getCornerRadiusDefault(context);
+        }
+        return radius;
+    }
+
+    /**
+     * Returns the radius of the top corners (the distance from the true corner to the point where
+     * the curve ends), in pixels.
+     */
+    public static int getCornerRadiusTop(Context context) {
+        int radius = 0;
+
+        int resourceId = context.getResources().getIdentifier("rounded_corner_radius_top",
+                "dimen", "android");
+        if (resourceId > 0) {
+            radius = context.getResources().getDimensionPixelSize(resourceId);
+        }
+
+        if (radius == 0) {
+            radius = getCornerRadiusDefault(context);
+        }
+        return radius;
+    }
+
+    private static int getCornerRadiusDefault(Context context) {
+        int radius = 0;
+
+        int resourceId = context.getResources().getIdentifier("rounded_corner_radius", "dimen",
+                "android");
+        if (resourceId > 0) {
+            radius = context.getResources().getDimensionPixelSize(resourceId);
+        }
+        return radius;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/EdgeLight.java b/packages/SystemUI/src/com/android/systemui/assist/ui/EdgeLight.java
new file mode 100644
index 0000000..9ae02c5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/assist/ui/EdgeLight.java
@@ -0,0 +1,99 @@
+/*
+ * 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.assist.ui;
+
+import androidx.annotation.ColorInt;
+
+/**
+ * Represents a line drawn on the perimeter of the display.
+ *
+ * Offsets and lengths are both normalized to the perimeter of the display – ex. a length of 1
+ * is equal to the perimeter of the display. Positions move counter-clockwise as values increase.
+ *
+ * If there is no bottom corner radius, the origin is the bottom-left corner.
+ * If there is a bottom corner radius, the origin is immediately after the bottom corner radius,
+ * counter-clockwise.
+ */
+public final class EdgeLight {
+    @ColorInt
+    private int mColor;
+    private float mOffset;
+    private float mLength;
+
+    /** Copies a list of EdgeLights. */
+    public static EdgeLight[] copy(EdgeLight[] array) {
+        EdgeLight[] copy = new EdgeLight[array.length];
+        for (int i = 0; i < array.length; i++) {
+            copy[i] = new EdgeLight(array[i]);
+        }
+        return copy;
+    }
+
+    public EdgeLight(@ColorInt int color, float offset, float length) {
+        mColor = color;
+        mOffset = offset;
+        mLength = length;
+    }
+
+    public EdgeLight(EdgeLight sourceLight) {
+        mColor = sourceLight.getColor();
+        mOffset = sourceLight.getOffset();
+        mLength = sourceLight.getLength();
+    }
+
+    /** Returns the current edge light color. */
+    @ColorInt
+    public int getColor() {
+        return mColor;
+    }
+
+    /** Sets the edge light color. */
+    public void setColor(@ColorInt int color) {
+        mColor = color;
+    }
+
+    /** Returns the edge light length, in units of the total device perimeter. */
+    public float getLength() {
+        return mLength;
+    }
+
+    /** Sets the edge light length, in units of the total device perimeter. */
+    public void setLength(float length) {
+        mLength = length;
+    }
+
+    /**
+     * Returns the current offset, in units of the total device perimeter and measured from the
+     * bottom-left corner (see class description).
+     */
+    public float getOffset() {
+        return mOffset;
+    }
+
+    /**
+     * Sets the current offset, in units of the total device perimeter and measured from the
+     * bottom-left corner (see class description).
+     */
+    public void setOffset(float offset) {
+        mOffset = offset;
+    }
+
+    /** Returns the center, measured from the bottom-left corner (see class description). */
+    public float getCenter() {
+        return mOffset + (mLength / 2.f);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/InvocationLightsView.java b/packages/SystemUI/src/com/android/systemui/assist/ui/InvocationLightsView.java
new file mode 100644
index 0000000..de1d7c8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/assist/ui/InvocationLightsView.java
@@ -0,0 +1,194 @@
+/*
+ * 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.assist.ui;
+
+import android.annotation.ColorInt;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.MathUtils;
+import android.view.View;
+
+import com.android.systemui.R;
+
+import java.util.ArrayList;
+
+/**
+ * Shows lights at the bottom of the phone, marking the invocation progress.
+ */
+public class InvocationLightsView extends View {
+
+    private static final String TAG = "InvocationLightsView";
+
+    private static final int LIGHT_HEIGHT_DP = 3;
+    // minimum light length as a fraction of the corner length
+    private static final float MINIMUM_CORNER_RATIO = .6f;
+
+    protected final ArrayList<EdgeLight> mAssistInvocationLights = new ArrayList<>();
+    protected final PerimeterPathGuide mGuide;
+
+    private final Paint mPaint = new Paint();
+    // Path used to render lights. One instance is used to draw all lights and is cached to avoid
+    // allocation on each frame.
+    private final Path mPath = new Path();
+    private final int mViewHeight;
+
+    // Allocate variable for screen location lookup to avoid memory alloc onDraw()
+    private int[] mScreenLocation = new int[2];
+
+    public InvocationLightsView(Context context) {
+        this(context, null);
+    }
+
+    public InvocationLightsView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public InvocationLightsView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public InvocationLightsView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        int strokeWidth = DisplayUtils.convertDpToPx(LIGHT_HEIGHT_DP, context);
+        mPaint.setStrokeWidth(strokeWidth);
+        mPaint.setStyle(Paint.Style.STROKE);
+        mPaint.setStrokeJoin(Paint.Join.MITER);
+        mPaint.setAntiAlias(true);
+
+        int cornerRadiusBottom = DisplayUtils.getCornerRadiusBottom(context);
+        int cornerRadiusTop = DisplayUtils.getCornerRadiusTop(context);
+        int displayWidth = DisplayUtils.getWidth(context);
+        int displayHeight = DisplayUtils.getHeight(context);
+        CircularCornerPathRenderer cornerPathRenderer = new CircularCornerPathRenderer(
+                cornerRadiusBottom, cornerRadiusTop, displayWidth, displayHeight);
+        mGuide = new PerimeterPathGuide(context, cornerPathRenderer,
+                strokeWidth / 2, displayWidth, displayHeight);
+
+        mViewHeight = Math.max(cornerRadiusBottom, cornerRadiusTop);
+
+        @ColorInt int lightColor = getResources().getColor(R.color.default_invocation_lights_color);
+        for (int i = 0; i < 4; i++) {
+            mAssistInvocationLights.add(new EdgeLight(lightColor, 0, 0));
+        }
+    }
+
+    /**
+     * Updates positions of the invocation lights based on the progress (a float between 0 and 1).
+     * The lights begin at the device corners and expand inward until they meet at the center.
+     */
+    public void onInvocationProgress(float progress) {
+        if (progress == 0) {
+            setVisibility(View.GONE);
+        } else {
+            float cornerLengthNormalized =
+                    mGuide.getRegionWidth(PerimeterPathGuide.Region.BOTTOM_LEFT);
+            float arcLengthNormalized = cornerLengthNormalized * MINIMUM_CORNER_RATIO;
+            float arcOffsetNormalized = (cornerLengthNormalized - arcLengthNormalized) / 2f;
+
+            float minLightLength = arcLengthNormalized / 2;
+            float maxLightLength = mGuide.getRegionWidth(PerimeterPathGuide.Region.BOTTOM) / 4f;
+
+            float lightLength = MathUtils.lerp(minLightLength, maxLightLength, progress);
+
+            float leftStart = (-cornerLengthNormalized + arcOffsetNormalized) * (1 - progress);
+            float rightStart = mGuide.getRegionWidth(PerimeterPathGuide.Region.BOTTOM)
+                    + (cornerLengthNormalized - arcOffsetNormalized) * (1 - progress);
+
+            setLight(0, leftStart, lightLength);
+            setLight(1, leftStart + lightLength, lightLength);
+            setLight(2, rightStart - (lightLength * 2), lightLength);
+            setLight(3, rightStart - lightLength, lightLength);
+            setVisibility(View.VISIBLE);
+        }
+        invalidate();
+    }
+
+    /**
+     * Hides and resets the invocation lights.
+     */
+    public void hide() {
+        setVisibility(GONE);
+        for (EdgeLight light : mAssistInvocationLights) {
+            light.setLength(0);
+        }
+    }
+
+    /**
+     * Sets the invocation light colors, from left to right.
+     */
+    public void setColors(@ColorInt int color1, @ColorInt int color2,
+            @ColorInt int color3, @ColorInt int color4) {
+        mAssistInvocationLights.get(0).setColor(color1);
+        mAssistInvocationLights.get(1).setColor(color2);
+        mAssistInvocationLights.get(2).setColor(color3);
+        mAssistInvocationLights.get(3).setColor(color4);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        getLayoutParams().height = mViewHeight;
+        requestLayout();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+
+        int rotation = getContext().getDisplay().getRotation();
+        mGuide.setRotation(rotation);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        // If the view doesn't take up the whole screen, offset the canvas by its translation
+        // distance such that PerimeterPathGuide's paths are drawn properly based upon the actual
+        // screen edges.
+        getLocationOnScreen(mScreenLocation);
+        canvas.translate(-mScreenLocation[0], -mScreenLocation[1]);
+
+        // if the lights are different colors, the inner ones need to be drawn last and with a
+        // square cap so that the join between lights is straight
+        mPaint.setStrokeCap(Paint.Cap.ROUND);
+        renderLight(mAssistInvocationLights.get(0), canvas);
+        renderLight(mAssistInvocationLights.get(3), canvas);
+
+        mPaint.setStrokeCap(Paint.Cap.SQUARE);
+        renderLight(mAssistInvocationLights.get(1), canvas);
+        renderLight(mAssistInvocationLights.get(2), canvas);
+    }
+
+    protected void setLight(int index, float offset, float length) {
+        if (index < 0 || index >= 4) {
+            Log.w(TAG, "invalid invocation light index: " + index);
+        }
+        mAssistInvocationLights.get(index).setOffset(offset);
+        mAssistInvocationLights.get(index).setLength(length);
+    }
+
+    private void renderLight(EdgeLight light, Canvas canvas) {
+        mGuide.strokeSegment(mPath, light.getOffset(), light.getOffset() + light.getLength());
+        mPaint.setColor(light.getColor());
+        canvas.drawPath(mPath, mPaint);
+    }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/PerimeterPathGuide.java b/packages/SystemUI/src/com/android/systemui/assist/ui/PerimeterPathGuide.java
new file mode 100644
index 0000000..8eea368
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/assist/ui/PerimeterPathGuide.java
@@ -0,0 +1,389 @@
+/*
+ * 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.assist.ui;
+
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.util.Log;
+import android.util.Pair;
+import android.view.Surface;
+
+import androidx.core.math.MathUtils;
+
+/**
+ * PerimeterPathGuide establishes a coordinate system for drawing paths along the perimeter of the
+ * screen. All positions around the perimeter have a coordinate [0, 1). The origin is the bottom
+ * left corner of the screen, to the right of the curved corner, if any. Coordinates increase
+ * counter-clockwise around the screen.
+ *
+ * Non-square screens require PerimeterPathGuide to be notified when the rotation changes, such that
+ * it can recompute the edge lengths for the coordinate system.
+ */
+public class PerimeterPathGuide {
+
+    private static final String TAG = "PerimeterPathGuide";
+
+    /**
+     * For convenience, labels sections of the device perimeter.
+     *
+     * Must be listed in CCW order.
+     */
+    public enum Region {
+        BOTTOM,
+        BOTTOM_RIGHT,
+        RIGHT,
+        TOP_RIGHT,
+        TOP,
+        TOP_LEFT,
+        LEFT,
+        BOTTOM_LEFT
+    }
+
+    private final int mDeviceWidthPx;
+    private final int mDeviceHeightPx;
+    private final int mTopCornerRadiusPx;
+    private final int mBottomCornerRadiusPx;
+
+    private class RegionAttributes {
+        public float absoluteLength;
+        public float normalizedLength;
+        public float endCoordinate;
+        public Path path;
+    }
+
+    // Allocate a Path and PathMeasure for use by intermediate operations that would otherwise have
+    // to allocate. reset() must be called before using this path, this ensures state from previous
+    // operations is cleared.
+    private final Path mScratchPath = new Path();
+    private final CornerPathRenderer mCornerPathRenderer;
+    private final PathMeasure mScratchPathMeasure = new PathMeasure(mScratchPath, false);
+    private RegionAttributes[] mRegions;
+    private final int mEdgeInset;
+    private int mRotation = ROTATION_0;
+
+    public PerimeterPathGuide(Context context, CornerPathRenderer cornerPathRenderer,
+            int edgeInset, int screenWidth, int screenHeight) {
+        mCornerPathRenderer = cornerPathRenderer;
+        mDeviceWidthPx = screenWidth;
+        mDeviceHeightPx = screenHeight;
+        mTopCornerRadiusPx = DisplayUtils.getCornerRadiusTop(context);
+        mBottomCornerRadiusPx = DisplayUtils.getCornerRadiusBottom(context);
+        mEdgeInset = edgeInset;
+
+        mRegions = new RegionAttributes[8];
+        for (int i = 0; i < mRegions.length; i++) {
+            mRegions[i] = new RegionAttributes();
+        }
+        computeRegions();
+    }
+
+    /**
+     * Sets the rotation.
+     *
+     * @param rotation one of Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180,
+     *                  Surface.ROTATION_270
+     */
+    public void setRotation(int rotation) {
+        if (rotation != mRotation) {
+            switch (rotation) {
+                case ROTATION_0:
+                case ROTATION_90:
+                case ROTATION_180:
+                case ROTATION_270:
+                    mRotation = rotation;
+                    computeRegions();
+                    break;
+                default:
+                    Log.e(TAG, "Invalid rotation provided: " + rotation);
+            }
+        }
+    }
+
+    /**
+     * Sets path to the section of the perimeter between startCoord and endCoord (measured
+     * counter-clockwise from the bottom left).
+     */
+    public void strokeSegment(Path path, float startCoord, float endCoord) {
+        path.reset();
+
+        startCoord = ((startCoord % 1) + 1) % 1;  // Wrap to the range [0, 1).
+        endCoord = ((endCoord % 1) + 1) % 1;  // Wrap to the range [0, 1).
+        boolean outOfOrder = startCoord > endCoord;
+
+        if (outOfOrder) {
+            strokeSegmentInternal(path, startCoord, 1f);
+            startCoord = 0;
+        }
+        strokeSegmentInternal(path, startCoord, endCoord);
+    }
+
+    /**
+     * Returns the device perimeter in pixels.
+     */
+    public float getPerimeterPx() {
+        float total = 0;
+        for (RegionAttributes region : mRegions) {
+            total += region.absoluteLength;
+        }
+        return total;
+    }
+
+    /**
+     * Returns the bottom corner radius in pixels.
+     */
+    public float getBottomCornerRadiusPx() {
+        return mBottomCornerRadiusPx;
+    }
+
+    /**
+     * Given a region and a progress value [0,1] indicating the counter-clockwise progress within
+     * that region, compute the global [0,1) coordinate.
+     */
+    public float getCoord(Region region, float progress) {
+        RegionAttributes regionAttributes = mRegions[region.ordinal()];
+        progress = MathUtils.clamp(progress, 0, 1);
+        return regionAttributes.endCoordinate - (1 - progress) * regionAttributes.normalizedLength;
+    }
+
+    /**
+     * Returns the center of the provided region, relative to the entire perimeter.
+     */
+    public float getRegionCenter(Region region) {
+        return getCoord(region, 0.5f);
+    }
+
+    /**
+     * Returns the width of the provided region, in units relative to the entire perimeter.
+     */
+    public float getRegionWidth(Region region) {
+        return mRegions[region.ordinal()].normalizedLength;
+    }
+
+    /**
+     * Points are expressed in terms of their relative position on the perimeter of the display,
+     * moving counter-clockwise. This method converts a point to clockwise, assisting use cases
+     * such as animating to a point clockwise instead of counter-clockwise.
+     *
+     * @param point A point in the range from 0 to 1.
+     * @return A point in the range of -1 to 0 that represents the same location as {@code point}.
+     */
+    public static float makeClockwise(float point) {
+        return point - 1;
+    }
+
+    private int getPhysicalCornerRadius(CircularCornerPathRenderer.Corner corner) {
+        if (corner == CircularCornerPathRenderer.Corner.BOTTOM_LEFT
+                || corner == CircularCornerPathRenderer.Corner.BOTTOM_RIGHT) {
+            return mBottomCornerRadiusPx;
+        }
+        return mTopCornerRadiusPx;
+    }
+
+    // Populate mRegions based upon the current rotation value.
+    private void computeRegions() {
+        int screenWidth = mDeviceWidthPx;
+        int screenHeight = mDeviceHeightPx;
+
+        int rotateMatrix = 0;
+
+        switch (mRotation) {
+            case ROTATION_90:
+                rotateMatrix = -90;
+                break;
+            case ROTATION_180:
+                rotateMatrix = -180;
+                break;
+            case Surface.ROTATION_270:
+                rotateMatrix = -270;
+                break;
+        }
+
+        Matrix matrix = new Matrix();
+        matrix.postRotate(rotateMatrix, mDeviceWidthPx / 2, mDeviceHeightPx / 2);
+
+        if (mRotation == ROTATION_90 || mRotation == Surface.ROTATION_270) {
+            screenHeight = mDeviceWidthPx;
+            screenWidth = mDeviceHeightPx;
+            matrix.postTranslate((mDeviceHeightPx
+                    - mDeviceWidthPx) / 2, (mDeviceWidthPx - mDeviceHeightPx) / 2);
+        }
+
+        CircularCornerPathRenderer.Corner screenBottomLeft = getRotatedCorner(
+                CircularCornerPathRenderer.Corner.BOTTOM_LEFT);
+        CircularCornerPathRenderer.Corner screenBottomRight = getRotatedCorner(
+                CircularCornerPathRenderer.Corner.BOTTOM_RIGHT);
+        CircularCornerPathRenderer.Corner screenTopLeft = getRotatedCorner(
+                CircularCornerPathRenderer.Corner.TOP_LEFT);
+        CircularCornerPathRenderer.Corner screenTopRight = getRotatedCorner(
+                CircularCornerPathRenderer.Corner.TOP_RIGHT);
+
+        mRegions[Region.BOTTOM_LEFT.ordinal()].path =
+                mCornerPathRenderer.getInsetPath(screenBottomLeft, mEdgeInset);
+        mRegions[Region.BOTTOM_RIGHT.ordinal()].path =
+                mCornerPathRenderer.getInsetPath(screenBottomRight, mEdgeInset);
+        mRegions[Region.TOP_RIGHT.ordinal()].path =
+                mCornerPathRenderer.getInsetPath(screenTopRight, mEdgeInset);
+        mRegions[Region.TOP_LEFT.ordinal()].path =
+                mCornerPathRenderer.getInsetPath(screenTopLeft, mEdgeInset);
+
+        mRegions[Region.BOTTOM_LEFT.ordinal()].path.transform(matrix);
+        mRegions[Region.BOTTOM_RIGHT.ordinal()].path.transform(matrix);
+        mRegions[Region.TOP_RIGHT.ordinal()].path.transform(matrix);
+        mRegions[Region.TOP_LEFT.ordinal()].path.transform(matrix);
+
+
+        Path bottomPath = new Path();
+        bottomPath.moveTo(getPhysicalCornerRadius(screenBottomLeft), screenHeight - mEdgeInset);
+        bottomPath.lineTo(screenWidth - getPhysicalCornerRadius(screenBottomRight),
+                screenHeight - mEdgeInset);
+        mRegions[Region.BOTTOM.ordinal()].path = bottomPath;
+
+        Path topPath = new Path();
+        topPath.moveTo(screenWidth - getPhysicalCornerRadius(screenTopRight), mEdgeInset);
+        topPath.lineTo(getPhysicalCornerRadius(screenTopLeft), mEdgeInset);
+        mRegions[Region.TOP.ordinal()].path = topPath;
+
+        Path rightPath = new Path();
+        rightPath.moveTo(screenWidth - mEdgeInset,
+                screenHeight - getPhysicalCornerRadius(screenBottomRight));
+        rightPath.lineTo(screenWidth - mEdgeInset, getPhysicalCornerRadius(screenTopRight));
+        mRegions[Region.RIGHT.ordinal()].path = rightPath;
+
+        Path leftPath = new Path();
+        leftPath.moveTo(mEdgeInset,
+                getPhysicalCornerRadius(screenTopLeft));
+        leftPath.lineTo(mEdgeInset, screenHeight - getPhysicalCornerRadius(screenBottomLeft));
+        mRegions[Region.LEFT.ordinal()].path = leftPath;
+
+        float perimeterLength = 0;
+        PathMeasure pathMeasure = new PathMeasure();
+        for (int i = 0; i < mRegions.length; i++) {
+            pathMeasure.setPath(mRegions[i].path, false);
+            mRegions[i].absoluteLength = pathMeasure.getLength();
+            perimeterLength += mRegions[i].absoluteLength;
+        }
+
+        float accum = 0;
+        for (int i = 0; i < mRegions.length; i++) {
+            mRegions[i].normalizedLength = mRegions[i].absoluteLength / perimeterLength;
+            accum += mRegions[i].normalizedLength;
+            mRegions[i].endCoordinate = accum;
+        }
+    }
+
+    private CircularCornerPathRenderer.Corner getRotatedCorner(
+            CircularCornerPathRenderer.Corner screenCorner) {
+        int corner = screenCorner.ordinal();
+        switch (mRotation) {
+            case ROTATION_90:
+                corner += 3;
+                break;
+            case ROTATION_180:
+                corner += 2;
+                break;
+            case Surface.ROTATION_270:
+                corner += 1;
+                break;
+        }
+        return CircularCornerPathRenderer.Corner.values()[corner % 4];
+    }
+
+    private void strokeSegmentInternal(Path path, float startCoord, float endCoord) {
+        Pair<Region, Float> startPoint = placePoint(startCoord);
+        Pair<Region, Float> endPoint = placePoint(endCoord);
+
+        if (startPoint.first.equals(endPoint.first)) {
+            strokeRegion(path, startPoint.first, startPoint.second, endPoint.second);
+        } else {
+            strokeRegion(path, startPoint.first, startPoint.second, 1f);
+            boolean hitStart = false;
+            for (Region r : Region.values()) {
+                if (r.equals(startPoint.first)) {
+                    hitStart = true;
+                    continue;
+                }
+                if (hitStart) {
+                    if (!r.equals(endPoint.first)) {
+                        strokeRegion(path, r, 0f, 1f);
+                    } else {
+                        strokeRegion(path, r, 0f, endPoint.second);
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+    private void strokeRegion(Path path, Region r, float relativeStart, float relativeEnd) {
+        if (relativeStart == relativeEnd) {
+            return;
+        }
+
+        mScratchPathMeasure.setPath(mRegions[r.ordinal()].path, false);
+        mScratchPathMeasure.getSegment(relativeStart * mScratchPathMeasure.getLength(),
+                relativeEnd * mScratchPathMeasure.getLength(), path, true);
+    }
+
+    /**
+     * Return the Region where the point is located, and its relative position within that region
+     * (from 0 to 1).
+     * Note that we move counterclockwise around the perimeter; for example, a relative position of
+     * 0 in
+     * the BOTTOM region is on the left side of the screen, but in the TOP region it’s on the
+     * right.
+     */
+    private Pair<Region, Float> placePoint(float coord) {
+        if (0 > coord || coord > 1) {
+            coord = ((coord % 1) + 1)
+                    % 1;  // Wrap to the range [0, 1). Inputs of exactly 1 are preserved.
+        }
+
+        Region r = getRegionForPoint(coord);
+        if (r.equals(Region.BOTTOM)) {
+            return Pair.create(r, coord / mRegions[r.ordinal()].normalizedLength);
+        } else {
+            float coordOffsetInRegion = coord - mRegions[r.ordinal() - 1].endCoordinate;
+            float coordRelativeToRegion =
+                    coordOffsetInRegion / mRegions[r.ordinal()].normalizedLength;
+            return Pair.create(r, coordRelativeToRegion);
+        }
+    }
+
+    private Region getRegionForPoint(float coord) {
+        // If coord is outside of [0,1], wrap to [0,1).
+        if (coord < 0 || coord > 1) {
+            coord = ((coord % 1) + 1) % 1;
+        }
+
+        for (Region region : Region.values()) {
+            if (coord <= mRegions[region.ordinal()].endCoordinate) {
+                return region;
+            }
+        }
+
+        // Should never happen.
+        Log.e(TAG, "Fell out of getRegionForPoint");
+        return Region.BOTTOM;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 4ec79a6..392183b 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.bubbles;
 
+import static android.app.Notification.FLAG_BUBBLE;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_BADGE;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
@@ -40,8 +41,6 @@
 
 import android.app.ActivityManager;
 import android.app.ActivityManager.RunningTaskInfo;
-import android.app.ActivityTaskManager;
-import android.app.IActivityTaskManager;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
@@ -53,6 +52,7 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.provider.Settings;
+import android.service.notification.NotificationListenerService.RankingMap;
 import android.service.notification.StatusBarNotification;
 import android.service.notification.ZenModeConfig;
 import android.util.Log;
@@ -138,7 +138,6 @@
 
     private final Context mContext;
     private final NotificationEntryManager mNotificationEntryManager;
-    private final IActivityTaskManager mActivityTaskManager;
     private final BubbleTaskStackListener mTaskStackListener;
     private BubbleStateChangeListener mStateChangeListener;
     private BubbleExpandListener mExpandListener;
@@ -250,7 +249,6 @@
         mStatusBarStateListener = new StatusBarStateListener();
         Dependency.get(StatusBarStateController.class).addCallback(mStatusBarStateListener);
 
-        mActivityTaskManager = ActivityTaskManager.getService();
         mTaskStackListener = new BubbleTaskStackListener();
         ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
 
@@ -509,6 +507,12 @@
                 updateBubble(entry);
             }
         }
+
+        @Override
+        public void onNotificationRankingUpdated(RankingMap rankingMap) {
+            // Forward to BubbleData to block any bubbles which should no longer be shown
+            mBubbleData.notificationRankingUpdated(rankingMap);
+        }
     };
 
     @SuppressWarnings("FieldCanBeLocal")
@@ -547,8 +551,11 @@
                     mNotificationEntryManager.performRemoveNotification(bubble.entry.notification,
                             UNDEFINED_DISMISS_REASON);
                 } else {
-                    // The notification is still in the shade but we've removed the bubble so
-                    // lets make sure NoMan knows it's not a bubble anymore
+                    // Update the flag for SysUI
+                    bubble.entry.notification.getNotification().flags &= ~FLAG_BUBBLE;
+
+                    // Make sure NoMan knows it's not a bubble anymore so anyone querying it will
+                    // get right result back
                     try {
                         mBarService.onNotificationBubbleChanged(bubble.getKey(),
                                 false /* isBubble */);
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index 6ab973e..5575b35 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
@@ -22,6 +22,8 @@
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.content.Context;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.NotificationListenerService.RankingMap;
 import android.util.Log;
 import android.util.Pair;
 
@@ -114,6 +116,8 @@
     // State tracked during an operation -- keeps track of what listener events to dispatch.
     private Update mStateChange;
 
+    private NotificationListenerService.Ranking mTmpRanking;
+
     private TimeSource mTimeSource = System::currentTimeMillis;
 
     @Nullable
@@ -193,6 +197,31 @@
         dispatchPendingChanges();
     }
 
+    /**
+     * Called when NotificationListener has received adjusted notification rank and reapplied
+     * filtering and sorting. This is used to dismiss any bubbles which should no longer be shown
+     * due to changes in permissions on the notification channel or the global setting.
+     *
+     * @param rankingMap the updated ranking map from NotificationListenerService
+     */
+    public void notificationRankingUpdated(RankingMap rankingMap) {
+        if (mTmpRanking == null) {
+            mTmpRanking = new NotificationListenerService.Ranking();
+        }
+
+        String[] orderedKeys = rankingMap.getOrderedKeys();
+        for (int i = 0; i < orderedKeys.length; i++) {
+            String key = orderedKeys[i];
+            if (hasBubbleWithKey(key)) {
+                rankingMap.getRanking(key, mTmpRanking);
+                if (!mTmpRanking.canBubble()) {
+                    doRemove(key, BubbleController.DISMISS_BLOCKED);
+                }
+            }
+        }
+        dispatchPendingChanges();
+    }
+
     private void doAdd(Bubble bubble) {
         if (DEBUG) {
             Log.d(TAG, "doAdd: " + bubble);
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 6de0fb5..771df2d 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -330,9 +330,7 @@
         mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER;
 
         mBubbleContainer = new PhysicsAnimationLayout(context);
-        mBubbleContainer.setMaxRenderedChildren(
-                getResources().getInteger(R.integer.bubbles_max_rendered));
-        mBubbleContainer.setController(mStackAnimationController);
+        mBubbleContainer.setActiveController(mStackAnimationController);
         mBubbleContainer.setElevation(elevation);
         mBubbleContainer.setClipChildren(false);
         addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
@@ -728,11 +726,10 @@
     public void updateBubbleOrder(List<Bubble> bubbles) {
         for (int i = 0; i < bubbles.size(); i++) {
             Bubble bubble = bubbles.get(i);
-            mBubbleContainer.moveViewTo(bubble.iconView, i);
+            mBubbleContainer.reorderView(bubble.iconView, i);
         }
     }
 
-
     /**
      * Changes the currently selected bubble. If the stack is already expanded, the newly selected
      * bubble will be shown immediately. This does not change the expanded state or change the
@@ -909,19 +906,17 @@
             };
 
             if (shouldExpand) {
-                mBubbleContainer.setController(mExpandedAnimationController);
-                mExpandedAnimationController.expandFromStack(
-                        mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
-                        /* collapseTo */,
-                        () -> {
-                            updatePointerPosition();
-                            updateAfter.run();
-                        } /* after */);
+                mBubbleContainer.setActiveController(mExpandedAnimationController);
+                mExpandedAnimationController.expandFromStack(() -> {
+                    updatePointerPosition();
+                    updateAfter.run();
+                } /* after */);
             } else {
                 mBubbleContainer.cancelAllAnimations();
                 mExpandedAnimationController.collapseBackToStack(
+                        mStackAnimationController.getStackPositionAlongNearestHorizontalEdge(),
                         () -> {
-                            mBubbleContainer.setController(mStackAnimationController);
+                            mBubbleContainer.setActiveController(mStackAnimationController);
                             updateAfter.run();
                         });
             }
@@ -1014,7 +1009,7 @@
         }
 
         mStackAnimationController.cancelStackPositionAnimations();
-        mBubbleContainer.setController(mStackAnimationController);
+        mBubbleContainer.setActiveController(mStackAnimationController);
         hideFlyoutImmediate();
 
         mDraggingInDismissTarget = false;
@@ -1112,6 +1107,10 @@
     /** Called when a gesture is completed or cancelled. */
     void onGestureFinished() {
         mIsGestureInProgress = false;
+
+        if (mIsExpanded) {
+            mExpandedAnimationController.onGestureFinished();
+        }
     }
 
     /** Prepares and starts the desaturate/darken animation on the bubble stack. */
@@ -1202,6 +1201,7 @@
      */
     void magnetToStackIfNeededThenAnimateDismissal(
             View touchedView, float velX, float velY, Runnable after) {
+        final View draggedOutBubble = mExpandedAnimationController.getDraggedOutBubble();
         final Runnable animateDismissal = () -> {
             mAfterMagnet = null;
 
@@ -1219,7 +1219,7 @@
                             resetDesaturationAndDarken();
                         });
             } else {
-                mExpandedAnimationController.dismissDraggedOutBubble(() -> {
+                mExpandedAnimationController.dismissDraggedOutBubble(draggedOutBubble, () -> {
                     mAnimatingMagnet = false;
                     mShowingDismiss = false;
                     mDraggingInDismissTarget = false;
@@ -1386,10 +1386,18 @@
                 };
 
                 // Post in case layout isn't complete and getWidth returns 0.
-                post(() -> mFlyout.showFlyout(
-                        updateMessage, mStackAnimationController.getStackPosition(), getWidth(),
-                        mStackAnimationController.isStackOnLeftSide(),
-                        bubble.iconView.getBadgeColor(), mAfterFlyoutHides));
+                post(() -> {
+                    // An auto-expanding bubble could have been posted during the time it takes to
+                    // layout.
+                    if (isExpanded()) {
+                        return;
+                    }
+
+                    mFlyout.showFlyout(
+                            updateMessage, mStackAnimationController.getStackPosition(), getWidth(),
+                            mStackAnimationController.isStackOnLeftSide(),
+                            bubble.iconView.getBadgeColor(), mAfterFlyoutHides);
+                });
             }
 
             mFlyout.removeCallbacks(mHideFlyout);
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
index 24337a3..1fa0e12 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
@@ -22,6 +22,7 @@
 import android.view.View;
 import android.view.WindowInsets;
 
+import androidx.annotation.Nullable;
 import androidx.dynamicanimation.animation.DynamicAnimation;
 import androidx.dynamicanimation.animation.SpringForce;
 
@@ -63,12 +64,16 @@
     private Point mDisplaySize;
     /** Size of dismiss target at bottom of screen. */
     private float mPipDismissHeight;
-    /** Max number of bubbles shown in row above expanded view.*/
-    private int mBubblesMaxRendered;
 
     /** Whether the dragged-out bubble is in the dismiss target. */
     private boolean mIndividualBubbleWithinDismissTarget = false;
 
+    private boolean mAnimatingExpand = false;
+    private boolean mAnimatingCollapse = false;
+    private Runnable mAfterExpand;
+    private Runnable mAfterCollapse;
+    private PointF mCollapsePoint;
+
     /**
      * Whether the dragged out bubble is springing towards the touch point, rather than using the
      * default behavior of moving directly to the touch point.
@@ -97,56 +102,60 @@
     private View mBubbleDraggingOut;
 
     /**
-     * Drag velocities for the dragging-out bubble when the drag finished. These are used by
-     * {@link #onChildRemoved} to animate out the bubble while respecting touch velocity.
-     */
-    private float mBubbleDraggingOutVelX;
-    private float mBubbleDraggingOutVelY;
-
-    @Override
-    protected void setLayout(PhysicsAnimationLayout layout) {
-        super.setLayout(layout);
-
-        final Resources res = layout.getResources();
-        mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
-        mBubblePaddingPx = res.getDimensionPixelSize(R.dimen.bubble_padding);
-        mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
-        mStatusBarHeight =
-                res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
-        mPipDismissHeight = res.getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height);
-        mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered);
-    }
-
-    /**
      * Animates expanding the bubbles into a row along the top of the screen.
      */
-    public void expandFromStack(PointF collapseTo, Runnable after) {
-        animationsForChildrenFromIndex(
-                0, /* startIndex */
-                new ChildAnimationConfigurator() {
-                    @Override
-                    public void configureAnimationForChildAtIndex(
-                            int index, PhysicsAnimationLayout.PhysicsPropertyAnimator animation) {
-                        animation.position(getBubbleLeft(index), getExpandedY());
-                    }
-            })
-            .startAll(after);
+    public void expandFromStack(Runnable after) {
+        mAnimatingCollapse = false;
+        mAnimatingExpand = true;
+        mAfterExpand = after;
 
-        mCollapseToPoint = collapseTo;
+        startOrUpdateExpandAnimation();
     }
 
     /** Animate collapsing the bubbles back to their stacked position. */
-    public void collapseBackToStack(Runnable after) {
-        // Stack to the left if we're going to the left, or right if not.
-        final float sideMultiplier = mLayout.isFirstChildXLeftOfCenter(mCollapseToPoint.x) ? -1 : 1;
+    public void collapseBackToStack(PointF collapsePoint, Runnable after) {
+        mAnimatingExpand = false;
+        mAnimatingCollapse = true;
+        mAfterCollapse = after;
+        mCollapsePoint = collapsePoint;
 
+        startOrUpdateCollapseAnimation();
+    }
+
+    private void startOrUpdateExpandAnimation() {
         animationsForChildrenFromIndex(
                 0, /* startIndex */
-                (index, animation) ->
+                (index, animation) -> animation.position(getBubbleLeft(index), getExpandedY()))
+                .startAll(() -> {
+                    mAnimatingExpand = false;
+
+                    if (mAfterExpand != null) {
+                        mAfterExpand.run();
+                    }
+
+                    mAfterExpand = null;
+                });
+    }
+
+    private void startOrUpdateCollapseAnimation() {
+        // Stack to the left if we're going to the left, or right if not.
+        final float sideMultiplier = mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x) ? -1 : 1;
+        animationsForChildrenFromIndex(
+                0, /* startIndex */
+                (index, animation) -> {
                     animation.position(
-                            mCollapseToPoint.x + (sideMultiplier * index * mStackOffsetPx),
-                            mCollapseToPoint.y))
-            .startAll(after /* endAction */);
+                            mCollapsePoint.x + (sideMultiplier * index * mStackOffsetPx),
+                            mCollapsePoint.y);
+                })
+                .startAll(() -> {
+                    mAnimatingCollapse = false;
+
+                    if (mAfterCollapse != null) {
+                        mAfterCollapse.run();
+                    }
+
+                    mAfterCollapse = null;
+                });
     }
 
     /** Prepares the given bubble to be dragged out. */
@@ -190,10 +199,10 @@
     }
 
     /** Plays a dismiss animation on the dragged out bubble. */
-    public void dismissDraggedOutBubble(Runnable after) {
+    public void dismissDraggedOutBubble(View bubble, Runnable after) {
         mIndividualBubbleWithinDismissTarget = false;
 
-        animationForChild(mBubbleDraggingOut)
+        animationForChild(bubble)
                 .withStiffness(SpringForce.STIFFNESS_HIGH)
                 .scaleX(1.1f)
                 .scaleY(1.1f)
@@ -203,6 +212,10 @@
         updateBubblePositions();
     }
 
+    @Nullable public View getDraggedOutBubble() {
+        return mBubbleDraggingOut;
+    }
+
     /** Magnets the given bubble to the dismiss target. */
     public void magnetBubbleToDismiss(
             View bubbleView, float velX, float velY, float destY, Runnable after) {
@@ -241,24 +254,17 @@
         final int index = mLayout.indexOfChild(bubbleView);
 
         animationForChildAtIndex(index)
-            .position(getBubbleLeft(index), getExpandedY())
-            .withPositionStartVelocities(velX, velY)
-            .start(() -> bubbleView.setTranslationZ(0f) /* after */);
+                .position(getBubbleLeft(index), getExpandedY())
+                .withPositionStartVelocities(velX, velY)
+                .start(() -> bubbleView.setTranslationZ(0f) /* after */);
 
-        mBubbleDraggingOut = null;
-        mBubbleDraggedOutEnough = false;
         updateBubblePositions();
     }
 
-    /**
-     * Sets configuration variables so that when the given bubble is removed, the animations are
-     * started with the given velocities.
-     */
-    public void prepareForDismissalWithVelocity(View bubbleView, float velX, float velY) {
-        mBubbleDraggingOut = bubbleView;
-        mBubbleDraggingOutVelX = velX;
-        mBubbleDraggingOutVelY = velY;
+    /** Resets bubble drag out gesture flags. */
+    public void onGestureFinished() {
         mBubbleDraggedOutEnough = false;
+        mBubbleDraggingOut = null;
     }
 
     /**
@@ -297,6 +303,23 @@
     }
 
     @Override
+    void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
+        final Resources res = layout.getResources();
+        mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
+        mBubblePaddingPx = res.getDimensionPixelSize(R.dimen.bubble_padding);
+        mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
+        mStatusBarHeight =
+                res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
+        mPipDismissHeight = res.getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height);
+
+        // Ensure that all child views are at 1x scale, and visible, in case they were animating
+        // in.
+        mLayout.setVisibility(View.VISIBLE);
+        animationsForChildrenFromIndex(0 /* startIndex */, (index, animation) ->
+                animation.scaleX(1f).scaleY(1f).alpha(1f)).startAll();
+    }
+
+    @Override
     Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
         return Sets.newHashSet(
                 DynamicAnimation.TRANSLATION_X,
@@ -325,14 +348,21 @@
 
     @Override
     void onChildAdded(View child, int index) {
-        child.setTranslationX(getXForChildAtIndex(index));
-
-        animationForChild(child)
-                .translationY(
-                        getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */
-                        getExpandedY() /* to */)
-                .start();
-        updateBubblePositions();
+        // If a bubble is added while the expand/collapse animations are playing, update the
+        // animation to include the new bubble.
+        if (mAnimatingExpand) {
+            startOrUpdateExpandAnimation();
+        } else if (mAnimatingCollapse) {
+            startOrUpdateCollapseAnimation();
+        } else {
+            child.setTranslationX(getXForChildAtIndex(index));
+            animationForChild(child)
+                    .translationY(
+                            getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */
+                            getExpandedY() /* to */)
+                    .start();
+            updateBubblePositions();
+        }
     }
 
     @Override
@@ -357,19 +387,15 @@
     }
 
     @Override
-    protected void setChildVisibility(View child, int index, int visibility) {
-        if (visibility == View.VISIBLE) {
-            // Set alpha to 0 but then become visible immediately so the animation is visible.
-            child.setAlpha(0f);
-            child.setVisibility(View.VISIBLE);
-        }
-
-        animationForChild(child)
-                .alpha(visibility == View.GONE ? 0f : 1f)
-                .start(() -> super.setChildVisibility(child, index, visibility) /* after */);
+    void onChildReordered(View child, int oldIndex, int newIndex) {
+        updateBubblePositions();
     }
 
     private void updateBubblePositions() {
+        if (mAnimatingExpand || mAnimatingCollapse) {
+            return;
+        }
+
         for (int i = 0; i < mLayout.getChildCount(); i++) {
             final View bubble = mLayout.getChildAt(i);
 
@@ -378,6 +404,7 @@
             if (bubble.equals(mBubbleDraggingOut)) {
                 return;
             }
+
             animationForChild(bubble)
                     .translationX(getBubbleLeft(i))
                     .start();
@@ -403,10 +430,7 @@
             return 0;
         }
         int bubbleCount = mLayout.getChildCount();
-        if (bubbleCount > mBubblesMaxRendered) {
-            // Only shown bubbles are relevant for calculating position.
-            bubbleCount = mBubblesMaxRendered;
-        }
+
         // Width calculations.
         double bubble = bubbleCount * mBubbleSizePx;
         float gap = (bubbleCount - 1) * mBubblePaddingPx;
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java
index 997d2c4..3a33392 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java
@@ -17,10 +17,12 @@
 package com.android.systemui.bubbles.animation;
 
 import android.content.Context;
+import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
 
+import androidx.annotation.Nullable;
 import androidx.dynamicanimation.animation.DynamicAnimation;
 import androidx.dynamicanimation.animation.SpringAnimation;
 import androidx.dynamicanimation.animation.SpringForce;
@@ -137,12 +139,33 @@
          */
         abstract void onChildRemoved(View child, int index, Runnable finishRemoval);
 
+        /** Called when a child view has been reordered in the view hierachy. */
+        abstract void onChildReordered(View child, int oldIndex, int newIndex);
+
+        /**
+         * Called when the controller is set as the active animation controller for the given
+         * layout. Once active, the controller can start animations using the animator instances
+         * returned by {@link #animationForChild}.
+         *
+         * While all animations started by the previous controller will be cancelled, the new
+         * controller should not make any assumptions about the state of the layout or its children.
+         * Their translation, alpha, scale, etc. values may have been changed by the previous
+         * controller and should be reset here if relevant.
+         */
+        abstract void onActiveControllerForLayout(PhysicsAnimationLayout layout);
+
         protected PhysicsAnimationLayout mLayout;
 
         PhysicsAnimationController() { }
 
+        /** Whether this controller is the currently active controller for its associated layout. */
+        protected boolean isActiveController() {
+            return this == mLayout.mController;
+        }
+
         protected void setLayout(PhysicsAnimationLayout layout) {
             this.mLayout = layout;
+            onActiveControllerForLayout(layout);
         }
 
         protected PhysicsAnimationLayout getLayout() {
@@ -150,15 +173,6 @@
         }
 
         /**
-         * Sets the child's visibility when it moves beyond or within the limits set by a call to
-         * {@link PhysicsAnimationLayout#setMaxRenderedChildren}. This can be overridden to animate
-         * this transition.
-         */
-        protected void setChildVisibility(View child, int index, int visibility) {
-            child.setVisibility(visibility);
-        }
-
-        /**
          * Returns a {@link PhysicsPropertyAnimator} instance for the given child view.
          */
         protected PhysicsPropertyAnimator animationForChild(View child) {
@@ -170,6 +184,9 @@
                 child.setTag(R.id.physics_animator_tag, animator);
             }
 
+            animator.clearAnimator();
+            animator.setAssociatedController(this);
+
             return animator;
         }
 
@@ -235,32 +252,17 @@
             new HashMap<>();
 
     /** The currently active animation controller. */
-    private PhysicsAnimationController mController;
-
-    /**
-     * The maximum number of children to render and animate at a time. See
-     * {@link #setMaxRenderedChildren}.
-     */
-    private int mMaxRenderedChildren = 5;
+    @Nullable protected PhysicsAnimationController mController;
 
     public PhysicsAnimationLayout(Context context) {
         super(context);
     }
 
     /**
-     * The maximum number of children to render and animate at a time. Any child views added beyond
-     * this limit will be set to {@link View#GONE}. If any animations attempt to run on the view,
-     * the corresponding property will be set with no animation.
-     */
-    public void setMaxRenderedChildren(int max) {
-        this.mMaxRenderedChildren = max;
-    }
-
-    /**
      * Sets the animation controller and constructs or reconfigures the layout's physics animations
      * to meet the controller's specifications.
      */
-    public void setController(PhysicsAnimationController controller) {
+    public void setActiveController(PhysicsAnimationController controller) {
         cancelAllAnimations();
         mEndActionForProperty.clear();
 
@@ -312,42 +314,11 @@
 
     @Override
     public void addView(View child, int index, ViewGroup.LayoutParams params) {
-        super.addView(child, index, params);
-
-        // Set up animations for the new view, if the controller is set. If it isn't set, we'll be
-        // setting up animations for all children when setController is called.
-        if (mController != null) {
-            for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) {
-                setUpAnimationForChild(property, child, index);
-            }
-
-            mController.onChildAdded(child, index);
-        }
-
-        setChildrenVisibility();
+        addViewInternal(child, index, params, false /* isReorder */);
     }
 
     @Override
     public void removeView(View view) {
-        removeViewAndThen(view, /* callback */ null);
-    }
-
-    @Override
-    public void removeViewAt(int index) {
-        removeView(getChildAt(index));
-    }
-
-    /** Immediately moves the view from wherever it currently is, to the given index. */
-    public void moveViewTo(View view, int index) {
-        super.removeView(view);
-        addView(view, index);
-    }
-
-    /**
-     * Let the controller know that this view should be removed, and then call the callback once the
-     * controller has finished any removal animations and the view has actually been removed.
-     */
-    public void removeViewAndThen(View view, Runnable callback) {
         if (mController != null) {
             final int index = indexOfChild(view);
 
@@ -355,8 +326,6 @@
             super.removeView(view);
             addTransientView(view, index);
 
-            setChildrenVisibility();
-
             // Tell the controller to animate this view out, and call the callback when it's
             // finished.
             mController.onChildRemoved(view, index, () -> {
@@ -364,19 +333,28 @@
                 // any are still running and then remove it.
                 cancelAnimationsOnView(view);
                 removeTransientView(view);
-
-                if (callback != null) {
-                    callback.run();
-                }
             });
         } else {
             // Without a controller, nobody will animate this view out, so it gets an unceremonious
             // departure.
             super.removeView(view);
+        }
+    }
 
-            if (callback != null) {
-                callback.run();
-            }
+    @Override
+    public void removeViewAt(int index) {
+        removeView(getChildAt(index));
+    }
+
+    /** Immediately re-orders the view to the given index. */
+    public void reorderView(View view, int index) {
+        final int oldIndex = indexOfChild(view);
+
+        super.removeView(view);
+        addViewInternal(view, index, view.getLayoutParams(), true /* isReorder */);
+
+        if (mController != null) {
+            mController.onChildReordered(view, oldIndex, index);
         }
     }
 
@@ -427,6 +405,10 @@
         }
     }
 
+    protected boolean isActiveController(PhysicsAnimationController controller) {
+        return mController == controller;
+    }
+
     /** Whether the first child would be left of center if translated to the given x value. */
     protected boolean isFirstChildXLeftOfCenter(float x) {
         if (getChildCount() > 0) {
@@ -454,6 +436,26 @@
     }
 
     /**
+     * Adds a view to the layout. If this addition is not the result of a call to
+     * {@link #reorderView}, this will also notify the controller via
+     * {@link PhysicsAnimationController#onChildAdded} and set up animations for the view.
+     */
+    private void addViewInternal(
+            View child, int index, ViewGroup.LayoutParams params, boolean isReorder) {
+        super.addView(child, index, params);
+
+        // Set up animations for the new view, if the controller is set. If it isn't set, we'll be
+        // setting up animations for all children when setActiveController is called.
+        if (mController != null && !isReorder) {
+            for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) {
+                setUpAnimationForChild(property, child, index);
+            }
+
+            mController.onChildAdded(child, index);
+        }
+    }
+
+    /**
      * Retrieves the animation of the given property from the view at the given index via the view
      * tag system.
      */
@@ -481,33 +483,16 @@
         SpringAnimation newAnim = new SpringAnimation(child, property);
         newAnim.addUpdateListener((animation, value, velocity) -> {
             final int indexOfChild = indexOfChild(child);
-            final int nextAnimInChain =
-                    mController.getNextAnimationInChain(property, indexOfChild);
+            final int nextAnimInChain = mController.getNextAnimationInChain(property, indexOfChild);
 
             if (nextAnimInChain == PhysicsAnimationController.NONE || indexOfChild < 0) {
                 return;
             }
 
-            final int animIndex = indexOfChild(child);
-            final float offset =
-                    mController.getOffsetForChainedPropertyAnimation(property);
-
-            // If this property's animations should be chained, then check to see if there is a
-            // subsequent animation within the rendering limit, and if so, tell it to animate to
-            // this animation's new value (plus the offset).
-            if (nextAnimInChain < Math.min(getChildCount(), mMaxRenderedChildren)) {
-                getAnimationAtIndex(property, animIndex + 1)
+            final float offset = mController.getOffsetForChainedPropertyAnimation(property);
+            if (nextAnimInChain < getChildCount()) {
+                getAnimationAtIndex(property, nextAnimInChain)
                         .animateToFinalPosition(value + offset);
-            } else if (nextAnimInChain < getChildCount()) {
-                // If the next child view is not rendered, update the property directly without
-                // animating it, so that the view is still in the correct state if it later
-                // becomes visible.
-                for (int i = nextAnimInChain; i < getChildCount(); i++) {
-                    // 'value' here is the value of the last child within the rendering limit,
-                    // not the first child's value - so we want to subtract the last child's
-                    // index when calculating the offset.
-                    property.setValue(getChildAt(i), value + offset * (i - animIndex));
-                }
             }
         });
 
@@ -516,22 +501,6 @@
         child.setTag(getTagIdForProperty(property), newAnim);
     }
 
-    /** Hides children beyond the max rendering count. */
-    private void setChildrenVisibility() {
-        for (int i = 0; i < getChildCount(); i++) {
-            final int targetVisibility = i < mMaxRenderedChildren ? View.VISIBLE : View.GONE;
-            final View targetView = getChildAt(i);
-
-            if (targetView.getVisibility() != targetVisibility) {
-                if (mController != null) {
-                    mController.setChildVisibility(targetView, i, targetVisibility);
-                } else {
-                    targetView.setVisibility(targetVisibility);
-                }
-            }
-        }
-    }
-
     /** Return a stable ID to use as a tag key for the given property's animations. */
     private int getTagIdForProperty(DynamicAnimation.ViewProperty property) {
         if (property.equals(DynamicAnimation.TRANSLATION_X)) {
@@ -592,7 +561,7 @@
         private View mView;
 
         /** Start velocity to use for all property animations. */
-        private float mDefaultStartVelocity = 0f;
+        private float mDefaultStartVelocity = -Float.MAX_VALUE;
 
         /** Start delay to use when start is called. */
         private long mStartDelay = 0;
@@ -625,6 +594,15 @@
          */
         private Map<DynamicAnimation.ViewProperty, Float> mAnimatedProperties = new HashMap<>();
 
+        /**
+         * All of the initial property values that have been set. These values will be instantly set
+         * when {@link #start} is called, just before the animation begins.
+         */
+        private Map<DynamicAnimation.ViewProperty, Float> mInitialPropertyValues = new HashMap<>();
+
+        /** The animation controller that last retrieved this animator instance. */
+        private PhysicsAnimationController mAssociatedController;
+
         protected PhysicsPropertyAnimator(View view) {
             this.mView = view;
         }
@@ -644,7 +622,7 @@
 
         /** Set the view's alpha value to 'from', then animate it to the given value. */
         public PhysicsPropertyAnimator alpha(float from, float to, Runnable... endActions) {
-            mView.setAlpha(from);
+            mInitialPropertyValues.put(DynamicAnimation.ALPHA, from);
             return alpha(to, endActions);
         }
 
@@ -656,7 +634,7 @@
         /** Set the view's translationX value to 'from', then animate it to the given value. */
         public PhysicsPropertyAnimator translationX(
                 float from, float to, Runnable... endActions) {
-            mView.setTranslationX(from);
+            mInitialPropertyValues.put(DynamicAnimation.TRANSLATION_X, from);
             return translationX(to, endActions);
         }
 
@@ -668,7 +646,7 @@
         /** Set the view's translationY value to 'from', then animate it to the given value. */
         public PhysicsPropertyAnimator translationY(
                 float from, float to, Runnable... endActions) {
-            mView.setTranslationY(from);
+            mInitialPropertyValues.put(DynamicAnimation.TRANSLATION_Y, from);
             return translationY(to, endActions);
         }
 
@@ -690,7 +668,7 @@
 
         /** Set the view's scaleX value to 'from', then animate it to the given value. */
         public PhysicsPropertyAnimator scaleX(float from, float to, Runnable... endActions) {
-            mView.setScaleX(from);
+            mInitialPropertyValues.put(DynamicAnimation.SCALE_X, from);
             return scaleX(to, endActions);
         }
 
@@ -701,7 +679,7 @@
 
         /** Set the view's scaleY value to 'from', then animate it to the given value. */
         public PhysicsPropertyAnimator scaleY(float from, float to, Runnable... endActions) {
-            mView.setScaleY(from);
+            mInitialPropertyValues.put(DynamicAnimation.SCALE_Y, from);
             return scaleY(to, endActions);
         }
 
@@ -750,6 +728,13 @@
          * animated property on every child (including chained animations) have ended.
          */
         public void start(Runnable... after) {
+            if (!isActiveController(mAssociatedController)) {
+                Log.w(TAG, "Only the active animation controller is allowed to start animations. "
+                        + "Use PhysicsAnimationLayout#setActiveController to set the active "
+                        + "animation controller.");
+                return;
+            }
+
             final Set<DynamicAnimation.ViewProperty> properties = getAnimatedProperties();
 
             // If there are end actions, set an end listener on the layout for all the properties
@@ -791,6 +776,10 @@
 
             // Actually start the animations.
             for (DynamicAnimation.ViewProperty property : properties) {
+                if (mInitialPropertyValues.containsKey(property)) {
+                    property.setValue(mView, mInitialPropertyValues.get(property));
+                }
+
                 final SpringForce defaultSpringForce = mController.getSpringForce(property, mView);
                 animateValueForChild(
                         property,
@@ -803,14 +792,7 @@
                         mEndActionsForProperty.get(property));
             }
 
-            // Clear out the animator.
-            mAnimatedProperties.clear();
-            mPositionStartVelocities.clear();
-            mDefaultStartVelocity = 0;
-            mStartDelay = 0;
-            mStiffness = -1;
-            mDampingRatio = -1;
-            mEndActionsForProperty.clear();
+            clearAnimator();
         }
 
         /** Returns the set of properties that will animate once {@link #start} is called. */
@@ -847,20 +829,50 @@
                     });
                 }
 
-                animation.getSpring().setStiffness(stiffness);
-                animation.getSpring().setDampingRatio(dampingRatio);
+                final SpringForce animationSpring = animation.getSpring();
 
-                if (startVel > 0) {
-                    animation.setStartVelocity(startVel);
+                if (animationSpring == null) {
+                    return;
                 }
 
+                final Runnable configureAndStartAnimation = () -> {
+                    animationSpring.setStiffness(stiffness);
+                    animationSpring.setDampingRatio(dampingRatio);
+
+                    if (startVel > -Float.MAX_VALUE) {
+                        animation.setStartVelocity(startVel);
+                    }
+
+                    animationSpring.setFinalPosition(value);
+                    animation.start();
+                };
+
                 if (startDelay > 0) {
-                    postDelayed(() -> animation.animateToFinalPosition(value), startDelay);
+                    postDelayed(configureAndStartAnimation, startDelay);
                 } else {
-                    animation.animateToFinalPosition(value);
+                    configureAndStartAnimation.run();
                 }
             }
         }
+
+        private void clearAnimator() {
+            mInitialPropertyValues.clear();
+            mAnimatedProperties.clear();
+            mPositionStartVelocities.clear();
+            mDefaultStartVelocity = -Float.MAX_VALUE;
+            mStartDelay = 0;
+            mStiffness = -1;
+            mDampingRatio = -1;
+            mEndActionsForProperty.clear();
+        }
+
+        /**
+         * Sets the controller that last retrieved this animator instance, so that we can prevent
+         * {@link #start} from actually starting animations if called by a non-active controller.
+         */
+        private void setAssociatedController(PhysicsAnimationController controller) {
+            mAssociatedController = controller;
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
index b9cdc844..ab8752e4 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
@@ -154,21 +154,6 @@
     /** Height of the status bar. */
     private float mStatusBarHeight;
 
-    @Override
-    protected void setLayout(PhysicsAnimationLayout layout) {
-        super.setLayout(layout);
-
-        Resources res = layout.getResources();
-        mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
-        mIndividualBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
-        mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding);
-        mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen);
-        mStackStartingVerticalOffset =
-                res.getDimensionPixelSize(R.dimen.bubble_stack_starting_offset_y);
-        mStatusBarHeight =
-                res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
-    }
-
     /**
      * Instantly move the first bubble to the given point, and animate the rest of the stack behind
      * it with the 'following' effect.
@@ -286,6 +271,8 @@
                 },
                 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
 
+        // If we're flinging now, there's no more touch event to catch up to.
+        mFirstBubbleSpringingToTouch = false;
         mIsMovingFromFlinging = true;
         return destinationRelativeX;
     }
@@ -656,19 +643,38 @@
 
         if (mLayout.getChildCount() > 0) {
             animationForChildAtIndex(0).translationX(mStackPosition.x).start();
+        } else {
+            // Set the start position back to the default since we're out of bubbles. New bubbles
+            // will then animate in from the start position.
+            mStackPosition = getDefaultStartPosition();
         }
     }
 
+    @Override
+    void onChildReordered(View child, int oldIndex, int newIndex) {}
+
+    @Override
+    void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
+        Resources res = layout.getResources();
+        mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
+        mIndividualBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
+        mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding);
+        mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen);
+        mStackStartingVerticalOffset =
+                res.getDimensionPixelSize(R.dimen.bubble_stack_starting_offset_y);
+        mStatusBarHeight =
+                res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
+    }
+
     /** Moves the stack, without any animation, to the starting position. */
     private void moveStackToStartPosition() {
         // Post to ensure that the layout's width and height have been calculated.
         mLayout.setVisibility(View.INVISIBLE);
         mLayout.post(() -> {
+            setStackPosition(mRestingStackPosition == null
+                    ? getDefaultStartPosition()
+                    : mRestingStackPosition);
             mStackMovedToStartPosition = true;
-            setStackPosition(
-                    mRestingStackPosition == null
-                            ? getDefaultStartPosition()
-                            : mRestingStackPosition);
             mLayout.setVisibility(View.VISIBLE);
 
             // Animate in the top bubble now that we're visible.
@@ -707,15 +713,20 @@
         Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y));
         mStackPosition.set(pos.x, pos.y);
 
-        mLayout.cancelAllAnimations();
-        cancelStackPositionAnimations();
+        // If we're not the active controller, we don't want to physically move the bubble views.
+        if (isActiveController()) {
+            mLayout.cancelAllAnimations();
+            cancelStackPositionAnimations();
 
-        // Since we're not using the chained animations, apply the offsets manually.
-        final float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
-        final float yOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_Y);
-        for (int i = 0; i < mLayout.getChildCount(); i++) {
-            mLayout.getChildAt(i).setTranslationX(pos.x + (i * xOffset));
-            mLayout.getChildAt(i).setTranslationY(pos.y + (i * yOffset));
+            // Since we're not using the chained animations, apply the offsets manually.
+            final float xOffset = getOffsetForChainedPropertyAnimation(
+                    DynamicAnimation.TRANSLATION_X);
+            final float yOffset = getOffsetForChainedPropertyAnimation(
+                    DynamicAnimation.TRANSLATION_Y);
+            for (int i = 0; i < mLayout.getChildCount(); i++) {
+                mLayout.getChildAt(i).setTranslationX(pos.x + (i * xOffset));
+                mLayout.getChildAt(i).setTranslationY(pos.y + (i * yOffset));
+            }
         }
     }
 
@@ -732,6 +743,10 @@
 
     /** Animates in the given bubble. */
     private void animateInBubble(View child) {
+        if (!isActiveController()) {
+            return;
+        }
+
         child.setTranslationY(mStackPosition.y);
 
         float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
diff --git a/packages/SystemUI/src/com/android/systemui/colorextraction/SysuiColorExtractor.java b/packages/SystemUI/src/com/android/systemui/colorextraction/SysuiColorExtractor.java
index 05665b5..835ffc9 100644
--- a/packages/SystemUI/src/com/android/systemui/colorextraction/SysuiColorExtractor.java
+++ b/packages/SystemUI/src/com/android/systemui/colorextraction/SysuiColorExtractor.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.colorextraction;
 
+import android.annotation.ColorInt;
 import android.app.WallpaperColors;
 import android.app.WallpaperManager;
 import android.content.Context;
@@ -113,7 +114,11 @@
         super.onColorsChanged(colors, which);
 
         if ((which & WallpaperManager.FLAG_SYSTEM) != 0) {
+            @ColorInt int oldColor = mWpHiddenColors.getMainColor();
             updateDefaultGradients(colors);
+            if (oldColor != mWpHiddenColors.getMainColor()) {
+                triggerColorsChanged(WallpaperManager.FLAG_SYSTEM);
+            }
         }
     }
 
@@ -121,6 +126,7 @@
     public void onUiModeChanged() {
         WallpaperColors systemColors = getWallpaperColors(WallpaperManager.FLAG_SYSTEM);
         updateDefaultGradients(systemColors);
+        triggerColorsChanged(WallpaperManager.FLAG_SYSTEM);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
index ff16ed0..dc97754 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -551,7 +551,7 @@
 
     private class EmergencyDialerAction extends EmergencyAction {
         private EmergencyDialerAction() {
-            super(R.drawable.ic_faster_emergency,
+            super(com.android.systemui.R.drawable.ic_emergency_star,
                     R.string.global_action_emergency);
         }
 
@@ -1588,17 +1588,13 @@
                 // Disable rotation suggestions, if enabled
                 setRotationSuggestionsEnabled(false);
 
-                FrameLayout panelContainer = new FrameLayout(mContext);
+                FrameLayout panelContainer =
+                        findViewById(com.android.systemui.R.id.global_actions_panel_container);
                 FrameLayout.LayoutParams panelParams =
                         new FrameLayout.LayoutParams(
                                 FrameLayout.LayoutParams.MATCH_PARENT,
-                                FrameLayout.LayoutParams.WRAP_CONTENT);
+                                FrameLayout.LayoutParams.MATCH_PARENT);
                 panelContainer.addView(mPanelController.getPanelContent(), panelParams);
-                addContentView(
-                        panelContainer,
-                        new ViewGroup.LayoutParams(
-                                ViewGroup.LayoutParams.MATCH_PARENT,
-                                ViewGroup.LayoutParams.MATCH_PARENT));
                 mBackgroundDrawable = mPanelController.getBackgroundDrawable();
                 mScrimAlpha = 1f;
             }
@@ -1606,8 +1602,10 @@
 
         private void initializeLayout() {
             setContentView(getGlobalActionsLayoutId(mContext));
+            fixNavBarClipping();
             mGlobalActionsLayout = findViewById(com.android.systemui.R.id.global_actions_view);
             mGlobalActionsLayout.setOutsideTouchListener(view -> dismiss());
+            ((View) mGlobalActionsLayout.getParent()).setOnClickListener(view -> dismiss());
             mGlobalActionsLayout.setListViewAccessibilityDelegate(new View.AccessibilityDelegate() {
                 @Override
                 public boolean dispatchPopulateAccessibilityEvent(
@@ -1630,6 +1628,15 @@
             getWindow().setBackgroundDrawable(mBackgroundDrawable);
         }
 
+        private void fixNavBarClipping() {
+            ViewGroup content = findViewById(android.R.id.content);
+            content.setClipChildren(false);
+            content.setClipToPadding(false);
+            ViewGroup contentParent = (ViewGroup) content.getParent();
+            contentParent.setClipChildren(false);
+            contentParent.setClipToPadding(false);
+        }
+
         private int getGlobalActionsLayoutId(Context context) {
             int rotation = RotationUtils.getRotation(context);
             boolean useGridLayout = isForceGridEnabled(context)
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsGridLayout.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsGridLayout.java
index 03165f4..e1462d1 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsGridLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsGridLayout.java
@@ -42,8 +42,6 @@
         listView.setReverseSublists(shouldReverseSublists());
         listView.setReverseItems(shouldReverseListItems());
         listView.setSwapRowsAndColumns(shouldSwapRowsAndColumns());
-
-        fixNavBarClipping();
     }
 
     @Override
@@ -75,19 +73,6 @@
         }
     }
 
-    /**
-     * Allows the dialog to clip over the navbar, which prevents shadows and animations from being
-     * cut off.
-     */
-    private void fixNavBarClipping() {
-        ViewGroup parent = (ViewGroup) this.getParent();
-        ViewGroup parentParent = (ViewGroup) parent.getParent();
-        parent.setClipChildren(false);
-        parent.setClipToPadding(false);
-        parentParent.setClipChildren(false);
-        parentParent.setClipToPadding(false);
-    }
-
     @Override
     protected ListGridLayout getListView() {
         return (ListGridLayout) super.getListView();
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
index 35b8d203..d4c7366 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
@@ -56,6 +56,8 @@
 import com.android.systemui.statusbar.policy.NextAlarmControllerImpl;
 import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.statusbar.policy.ZenModeControllerImpl;
+import com.android.systemui.util.wakelock.SettableWakeLock;
+import com.android.systemui.util.wakelock.WakeLock;
 
 import java.util.Date;
 import java.util.HashSet;
@@ -101,6 +103,8 @@
     private final Handler mHandler;
     private final AlarmManager.OnAlarmListener mUpdateNextAlarm = this::updateNextAlarm;
     private final HashSet<Integer> mMediaInvisibleStates;
+    private final Object mMediaToken = new Object();
+    private SettableWakeLock mMediaWakeLock;
     private ZenModeController mZenModeController;
     private String mDatePattern;
     private DateFormat mDateFormat;
@@ -114,7 +118,8 @@
     private PendingIntent mPendingIntent;
     protected NotificationMediaManager mMediaManager;
     private StatusBarStateController mStatusBarStateController;
-    protected MediaMetadata mMediaMetaData;
+    private CharSequence mMediaTitle;
+    private CharSequence mMediaArtist;
     protected boolean mDozing;
     private boolean mMediaIsVisible;
 
@@ -218,24 +223,18 @@
     }
 
     protected boolean needsMediaLocked() {
-        return mMediaMetaData != null && mMediaIsVisible && mDozing;
+        return !TextUtils.isEmpty(mMediaTitle) && mMediaIsVisible && mDozing;
     }
 
     protected void addMediaLocked(ListBuilder listBuilder) {
-        if (mMediaMetaData == null) {
+        if (TextUtils.isEmpty(mMediaTitle)) {
             return;
         }
+        listBuilder.setHeader(new ListBuilder.HeaderBuilder(mHeaderUri).setTitle(mMediaTitle));
 
-        CharSequence title = mMediaMetaData.getText(MediaMetadata.METADATA_KEY_TITLE);
-        if (TextUtils.isEmpty(title)) {
-            title = getContext().getResources().getString(R.string.music_controls_no_title);
-        }
-        listBuilder.setHeader(new ListBuilder.HeaderBuilder(mHeaderUri).setTitle(title));
-
-        CharSequence album = mMediaMetaData.getText(MediaMetadata.METADATA_KEY_ARTIST);
-        if (!TextUtils.isEmpty(album)) {
+        if (!TextUtils.isEmpty(mMediaArtist)) {
             RowBuilder albumBuilder = new RowBuilder(mMediaUri);
-            albumBuilder.setTitle(album);
+            albumBuilder.setTitle(mMediaArtist);
 
             Icon mediaIcon = mMediaManager == null ? null : mMediaManager.getMediaIcon();
             IconCompat mediaIconCompat = mediaIcon == null ? null
@@ -306,6 +305,8 @@
         mZenModeController.addCallback(this);
         mDatePattern = getContext().getString(R.string.system_ui_aod_date_pattern);
         mPendingIntent = PendingIntent.getActivity(getContext(), 0, new Intent(), 0);
+        mMediaWakeLock = new SettableWakeLock(WakeLock.createPartial(getContext(), "media"),
+                "media");
         KeyguardSliceProvider.sInstance = this;
         registerClockUpdate();
         updateClockLocked();
@@ -425,12 +426,42 @@
     public void onMetadataOrStateChanged(MediaMetadata metadata, @PlaybackState.State int state) {
         synchronized (this) {
             boolean nextVisible = !mMediaInvisibleStates.contains(state);
-            if (nextVisible == mMediaIsVisible && metadata == mMediaMetaData) {
-                return;
+            mHandler.removeCallbacksAndMessages(mMediaToken);
+            if (mMediaIsVisible && !nextVisible) {
+                // We need to delay this event for a few millis when stopping to avoid jank in the
+                // animation. The media app might not send its update when buffering, and the slice
+                // would end up without a header for 0.5 second.
+                mMediaWakeLock.setAcquired(true);
+                mHandler.postDelayed(() -> {
+                    updateMediaStateLocked(metadata, state);
+                    mMediaWakeLock.setAcquired(false);
+                }, mMediaToken, 2000);
+            } else {
+                mMediaWakeLock.setAcquired(false);
+                updateMediaStateLocked(metadata, state);
             }
-            mMediaMetaData = metadata;
-            mMediaIsVisible = nextVisible;
         }
+    }
+
+    private void updateMediaStateLocked(MediaMetadata metadata, @PlaybackState.State int state) {
+        boolean nextVisible = !mMediaInvisibleStates.contains(state);
+        CharSequence title = null;
+        if (metadata != null) {
+            title = metadata.getText(MediaMetadata.METADATA_KEY_TITLE);
+            if (TextUtils.isEmpty(title)) {
+                title = getContext().getResources().getString(R.string.music_controls_no_title);
+            }
+        }
+        CharSequence artist = metadata == null ? null : metadata.getText(
+                MediaMetadata.METADATA_KEY_ARTIST);
+
+        if (nextVisible == mMediaIsVisible && TextUtils.equals(title, mMediaTitle)
+                && TextUtils.equals(artist, mMediaArtist)) {
+            return;
+        }
+        mMediaTitle = title;
+        mMediaArtist = artist;
+        mMediaIsVisible = nextVisible;
         notifyChange();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index ec6cfe9..9616f0a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -1930,12 +1930,6 @@
                         +  " --> flags=0x" + Integer.toHexString(flags));
             }
 
-            // TODO(b/113914868): investigation log for disappearing home button
-            Log.d(TAG, "adjustStatusBarLocked (b/113914868): flags=" + flags
-                    + "mShowing=" + mShowing + " mStatusBarManager=" + mStatusBarManager
-                    + " mOccluded=" + mOccluded + " isSecure=" + isSecure()
-                    + " force=" + forceHideHomeRecentsButtons);
-
             mStatusBarManager.disable(flags);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
index 9842748..410a13e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
@@ -461,6 +461,11 @@
 
         if (mHeaderTextContainerAlphaAnimator != null) {
             mHeaderTextContainerAlphaAnimator.setPosition(keyguardExpansionFraction);
+            if (keyguardExpansionFraction > 0) {
+                mHeaderTextContainerView.setVisibility(VISIBLE);
+            } else {
+                mHeaderTextContainerView.setVisibility(INVISIBLE);
+            }
         }
         if (mPrivacyChipAlphaAnimator != null) {
             mPrivacyChip.setExpanded(expansionFraction > 0.5);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
index 9d5871e..c375574 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
@@ -169,6 +169,5 @@
     public interface NotificationSettingsListener {
 
         default void onStatusBarIconsBehaviorChanged(boolean hideSilentStatusIcons) { }
-
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index d202190..fc2705f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -294,17 +294,17 @@
         int backgroundTop = 0;
         int clipTopAmount = 0;
         float firstElementRoundness = 0.0f;
-        ExpandableNotificationRow previousRow = null;
+        ActivatableNotificationView previousRow = null;
 
         for (int i = 0; i < mHostLayout.getChildCount(); i++) {
             ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
 
-            if (!(child instanceof ExpandableNotificationRow)
-                    || child.getVisibility() == GONE) {
+            if (!(child instanceof ActivatableNotificationView)
+                    || child.getVisibility() == GONE || child == this) {
                 continue;
             }
 
-            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+            ActivatableNotificationView row = (ActivatableNotificationView) child;
             float notificationClipEnd;
             boolean aboveShelf = ViewState.getFinalTranslationZ(row) > baseZHeight
                     || row.isPinned();
@@ -324,43 +324,55 @@
             }
             int clipTop = updateNotificationClipHeight(row, notificationClipEnd, notGoneIndex);
             clipTopAmount = Math.max(clipTop, clipTopAmount);
-            float inShelfAmount = updateIconAppearance(row, expandAmount, scrolling, scrollingFast,
-                    expandingAnimated, isLastChild);
-            numViewsInShelf += inShelfAmount;
-            int ownColorUntinted = row.getBackgroundColorWithoutTint();
-            if (rowTranslationY >= shelfStart && mNotGoneIndex == -1) {
-                mNotGoneIndex = notGoneIndex;
-                setTintColor(previousColor);
-                setOverrideTintColor(colorTwoBefore, transitionAmount);
 
-            } else if (mNotGoneIndex == -1) {
-                colorTwoBefore = previousColor;
-                transitionAmount = inShelfAmount;
-            }
-            if (isLastChild) {
-                if (colorOfViewBeforeLast == NO_COLOR) {
+            // If the current row is an ExpandableNotificationRow, update its color, roundedness,
+            // and icon state.
+            if (row instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow expandableRow = (ExpandableNotificationRow) row;
+
+                float inShelfAmount = updateIconAppearance(expandableRow, expandAmount, scrolling,
+                        scrollingFast,
+                        expandingAnimated, isLastChild);
+                numViewsInShelf += inShelfAmount;
+                int ownColorUntinted = row.getBackgroundColorWithoutTint();
+                if (rowTranslationY >= shelfStart && mNotGoneIndex == -1) {
+                    mNotGoneIndex = notGoneIndex;
+                    setTintColor(previousColor);
+                    setOverrideTintColor(colorTwoBefore, transitionAmount);
+
+                } else if (mNotGoneIndex == -1) {
+                    colorTwoBefore = previousColor;
+                    transitionAmount = inShelfAmount;
+                }
+                if (isLastChild) {
+                    if (colorOfViewBeforeLast == NO_COLOR) {
+                        colorOfViewBeforeLast = ownColorUntinted;
+                    }
+                    row.setOverrideTintColor(colorOfViewBeforeLast, inShelfAmount);
+                } else {
                     colorOfViewBeforeLast = ownColorUntinted;
+                    row.setOverrideTintColor(NO_COLOR, 0 /* overrideAmount */);
                 }
-                row.setOverrideTintColor(colorOfViewBeforeLast, inShelfAmount);
-            } else {
-                colorOfViewBeforeLast = ownColorUntinted;
-                row.setOverrideTintColor(NO_COLOR, 0 /* overrideAmount */);
-            }
-            if (notGoneIndex != 0 || !aboveShelf) {
-                row.setAboveShelf(false);
-            }
-            if (notGoneIndex == 0) {
-                StatusBarIconView icon = row.getEntry().expandedIcon;
-                NotificationIconContainer.IconState iconState = getIconState(icon);
-                // The icon state might be null in rare cases where the notification is actually
-                // added to the layout, but not to the shelf. An example are replied messages, since
-                // they don't show up on AOD
-                if (iconState != null && iconState.clampedAppearAmount == 1.0f) {
-                    // only if the first icon is fully in the shelf we want to clip to it!
-                    backgroundTop = (int) (row.getTranslationY() - getTranslationY());
-                    firstElementRoundness = row.getCurrentTopRoundness();
+                if (notGoneIndex != 0 || !aboveShelf) {
+                    expandableRow.setAboveShelf(false);
                 }
+                if (notGoneIndex == 0) {
+                    StatusBarIconView icon = expandableRow.getEntry().expandedIcon;
+                    NotificationIconContainer.IconState iconState = getIconState(icon);
+                    // The icon state might be null in rare cases where the notification is actually
+                    // added to the layout, but not to the shelf. An example are replied messages,
+                    // since they don't show up on AOD
+                    if (iconState != null && iconState.clampedAppearAmount == 1.0f) {
+                        // only if the first icon is fully in the shelf we want to clip to it!
+                        backgroundTop = (int) (row.getTranslationY() - getTranslationY());
+                        firstElementRoundness = row.getCurrentTopRoundness();
+                    }
+                }
+
+                previousColor = ownColorUntinted;
+                notGoneIndex++;
             }
+
             if (row.isFirstInSection() && previousRow != null && previousRow.isLastInSection()) {
                 // If the top of the shelf is between the view before a gap and the view after a gap
                 // then we need to adjust the shelf's top roundness.
@@ -379,8 +391,6 @@
                     backgroundTop = (int) distanceToGapBottom;
                 }
             }
-            notGoneIndex++;
-            previousColor = ownColorUntinted;
             previousRow = row;
         }
         clipTransientViews();
@@ -497,7 +507,7 @@
      * Update the clipping of this view.
      * @return the amount that our own top should be clipped
      */
-    private int updateNotificationClipHeight(ExpandableNotificationRow row,
+    private int updateNotificationClipHeight(ActivatableNotificationView row,
             float notificationClipEnd, int childIndex) {
         float viewEnd = row.getTranslationY() + row.getActualHeight();
         boolean isPinned = (row.isPinned() || row.isHeadsUpAnimatingAway())
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryListener.java
index a5a6d87..1aa6bc9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryListener.java
@@ -16,6 +16,8 @@
 package com.android.systemui.statusbar.notification;
 
 import android.annotation.Nullable;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.NotificationListenerService.RankingMap;
 import android.service.notification.StatusBarNotification;
 
 import com.android.internal.statusbar.NotificationVisibility;
@@ -98,4 +100,14 @@
             @Nullable NotificationVisibility visibility,
             boolean removedByUser) {
     }
+
+    /**
+     * Called whenever notification ranking changes, in response to
+     * {@link NotificationListenerService#onNotificationRankingUpdate}. This is called after
+     * NotificationData has processed the update and notifications have been re-sorted and filtered.
+     *
+     * @param rankingMap provides access to ranking information on currently active notifications
+     */
+    default void onNotificationRankingUpdated(RankingMap rankingMap) {
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
index 3ac5768..879a8df 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
@@ -355,10 +355,10 @@
                     // a child we're keeping around for reply!
                     continue;
                 }
-                entry.setKeepInParent(true);
+                childEntry.setKeepInParent(true);
                 // we need to set this state earlier as otherwise we might generate some weird
                 // animations
-                entry.removeRow();
+                childEntry.removeRow();
             }
         }
     }
@@ -483,6 +483,10 @@
         }
 
         updateNotifications();
+
+        for (NotificationEntryListener listener : mNotificationEntryListeners) {
+            listener.onNotificationRankingUpdated(rankingMap);
+        }
     }
 
     private void updateRankingOfPendingNotifications(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
index c65e90e..7c193b1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
@@ -65,7 +65,7 @@
     private val mDozeParameters: DozeParameters;
     var willWakeUp = false
         set(value) {
-            if (value && mDozeAmount != 0.0f) {
+            if (!value || mDozeAmount != 0.0f) {
                 field = value
             }
         }
@@ -228,7 +228,7 @@
                     // if we animate, we see the shelf briefly visible. Instead we fully animate
                     // the notification and its background out
                     animate = false
-                } else {
+                } else if (!mWakingUp && !willWakeUp){
                     entry.setAmbientGoingAway(true)
                     mEntrySetToClearWhenFinished.add(entry)
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationData.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationData.java
index 04ff58b..64b2f04 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationData.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationData.java
@@ -25,6 +25,7 @@
 import android.service.notification.SnoozeCriterion;
 import android.service.notification.StatusBarNotification;
 import android.util.ArrayMap;
+import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.Dependency;
@@ -69,7 +70,8 @@
         mHeadsUpManager = headsUpManager;
     }
 
-    private final Comparator<NotificationEntry> mRankingComparator =
+    @VisibleForTesting
+    protected final Comparator<NotificationEntry> mRankingComparator =
             new Comparator<NotificationEntry>() {
         private final Ranking mRankingA = new Ranking();
         private final Ranking mRankingB = new Ranking();
@@ -120,6 +122,8 @@
             } else if (aSystemMax != bSystemMax) {
                 // Upsort PRIORITY_MAX system notifications
                 return aSystemMax ? -1 : 1;
+            } else if (a.isHighPriority() != b.isHighPriority()) {
+                return -1 * Boolean.compare(a.isHighPriority(), b.isHighPriority());
             } else if (aRank != bRank) {
                 return aRank - bRank;
             } else {
@@ -231,17 +235,14 @@
 
     /**
      * Returns true if this notification should be displayed in the high-priority notifications
-     * section (and on the lockscreen and status bar).
+     * section
      */
     public boolean isHighPriority(StatusBarNotification statusBarNotification) {
         if (mRankingMap != null) {
             getRanking(statusBarNotification.getKey(), mTmpRanking);
             if (mTmpRanking.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT
-                    || isImportantOngoing(statusBarNotification.getNotification())
-                    || statusBarNotification.getNotification().hasMediaSession()
-                    || hasPerson(statusBarNotification.getNotification())
-                    || hasStyle(statusBarNotification.getNotification(),
-                    Notification.MessagingStyle.class)) {
+                    || hasHighPriorityCharacteristics(
+                            mTmpRanking.getChannel(), statusBarNotification)) {
                 return true;
             }
             if (mGroupManager.isSummaryOfGroup(statusBarNotification)) {
@@ -257,6 +258,25 @@
         return false;
     }
 
+    private boolean hasHighPriorityCharacteristics(NotificationChannel channel,
+            StatusBarNotification statusBarNotification) {
+
+        if (isImportantOngoing(statusBarNotification.getNotification())
+                || statusBarNotification.getNotification().hasMediaSession()
+                || hasPerson(statusBarNotification.getNotification())
+                || hasStyle(statusBarNotification.getNotification(),
+                Notification.MessagingStyle.class)) {
+            // Users who have long pressed and demoted to silent should not see the notification
+            // in the top section
+            if (channel != null && channel.hasUserSetImportance()) {
+                return false;
+            }
+            return true;
+        }
+
+        return false;
+    }
+
     private boolean isImportantOngoing(Notification notification) {
         return notification.isForegroundService()
                 && mTmpRanking.getImportance() >= NotificationManager.IMPORTANCE_LOW;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index a3e18ef..92c261c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -122,7 +122,6 @@
     public boolean suspended;
 
     private NotificationEntry parent; // our parent (if we're in a group)
-    private ArrayList<NotificationEntry> children = new ArrayList<NotificationEntry>();
     private ExpandableNotificationRow row; // the outer expanded view
 
     private int mCachedContrastColor = COLOR_INVALID;
@@ -277,10 +276,20 @@
 
     @Nullable
     public List<NotificationEntry> getChildren() {
-        if (children.size() <= 0) {
+        if (row == null) {
             return null;
         }
 
+        List<ExpandableNotificationRow> rowChildren = row.getNotificationChildren();
+        if (rowChildren == null) {
+            return null;
+        }
+
+        ArrayList<NotificationEntry> children = new ArrayList<>();
+        for (ExpandableNotificationRow child : rowChildren) {
+            children.add(child.getEntry());
+        }
+
         return children;
     }
 
@@ -740,7 +749,9 @@
         if (notification == null || !notification.isClearable()) {
             return false;
         }
-        if (children.size() > 0) {
+
+        List<NotificationEntry> children = getChildren();
+        if (children != null && children.size() > 0) {
             for (int i = 0; i < children.size(); i++) {
                 NotificationEntry child =  children.get(i);
                 if (!child.isClearable()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ButtonLinearLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ButtonLinearLayout.java
new file mode 100644
index 0000000..94bdd81
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ButtonLinearLayout.java
@@ -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.statusbar.notification.row;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Button;
+import android.widget.LinearLayout;
+
+public class ButtonLinearLayout extends LinearLayout {
+
+    public ButtonLinearLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return Button.class.getName();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
index b89b5cb..5378f90 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
@@ -43,6 +43,18 @@
 
 const val TAG = "ChannelDialogController"
 
+/**
+ * ChannelEditorDialogController is the controller for the dialog half-shelf
+ * that allows users to quickly turn off channels. It is launched from the NotificationInfo
+ * guts view and displays controls for toggling app notifications as well as up to 4 channels
+ * from that app like so:
+ *
+ *   APP TOGGLE                                                 <on/off>
+ *   - Channel from which we launched                           <on/off>
+ *   -                                                          <on/off>
+ *   - the next 3 channels sorted alphabetically for that app   <on/off>
+ *   -                                                          <on/off>
+ */
 @Singleton
 class ChannelEditorDialogController @Inject constructor(
     c: Context,
@@ -58,6 +70,9 @@
     private var appName: String? = null
     private var onSettingsClickListener: NotificationInfo.OnSettingsClickListener? = null
 
+    // Caller should set this if they care about when we dismiss
+    var onFinishListener: OnChannelEditorDialogFinishedListener? = null
+
     // Channels handed to us from NotificationInfo
     @VisibleForTesting
     internal val providedChannels = mutableListOf<NotificationChannel>()
@@ -144,6 +159,7 @@
     private fun done() {
         resetState()
         dialog.dismiss()
+        onFinishListener?.onChannelEditorDialogFinished()
     }
 
     private fun resetState() {
@@ -223,6 +239,8 @@
         dialog = Dialog(context)
 
         dialog.window?.requestFeature(Window.FEATURE_NO_TITLE)
+        // Prevent a11y readers from reading the first element in the dialog twice
+        dialog.setTitle("\u00A0")
         dialog.apply {
             setContentView(R.layout.notif_half_shelf)
             setCanceledOnTouchOutside(true)
@@ -240,7 +258,7 @@
 
             findViewById<TextView>(R.id.see_more_button)?.setOnClickListener {
                 onSettingsClickListener?.onClick(it, null, appUid!!)
-                dismiss()
+                done()
             }
 
             window?.apply {
@@ -265,3 +283,7 @@
             or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
             or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED)
 }
+
+interface OnChannelEditorDialogFinishedListener {
+    fun onChannelEditorDialogFinished()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
index 4d49760..6fe1477 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
@@ -143,7 +143,7 @@
     private fun updateViews() {
         val nc = channel ?: return
 
-        channelName.text = nc.name ?: "(missing)"
+        channelName.text = nc.name ?: ""
 
         nc.group?.let { groupId ->
             channelDescription.text = controller.groupNameForId(groupId)
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 dbc9a8b..d625b31 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
@@ -648,6 +648,9 @@
         if (!getShowingLayout().isDimmable()) {
             return false;
         }
+        if (showingAmbientPulsing()) {
+            return false;
+        }
         return super.isDimmable();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
index e4e8c80..626701c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
@@ -20,6 +20,10 @@
 import static android.app.NotificationManager.IMPORTANCE_LOW;
 import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
 
+import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
@@ -40,11 +44,12 @@
 import android.metrics.LogMaker;
 import android.os.Handler;
 import android.os.RemoteException;
-import android.provider.Settings;
 import android.service.notification.StatusBarNotification;
 import android.text.TextUtils;
-import android.transition.AutoTransition;
+import android.transition.ChangeBounds;
+import android.transition.Fade;
 import android.transition.TransitionManager;
+import android.transition.TransitionSet;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.View;
@@ -62,6 +67,7 @@
 import com.android.systemui.R;
 import com.android.systemui.statusbar.notification.logging.NotificationCounters;
 
+import java.lang.annotation.Retention;
 import java.util.List;
 import java.util.Set;
 
@@ -92,9 +98,8 @@
     // standard controls
     private static final int ACTION_ALERT = 5;
 
-    private static final int BUTTON_ANIM_TIME_MS = 200;
-
-    private static final boolean SHOW_BUTTON_SUMMARY = false;
+    private TextView mPriorityDescriptionView;
+    private TextView mSilentDescriptionView;
 
     private INotificationManager mINotificationManager;
     private PackageManager mPm;
@@ -110,8 +115,6 @@
     private NotificationChannel mSingleNotificationChannel;
     private int mStartingChannelImportance;
     private boolean mWasShownHighPriority;
-    private boolean mShowOnLockscreen;
-    private boolean mShowInStatusBar;
     private boolean mPressedApply;
 
     /**
@@ -129,8 +132,6 @@
     private OnSettingsClickListener mOnSettingsClickListener;
     private OnAppSettingsClickListener mAppSettingsClickListener;
     private NotificationGuts mGutsContainer;
-    private Drawable mSelectedBackground;
-    private Drawable mUnselectedBackground;
     private Drawable mPkgIcon;
 
     /** Whether this view is being shown as part of the blocking helper. */
@@ -145,28 +146,26 @@
     private OnClickListener mOnAlert = v -> {
         mExitReason = NotificationCounters.BLOCKING_HELPER_KEEP_SHOWING;
         mChosenImportance = IMPORTANCE_DEFAULT;
-        setImportanceSummary(ACTION_ALERT, true);
-        updateButtons(ACTION_ALERT);
+        applyAlertingBehavior(BEHAVIOR_ALERTING, true /* userTriggered */);
     };
 
     // used by standard ui
     private OnClickListener mOnSilent = v -> {
         mExitReason = NotificationCounters.BLOCKING_HELPER_DELIVER_SILENTLY;
         mChosenImportance = IMPORTANCE_LOW;
-        setImportanceSummary(ACTION_TOGGLE_SILENT, true);
-        updateButtons(ACTION_TOGGLE_SILENT);
+        applyAlertingBehavior(BEHAVIOR_SILENT, true /* userTriggered */);
     };
 
     // used by standard ui
     private OnClickListener mOnDismissSettings = v -> {
         mPressedApply = true;
-        closeControls(v);
+        closeControls(v, true);
     };
 
     // used by blocking helper
     private OnClickListener mOnKeepShowing = v -> {
         mExitReason = NotificationCounters.BLOCKING_HELPER_KEEP_SHOWING;
-        closeControls(v);
+        closeControls(v, true);
         mMetricsLogger.write(getLogMaker().setCategory(
                 MetricsEvent.NOTIFICATION_BLOCKING_HELPER)
                 .setType(MetricsEvent.TYPE_ACTION)
@@ -218,6 +217,14 @@
         super(context, attrs);
     }
 
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        mPriorityDescriptionView = findViewById(R.id.alert_summary);
+        mSilentDescriptionView = findViewById(R.id.silence_summary);
+    }
+
     // Specify a CheckSaveListener to override when/if the user's changes are committed.
     public interface CheckSaveListener {
         // Invoked when importance has changed and the NotificationInfo wants to try to save it.
@@ -293,9 +300,6 @@
         mDelegatePkg = mSbn.getOpPkg();
         mIsDeviceProvisioned = isDeviceProvisioned;
 
-        mSelectedBackground = mContext.getDrawable(R.drawable.button_border_selected);
-        mUnselectedBackground = mContext.getDrawable(R.drawable.button_border_unselected);
-
         int numTotalChannels = mINotificationManager.getNumNotificationChannelsForPackage(
                 pkg, mAppUid, false /* includeDeleted */);
         if (mNumUniqueChannelsInRow == 0) {
@@ -308,11 +312,6 @@
                     && numTotalChannels == 1;
         }
 
-        mShowInStatusBar = !mINotificationManager.shouldHideSilentStatusIcons(
-                mContext.getPackageName());
-        mShowOnLockscreen = Settings.Secure.getInt(mContext.getContentResolver(),
-                Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, 0) == 1;
-
         bindHeader();
         bindChannelDetails();
 
@@ -376,13 +375,9 @@
         silent.setOnClickListener(mOnSilent);
         alert.setOnClickListener(mOnAlert);
 
-        if (mWasShownHighPriority) {
-            updateButtons(ACTION_ALERT);
-            setImportanceSummary(ACTION_ALERT, false);
-        } else {
-            updateButtons(ACTION_TOGGLE_SILENT);
-            setImportanceSummary(ACTION_TOGGLE_SILENT, false);
-        }
+        applyAlertingBehavior(
+                mWasShownHighPriority ? BEHAVIOR_ALERTING : BEHAVIOR_SILENT,
+                false /* userTriggered */);
     }
 
     private void bindHeader() {
@@ -450,6 +445,8 @@
             if (mChannelEditorDialogController != null) {
                 mChannelEditorDialogController.prepareDialogForApp(mAppName, mPackageName, mAppUid,
                         mUniqueChannelsInRow, mPkgIcon, mOnSettingsClickListener);
+                mChannelEditorDialogController.setOnFinishListener(
+                        () -> closeControls(this, false));
                 mChannelEditorDialogController.show();
             }
         });
@@ -545,66 +542,45 @@
         }
     }
 
-    private void updateButtons(int blockState) {
-        View silence = findViewById(R.id.silence);
-        View alert = findViewById(R.id.alert);
-        TextView done = findViewById(R.id.done);
-        switch (blockState) {
-            case ACTION_TOGGLE_SILENT:
-                updateButtons(silence, alert);
-                if (mWasShownHighPriority) {
-                    done.setText(R.string.inline_ok_button);
-                } else {
-                    done.setText(R.string.inline_done_button);
-                }
-                break;
-            case ACTION_ALERT:
-                updateButtons(alert, silence);
-                if (mWasShownHighPriority) {
-                    done.setText(R.string.inline_done_button);
-                } else {
-                    done.setText(R.string.inline_ok_button);
-                }
-                break;
-        }
-    }
-
-    private void updateButtons(View selected, View unselected) {
-        selected.setBackground(mSelectedBackground);
-        selected.setSelected(true);
-        unselected.setBackground(mUnselectedBackground);
-        unselected.setSelected(false);
-    }
-
-    void setImportanceSummary(int blockState, boolean userTriggered) {
+    private void applyAlertingBehavior(@AlertingBehavior int behavior, boolean userTriggered) {
         if (userTriggered) {
-            AutoTransition transition = new AutoTransition();
-            transition.setDuration(BUTTON_ANIM_TIME_MS);
+            TransitionSet transition = new TransitionSet();
+            transition.setOrdering(TransitionSet.ORDERING_TOGETHER);
+            transition.addTransition(new Fade(Fade.OUT))
+                    .addTransition(new ChangeBounds())
+                    .addTransition(
+                            new Fade(Fade.IN)
+                                    .setStartDelay(150)
+                                    .setDuration(200)
+                                    .setInterpolator(FAST_OUT_SLOW_IN));
+            transition.setDuration(350);
+            transition.setInterpolator(FAST_OUT_SLOW_IN);
             TransitionManager.beginDelayedTransition(this, transition);
         }
-        if (SHOW_BUTTON_SUMMARY) {
-            if (blockState == ACTION_ALERT) {
-                TextView view = findViewById(R.id.alert_summary);
-                view.setVisibility(VISIBLE);
-                findViewById(R.id.silence_summary).setVisibility(GONE);
-                view.setText(R.string.notification_channel_summary_default);
-            } else {
-                TextView view = findViewById(R.id.silence_summary);
-                view.setVisibility(VISIBLE);
-                findViewById(R.id.alert_summary).setVisibility(GONE);
-                if (mShowInStatusBar) {
-                    if (mShowOnLockscreen) {
-                        view.setText(R.string.notification_channel_summary_low_status_lock);
-                    } else {
-                        view.setText(R.string.notification_channel_summary_low_status);
-                    }
-                } else if (mShowOnLockscreen) {
-                    view.setText(R.string.notification_channel_summary_low_lock);
-                } else {
-                    view.setText(R.string.notification_channel_summary_low);
-                }
-            }
+
+        View alert = findViewById(R.id.alert);
+        View silence = findViewById(R.id.silence);
+
+        switch (behavior) {
+            case BEHAVIOR_ALERTING:
+                alert.setSelected(true);
+                silence.setSelected(false);
+                mPriorityDescriptionView.setVisibility(VISIBLE);
+                mSilentDescriptionView.setVisibility(GONE);
+                break;
+            case BEHAVIOR_SILENT:
+                alert.setSelected(false);
+                silence.setSelected(true);
+                mSilentDescriptionView.setVisibility(VISIBLE);
+                mPriorityDescriptionView.setVisibility(GONE);
+                break;
+            default:
+                throw new IllegalArgumentException("Unrecognized alerting behavior: " + behavior);
         }
+
+        boolean isAChange = mWasShownHighPriority != (behavior == BEHAVIOR_ALERTING);
+        TextView done = findViewById(R.id.done);
+        done.setText(isAChange ? R.string.inline_ok_button : R.string.inline_done_button);
     }
 
     private void saveImportanceAndExitReason(@NotificationInfoAction int action) {
@@ -751,7 +727,7 @@
      * {@link #swapContent(boolean, boolean)} for where undo is handled.
      */
     @VisibleForTesting
-    void closeControls(View v) {
+    void closeControls(View v, boolean save) {
         int[] parentLoc = new int[2];
         int[] targetLoc = new int[2];
         mGutsContainer.getLocationOnScreen(parentLoc);
@@ -760,7 +736,7 @@
         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, true /* save */, false /* force */);
+        mGutsContainer.closeControls(x, y, save, false /* force */);
     }
 
     @Override
@@ -883,4 +859,10 @@
                 .setSubtype(mIsForBlockingHelper ? MetricsEvent.BLOCKING_HELPER_DISPLAY
                         : MetricsEvent.BLOCKING_HELPER_UNKNOWN);
     }
+
+    @Retention(SOURCE)
+    @IntDef({BEHAVIOR_ALERTING, BEHAVIOR_SILENT})
+    private @interface AlertingBehavior {}
+    private static final int BEHAVIOR_ALERTING = 0;
+    private static final int BEHAVIOR_SILENT = 1;
 }
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 ecbb216..d911e1a 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
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.notification.row;
 
+import static android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE;
+
 import static com.android.systemui.SwipeHelper.SWIPED_FAR_ENOUGH_SIZE_FRACTION;
 
 import android.animation.Animator;
@@ -29,6 +31,7 @@
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
 import android.os.Looper;
+import android.provider.Settings;
 import android.service.notification.StatusBarNotification;
 import android.util.ArrayMap;
 import android.view.LayoutInflater;
@@ -255,9 +258,13 @@
         mVertSpaceForIcons = res.getDimensionPixelSize(R.dimen.notification_min_height);
         mLeftMenuItems.clear();
         mRightMenuItems.clear();
+
+        boolean showSnooze = Settings.Secure.getInt(mContext.getContentResolver(),
+                SHOW_NOTIFICATION_SNOOZE, 0) == 1;
+
         // Construct the menu items based on the notification
-        if (!isForeground) {
-            // Only show snooze for non-foreground notifications
+        if (!isForeground && showSnooze) {
+            // Only show snooze for non-foreground notifications, and if the setting is on
             mSnoozeItem = createSnoozeItem(mContext);
         }
         mAppOpsItem = createAppOpsItem(mContext);
@@ -268,7 +275,7 @@
         }
 
         if (!mIsUsingBidirectionalSwipe) {
-            if (!isForeground) {
+            if (!isForeground && showSnooze) {
                 mRightMenuItems.add(mSnoozeItem);
             }
             mRightMenuItems.add(mInfoItem);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java
index 91c43a1..4526eaf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java
@@ -25,6 +25,7 @@
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
 import android.media.session.PlaybackState;
+import android.metrics.LogMaker;
 import android.os.Handler;
 import android.text.format.DateUtils;
 import android.util.Log;
@@ -35,6 +36,9 @@
 import android.widget.TextView;
 
 import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.systemui.Dependency;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.TransformableView;
@@ -62,8 +66,11 @@
     private NotificationMediaManager mMediaManager;
     private View mSeekBarView;
     private Context mContext;
+    private MetricsLogger mMetricsLogger;
 
-    private SeekBar.OnSeekBarChangeListener mSeekListener = new SeekBar.OnSeekBarChangeListener() {
+    @VisibleForTesting
+    protected SeekBar.OnSeekBarChangeListener mSeekListener =
+            new SeekBar.OnSeekBarChangeListener() {
         @Override
         public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
         }
@@ -76,6 +83,7 @@
         public void onStopTrackingTouch(SeekBar seekBar) {
             if (mMediaController != null && canSeekMedia()) {
                 mMediaController.getTransportControls().seekTo(mSeekBar.getProgress());
+                mMetricsLogger.write(newLog(MetricsEvent.TYPE_UPDATE));
             }
         }
     };
@@ -93,7 +101,8 @@
                 // Update the UI once, in case playback info changed while we were paused
                 mUpdatePlaybackUi.run();
                 clearTimer();
-            } else if (mSeekBarTimer == null) {
+            } else if (mSeekBarTimer == null && mSeekBarView != null
+                    && mSeekBarView.getVisibility() != View.GONE) {
                 startTimer();
             }
         }
@@ -104,6 +113,7 @@
         super(ctx, view, row);
         mContext = ctx;
         mMediaManager = Dependency.get(NotificationMediaManager.class);
+        mMetricsLogger = Dependency.get(MetricsLogger.class);
     }
 
     private void resolveViews() {
@@ -121,11 +131,13 @@
         }
 
         // Check for existing media controller and clean up / create as necessary
+        boolean controllerUpdated = false;
         if (mMediaController == null || !mMediaController.getSessionToken().equals(token)) {
             if (mMediaController != null) {
                 mMediaController.unregisterCallback(mMediaCallback);
             }
             mMediaController = new MediaController(mContext, token);
+            controllerUpdated = true;
         }
 
         if (mMediaController.getMetadata() != null) {
@@ -134,14 +146,21 @@
             if (duration <= 0) {
                 // Don't include the seekbar if this is a livestream
                 Log.d(TAG, "removing seekbar");
-                if (mSeekBarView != null) {
+                if (mSeekBarView != null && mSeekBarView.getVisibility() != View.GONE) {
                     mSeekBarView.setVisibility(View.GONE);
+                    mMetricsLogger.write(newLog(MetricsEvent.TYPE_CLOSE));
+                    clearTimer();
+                } else if (mSeekBarView == null && controllerUpdated) {
+                    // Only log if the controller changed, otherwise we would log multiple times for
+                    // the same notification when user pauses/resumes
+                    mMetricsLogger.write(newLog(MetricsEvent.TYPE_CLOSE));
                 }
                 return;
             } else {
                 // Otherwise, make sure the seekbar is visible
-                if (mSeekBarView != null) {
+                if (mSeekBarView != null && mSeekBarView.getVisibility() == View.GONE) {
                     mSeekBarView.setVisibility(View.VISIBLE);
+                    mMetricsLogger.write(newLog(MetricsEvent.TYPE_OPEN));
                 }
             }
         }
@@ -153,6 +172,7 @@
             stub.setLayoutInflater(layoutInflater);
             stub.setLayoutResource(R.layout.notification_material_media_seekbar);
             mSeekBarView = stub.inflate();
+            mMetricsLogger.write(newLog(MetricsEvent.TYPE_OPEN));
 
             mSeekBar = mSeekBarView.findViewById(R.id.notification_media_progress_bar);
             mSeekBar.setOnSeekBarChangeListener(mSeekListener);
@@ -161,17 +181,14 @@
             mSeekBarTotalTime = mSeekBarView.findViewById(R.id.notification_media_total_time);
 
             if (mSeekBarTimer == null) {
-                // Disable seeking if it is not supported for this media session
-                if (!canSeekMedia()) {
-                    mSeekBar.getThumb().setAlpha(0);
-                    mSeekBar.setEnabled(false);
+                if (canSeekMedia()) {
+                    // Log initial state, since it will not be updated
+                    mMetricsLogger.write(newLog(MetricsEvent.TYPE_DETAIL, 1));
                 } else {
-                    mSeekBar.getThumb().setAlpha(255);
-                    mSeekBar.setEnabled(true);
+                    setScrubberVisible(false);
                 }
 
                 startTimer();
-
                 mMediaController.registerCallback(mMediaCallback);
             }
         }
@@ -209,6 +226,16 @@
         return ((actions & PlaybackState.ACTION_SEEK_TO) != 0);
     }
 
+    private void setScrubberVisible(boolean isVisible) {
+        if (mSeekBar == null || mSeekBar.isEnabled() == isVisible) {
+            return;
+        }
+
+        mSeekBar.getThumb().setAlpha(isVisible ? 255 : 0);
+        mSeekBar.setEnabled(isVisible);
+        mMetricsLogger.write(newLog(MetricsEvent.TYPE_DETAIL, isVisible ? 1 : 0));
+    }
+
     protected final Runnable mUpdatePlaybackUi = new Runnable() {
         @Override
         public void run() {
@@ -228,6 +255,9 @@
                     mSeekBar.setProgress((int) position);
 
                     mSeekBarElapsedTime.setText(millisecondsToTimeString(position));
+
+                    // Update scrubber in case available actions have changed
+                    setScrubberVisible(canSeekMedia());
                 } else {
                     Log.d(TAG, "Controller missing data " + metadata + " " + playbackState);
                     clearTimer();
@@ -265,6 +295,7 @@
         int tintColor = getNotificationHeader().getOriginalIconColor();
         mSeekBarElapsedTime.setTextColor(tintColor);
         mSeekBarTotalTime.setTextColor(tintColor);
+        mSeekBarTotalTime.setShadowLayer(1.5f, 1.5f, 1.5f, mBackgroundColor);
 
         ColorStateList tintList = ColorStateList.valueOf(tintColor);
         mSeekBar.setThumbTintList(tintList);
@@ -293,4 +324,28 @@
     public boolean shouldClipToRounding(boolean topRounded, boolean bottomRounded) {
         return true;
     }
+
+    /**
+     * Returns an initialized LogMaker for logging changes to the seekbar
+     * @return new LogMaker
+     */
+    private LogMaker newLog(int event) {
+        String packageName = mRow.getEntry().notification.getPackageName();
+
+        return new LogMaker(MetricsEvent.MEDIA_NOTIFICATION_SEEKBAR)
+                .setType(event)
+                .setPackageName(packageName);
+    }
+
+    /**
+     * Returns an initialized LogMaker for logging changes with subtypes
+     * @return new LogMaker
+     */
+    private LogMaker newLog(int event, int subtype) {
+        String packageName = mRow.getEntry().notification.getPackageName();
+        return new LogMaker(MetricsEvent.MEDIA_NOTIFICATION_SEEKBAR)
+                .setType(event)
+                .setSubtype(subtype)
+                .setPackageName(packageName);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index e5fbf63..78dc9a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -212,7 +212,9 @@
     }
 
     public boolean isDimmed() {
-        return mDimmed;
+        // While we are expanding from pulse, we want the notifications not to be dimmed, otherwise
+        // you'd see the difference to the pulsing notification
+        return mDimmed && !(isPulseExpanding() && mDozeAmount == 1.0f);
     }
 
     public boolean isDark() {
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 dccf404..e6f4731 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
@@ -939,18 +939,11 @@
             return;
         }
 
-        float alpha =
-                BACKGROUND_ALPHA_DIMMED + (1 - BACKGROUND_ALPHA_DIMMED) * (1.0f - mDimAmount);
-        alpha *= 1f - mInterpolatedDarkAmount;
-        // We need to manually blend in the background color.
-        int scrimColor = mScrimController.getBackgroundColor();
-        int awakeColor = ColorUtils.blendARGB(scrimColor, mBgColor, alpha);
-
         // Interpolate between semi-transparent notification panel background color
         // and white AOD separator.
         float colorInterpolation = MathUtils.smoothStep(0.4f /* start */, 1f /* end */,
                 mLinearDarkAmount);
-        int color = ColorUtils.blendARGB(awakeColor, Color.WHITE, colorInterpolation);
+        int color = ColorUtils.blendARGB(mBgColor, Color.WHITE, colorInterpolation);
 
         if (mCachedBackgroundColor != color) {
             mCachedBackgroundColor = color;
@@ -5709,6 +5702,7 @@
                 view.setTranslationY(wakeUplocation);
             }
         }
+        mDimmedNeedsAnimation = true;
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java
index f221865..05a86fa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java
@@ -33,13 +33,14 @@
 import android.os.SystemClock;
 import android.util.Log;
 import android.util.MathUtils;
-import android.view.Choreographer;
 import android.view.Gravity;
 import android.view.IPinnedStackController;
 import android.view.IPinnedStackListener;
 import android.view.ISystemGestureExclusionListener;
+import android.view.InputChannel;
 import android.view.InputDevice;
 import android.view.InputEvent;
+import android.view.InputEventReceiver;
 import android.view.InputMonitor;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
@@ -53,7 +54,6 @@
 import com.android.systemui.R;
 import com.android.systemui.bubbles.BubbleController;
 import com.android.systemui.recents.OverviewProxyService;
-import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.WindowManagerWrapper;
 
@@ -165,12 +165,14 @@
         mEdgeWidth = res.getDimensionPixelSize(
                 com.android.internal.R.dimen.config_backGestureInset);
 
-        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+        // Reduce the default touch slop to ensure that we can intercept the gesture
+        // before the app starts to react to it.
+        // TODO(b/130352502) Tune this value and extract into a constant
+        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 0.75f;
         mLongPressTimeout = ViewConfiguration.getLongPressTimeout();
 
         mNavBarHeight = res.getDimensionPixelSize(R.dimen.navigation_bar_frame_height);
-        mMinArrowPosition = res.getDimensionPixelSize(
-                R.dimen.navigation_edge_arrow_min_y);
+        mMinArrowPosition = res.getDimensionPixelSize(R.dimen.navigation_edge_arrow_min_y);
         mFingerOffset = res.getDimensionPixelSize(R.dimen.navigation_edge_finger_offset);
     }
 
@@ -250,9 +252,8 @@
             // Register input event receiver
             mInputMonitor = InputManager.getInstance().monitorGestureInput(
                     "edge-swipe", mDisplayId);
-            mInputEventReceiver = new InputEventReceiver(mInputMonitor.getInputChannel(),
-                    Looper.getMainLooper(), Choreographer.getMainThreadInstance(),
-                    this::onInputEvent);
+            mInputEventReceiver = new SysUiInputEventReceiver(
+                    mInputMonitor.getInputChannel(), Looper.getMainLooper());
 
             // Add a nav bar panel window
             mEdgePanel = new NavigationBarEdgePanel(mContext);
@@ -440,4 +441,15 @@
         }
         InputManager.getInstance().injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
     }
+
+    class SysUiInputEventReceiver extends InputEventReceiver {
+        SysUiInputEventReceiver(InputChannel channel, Looper looper) {
+            super(channel, looper);
+        }
+
+        public void onInputEvent(InputEvent event) {
+            EdgeBackGestureHandler.this.onInputEvent(event);
+            finishInputEvent(event, true);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/FloatingRotationButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/FloatingRotationButton.java
new file mode 100644
index 0000000..a4965ba
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/FloatingRotationButton.java
@@ -0,0 +1,171 @@
+/*
+ * 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.phone;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PixelFormat;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.Surface;
+import android.view.View;
+import android.view.WindowManager;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.KeyButtonDrawable;
+import com.android.systemui.statusbar.policy.KeyButtonView;
+
+/** Containing logic for the rotation button on the physical left bottom corner of the screen. */
+public class FloatingRotationButton implements RotationButton {
+
+    private final Context mContext;
+    private final WindowManager mWindowManager;
+    private final KeyButtonView mKeyButtonView;
+    private final int mDiameter;
+    private final int mMargin;
+    private KeyButtonDrawable mKeyButtonDrawable;
+    private boolean mIsShowing;
+    private boolean mCanShow = true;
+
+    private RotationButtonController mRotationButtonController;
+
+    FloatingRotationButton(Context context) {
+        mContext = context;
+        mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+        mKeyButtonView = (KeyButtonView) LayoutInflater.from(mContext).inflate(
+                R.layout.rotate_suggestion, null);
+        mKeyButtonView.setVisibility(View.VISIBLE);
+
+        Resources res = mContext.getResources();
+        mDiameter = res.getDimensionPixelSize(R.dimen.floating_rotation_button_diameter);
+        mMargin = Math.max(res.getDimensionPixelSize(R.dimen.floating_rotation_button_min_margin),
+                res.getDimensionPixelSize(R.dimen.rounded_corner_content_padding));
+    }
+
+    @Override
+    public void setRotationButtonController(RotationButtonController rotationButtonController) {
+        mRotationButtonController = rotationButtonController;
+    }
+
+    @Override
+    public View getCurrentView() {
+        return mKeyButtonView;
+    }
+
+    @Override
+    public boolean show() {
+        if (!mCanShow || mIsShowing) {
+            return false;
+        }
+        mIsShowing = true;
+        int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
+        final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(mDiameter, mDiameter,
+                mMargin, mMargin, WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, flags,
+                PixelFormat.TRANSLUCENT);
+        lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
+        lp.setTitle("FloatingRotationButton");
+        switch (mWindowManager.getDefaultDisplay().getRotation()) {
+            case Surface.ROTATION_0:
+                lp.gravity = Gravity.BOTTOM | Gravity.LEFT;
+                break;
+            case Surface.ROTATION_90:
+                lp.gravity = Gravity.BOTTOM | Gravity.RIGHT;
+                break;
+            case Surface.ROTATION_180:
+                lp.gravity = Gravity.TOP | Gravity.RIGHT;
+                break;
+            case Surface.ROTATION_270:
+                lp.gravity = Gravity.TOP | Gravity.LEFT;
+                break;
+            default:
+                break;
+        }
+        updateIcon();
+        mWindowManager.addView(mKeyButtonView, lp);
+        if (mKeyButtonDrawable != null && mKeyButtonDrawable.canAnimate()) {
+            mKeyButtonDrawable.resetAnimation();
+            mKeyButtonDrawable.startAnimation();
+        }
+        return true;
+    }
+
+    @Override
+    public boolean hide() {
+        if (!mIsShowing) {
+            return false;
+        }
+        mWindowManager.removeViewImmediate(mKeyButtonView);
+        mRotationButtonController.cleanUp();
+        mIsShowing = false;
+        return true;
+    }
+
+    @Override
+    public boolean isVisible() {
+        return mIsShowing;
+    }
+
+    @Override
+    public void updateIcon() {
+        if (!mIsShowing) {
+            return;
+        }
+        mKeyButtonDrawable = getImageDrawable();
+        mKeyButtonView.setImageDrawable(mKeyButtonDrawable);
+        mKeyButtonDrawable.setCallback(mKeyButtonView);
+        if (mKeyButtonDrawable != null && mKeyButtonDrawable.canAnimate()) {
+            mKeyButtonDrawable.resetAnimation();
+            mKeyButtonDrawable.startAnimation();
+        }
+    }
+
+    @Override
+    public void setOnClickListener(View.OnClickListener onClickListener) {
+        mKeyButtonView.setOnClickListener(view -> {
+            hide();
+            onClickListener.onClick(view);
+        });
+    }
+
+    @Override
+    public void setOnHoverListener(View.OnHoverListener onHoverListener) {
+        mKeyButtonView.setOnHoverListener(onHoverListener);
+    }
+
+    @Override
+    public KeyButtonDrawable getImageDrawable() {
+        Context context = new ContextThemeWrapper(mContext.getApplicationContext(),
+                mRotationButtonController.getStyleRes());
+        return KeyButtonDrawable.create(context, R.drawable.ic_sysbar_rotate_button,
+                false /* shadow */, true /* hasOvalBg */);
+    }
+
+    @Override
+    public void setDarkIntensity(float darkIntensity) {
+        mKeyButtonView.setDarkIntensity(darkIntensity);
+    }
+
+    @Override
+    public void setCanShowRotationButton(boolean canShow) {
+        mCanShow = canShow;
+        if (!mCanShow) {
+            hide();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index b7a154d..a0cda69 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -22,8 +22,6 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
-import android.graphics.Region;
-import android.graphics.Region.Op;
 import android.util.Log;
 import android.util.Pools;
 import android.view.DisplayCutout;
@@ -78,6 +76,7 @@
     private int[] mTmpTwoArray = new int[2];
     private boolean mHeadsUpGoingAway;
     private int mStatusBarState;
+    private Rect mTouchableRegion = new Rect();
 
     private AnimationStateHandler mAnimationStateHandler;
 
@@ -297,10 +296,13 @@
     @Nullable
     public void updateTouchableRegion(ViewTreeObserver.InternalInsetsInfo info) {
         info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+        info.touchableRegion.set(calculateTouchableRegion());
+    }
 
+    public Rect calculateTouchableRegion() {
         if (!hasPinnedHeadsUp()) {
-            info.touchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight);
-            updateRegionForNotch(info.touchableRegion);
+            mTouchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight);
+            updateRegionForNotch(mTouchableRegion);
         } else {
             NotificationEntry topEntry = getTopEntry();
             if (topEntry.isChildInGroup()) {
@@ -315,11 +317,12 @@
             int minX = mTmpTwoArray[0];
             int maxX = mTmpTwoArray[0] + topRow.getWidth();
             int height = topRow.getIntrinsicHeight();
-            info.touchableRegion.set(minX, 0, maxX, mHeadsUpInset + height);
+            mTouchableRegion.set(minX, 0, maxX, mHeadsUpInset + height);
         }
+        return mTouchableRegion;
     }
 
-    private void updateRegionForNotch(Region region) {
+    private void updateRegionForNotch(Rect region) {
         DisplayCutout cutout = mStatusBarWindowView.getRootWindowInsets().getDisplayCutout();
         if (cutout == null) {
             return;
@@ -330,7 +333,7 @@
         Rect bounds = new Rect();
         ScreenDecorations.DisplayCutoutView.boundsFromDirection(cutout, Gravity.TOP, bounds);
         bounds.offset(0, mDisplayCutoutTouchableRegionSize);
-        region.op(bounds, Op.UNION);
+        region.union(bounds);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
index bbae62e..0b994ff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
@@ -285,8 +285,6 @@
     }
 
     public void hide(boolean destroyView) {
-        // TODO(b/113914868): investigation log for disappearing home button
-        Log.i(TAG, "KeyguardBouncer.hide (b/113914868): destroyView=" + destroyView);
         if (isShowing()) {
             StatsLog.write(StatsLog.KEYGUARD_BOUNCER_STATE_CHANGED,
                 StatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__HIDDEN);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java
index c9ba76c..d94a335 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java
@@ -86,6 +86,7 @@
 import com.android.internal.util.LatencyTracker;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
+import com.android.systemui.ScreenDecorations;
 import com.android.systemui.SysUiServiceProvider;
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.fragments.FragmentHostManager;
@@ -170,6 +171,7 @@
     public int mDisplayId;
     private boolean mIsOnDefaultDisplay;
     public boolean mHomeBlockedThisTouch;
+    private ScreenDecorations mScreenDecorations;
 
     private Handler mHandler = Dependency.get(Dependency.MAIN_HANDLER);
 
@@ -188,7 +190,7 @@
         @Override
         public void onQuickStepStarted() {
             // Use navbar dragging as a signal to hide the rotate button
-            mNavigationBarView.getRotateSuggestionButton().setRotateSuggestionButtonState(false);
+            mNavigationBarView.getRotationButtonController().setRotateSuggestionButtonState(false);
 
             // Hide the notifications panel when quick step starts
             mStatusBar.collapsePanel(true /* animate */);
@@ -333,27 +335,32 @@
 
         // Currently there is no accelerometer sensor on non-default display.
         if (mIsOnDefaultDisplay) {
-            final RotationContextButton rotationButton =
-                    mNavigationBarView.getRotateSuggestionButton();
-            rotationButton.setListener(mRotationButtonListener);
-            rotationButton.addRotationCallback(mRotationWatcher);
+            mNavigationBarView.getRotateSuggestionButton().setListener(mRotationButtonListener);
+
+            final RotationButtonController rotationButtonController =
+                    mNavigationBarView.getRotationButtonController();
+            rotationButtonController.addRotationCallback(mRotationWatcher);
 
             // Reset user rotation pref to match that of the WindowManager if starting in locked
             // mode. This will automatically happen when switching from auto-rotate to locked mode.
-            if (display != null && rotationButton.isRotationLocked()) {
-                final int winRotation = display.getRotation();
-                rotationButton.setRotationLockedAtAngle(winRotation);
+            if (display != null && rotationButtonController.isRotationLocked()) {
+                rotationButtonController.setRotationLockedAtAngle(display.getRotation());
             }
         } else {
             mDisabledFlags2 |= StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS;
         }
         setDisabled2Flags(mDisabledFlags2);
+
+        mScreenDecorations = SysUiServiceProvider.getComponent(getContext(),
+                ScreenDecorations.class);
+        getBarTransitions().addDarkIntensityListener(mScreenDecorations);
     }
 
     @Override
     public void onDestroyView() {
         super.onDestroyView();
         if (mNavigationBarView != null) {
+            mNavigationBarView.getBarTransitions().removeDarkIntensityListener(mScreenDecorations);
             mNavigationBarView.getBarTransitions().destroy();
             mNavigationBarView.getLightTransitionsController().destroy(getContext());
         }
@@ -458,34 +465,34 @@
             if (DEBUG_WINDOW_STATE) Log.d(TAG, "Navigation bar " + windowStateToString(state));
 
             updateSystemUiStateFlags(-1);
-            mNavigationBarView.getRotateSuggestionButton()
-                    .onNavigationBarWindowVisibilityChange(isNavBarWindowVisible());
+            mNavigationBarView.getRotationButtonController().onNavigationBarWindowVisibilityChange(
+                    isNavBarWindowVisible());
         }
     }
 
     @Override
     public void onRotationProposal(final int rotation, boolean isValid) {
         final int winRotation = mNavigationBarView.getDisplay().getRotation();
-        final boolean rotateSuggestionsDisabled = RotationContextButton
+        final boolean rotateSuggestionsDisabled = RotationButtonController
                 .hasDisable2RotateSuggestionFlag(mDisabledFlags2);
+        final RotationButtonController rotationButtonController =
+                mNavigationBarView.getRotationButtonController();
+        final RotationButton rotationButton = rotationButtonController.getRotationButton();
+
         if (RotationContextButton.DEBUG_ROTATION) {
             Log.v(TAG, "onRotationProposal proposedRotation=" + Surface.rotationToString(rotation)
                     + ", winRotation=" + Surface.rotationToString(winRotation)
                     + ", isValid=" + isValid + ", mNavBarWindowState="
                     + StatusBarManager.windowStateToString(mNavigationBarWindowState)
                     + ", rotateSuggestionsDisabled=" + rotateSuggestionsDisabled
-                    + ", isRotateButtonVisible=" + (mNavigationBarView == null ? "null" :
-                        mNavigationBarView.getRotateSuggestionButton().isVisible()));
+                    + ", isRotateButtonVisible=" + (mNavigationBarView == null ? "null"
+                    : rotationButton.isVisible()));
         }
 
         // Respect the disabled flag, no need for action as flag change callback will handle hiding
         if (rotateSuggestionsDisabled) return;
 
-        View rotationButton = mNavigationBarView.getRotateSuggestionButton().getCurrentView();
-        if (rotationButton != null && rotationButton.isAttachedToWindow()) {
-            mNavigationBarView.getRotateSuggestionButton()
-                    .onRotationProposal(rotation, winRotation, isValid);
-        }
+        rotationButtonController.onRotationProposal(rotation, winRotation, isValid);
     }
 
     /** Restores the System UI flags saved state to {@link NavigationBarFragment}. */
@@ -593,7 +600,7 @@
     private void setDisabled2Flags(int state2) {
         // Method only called on change of disable2 flags
         if (mNavigationBarView != null) {
-            mNavigationBarView.getRotateSuggestionButton().onDisable2FlagChanged(state2);
+            mNavigationBarView.getRotationButtonController().onDisable2FlagChanged(state2);
         }
     }
 
@@ -862,8 +869,8 @@
         boolean[] feedbackEnabled = new boolean[1];
         int a11yFlags = getA11yButtonState(feedbackEnabled);
 
-        mNavigationBarView.getRotateSuggestionButton()
-                .setAccessibilityFeedbackEnabled(feedbackEnabled[0]);
+        mNavigationBarView.getRotationButtonController().setAccessibilityFeedbackEnabled(
+                feedbackEnabled[0]);
 
         boolean clickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0;
         boolean longClickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0;
@@ -1020,7 +1027,7 @@
         getBarTransitions().transitionTo(barMode, animate);
     }
 
-    private BarTransitions getBarTransitions() {
+    public NavigationBarTransitions getBarTransitions() {
         return mNavigationBarView.getBarTransitions();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
index a12ae96..662c744 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
@@ -63,7 +63,6 @@
     public static final String RIGHT = "right";
     public static final String CONTEXTUAL = "contextual";
     public static final String IME_SWITCHER = "ime_switcher";
-    public static final String START_CONTEXTUAL = "start_contextual";
 
     public static final String GRAVITY_SEPARATOR = ";";
     public static final String BUTTON_SEPARATOR = ",";
@@ -395,8 +394,6 @@
             v = inflater.inflate(R.layout.home_handle, parent, false);
         } else if (IME_SWITCHER.equals(button)) {
             v = inflater.inflate(R.layout.ime_switcher, parent, false);
-        } else if (START_CONTEXTUAL.equals(button)) {
-            v = inflater.inflate(R.layout.start_contextual, parent, false);
         } else if (button.startsWith(KEY)) {
             String uri = extractImage(button);
             int code = extractKeycode(button);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java
index 3b3336b..2b5a28e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java
@@ -36,9 +36,23 @@
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
 
+import java.util.ArrayList;
+import java.util.List;
+
 public final class NavigationBarTransitions extends BarTransitions implements
         LightBarTransitionsController.DarkIntensityApplier {
 
+    /**
+     * Notified when the color of nav bar elements changes.
+     */
+    public interface DarkIntensityListener {
+        /**
+         * Called when the color of nav bar elements changes.
+         * @param darkIntensity 0 is the lightest color, 1 is the darkest.
+         */
+        void onDarkIntensity(float darkIntensity);
+    }
+
     private final NavigationBarView mView;
     private final IStatusBarService mBarService;
     private final LightBarTransitionsController mLightTransitionsController;
@@ -49,6 +63,7 @@
     private boolean mAutoDim;
     private View mNavButtons;
     private int mNavBarMode = NAV_BAR_MODE_3BUTTON;
+    private List<DarkIntensityListener> mDarkIntensityListeners;
 
     private final Handler mHandler = Handler.getMain();
     private final IWallpaperVisibilityListener mWallpaperVisibilityListener =
@@ -69,6 +84,7 @@
         mLightTransitionsController = new LightBarTransitionsController(view.getContext(), this);
         mAllowAutoDimWallpaperNotVisible = view.getContext().getResources()
                 .getBoolean(R.bool.config_navigation_bar_enable_auto_dim_no_visible_wallpaper);
+        mDarkIntensityListeners = new ArrayList();
 
         IWindowManager windowManagerService = Dependency.get(IWindowManager.class);
         try {
@@ -168,11 +184,16 @@
         applyDarkIntensity(mLightTransitionsController.getCurrentDarkIntensity());
     }
 
+    @Override
     public void applyDarkIntensity(float darkIntensity) {
         SparseArray<ButtonDispatcher> buttonDispatchers = mView.getButtonDispatchers();
         for (int i = buttonDispatchers.size() - 1; i >= 0; i--) {
             buttonDispatchers.valueAt(i).setDarkIntensity(darkIntensity);
         }
+        mView.getRotationButtonController().setDarkIntensity(darkIntensity);
+        for (DarkIntensityListener listener : mDarkIntensityListeners) {
+            listener.onDarkIntensity(darkIntensity);
+        }
         if (mAutoDim) {
             applyLightsOut(false, true);
         }
@@ -189,4 +210,18 @@
     public void onNavigationModeChanged(int mode) {
         mNavBarMode = mode;
     }
+
+    /**
+     * Register {@code listener} to be notified when the color of nav bar elements changes.
+     */
+    public void addDarkIntensityListener(DarkIntensityListener listener) {
+        mDarkIntensityListeners.add(listener);
+    }
+
+    /**
+     * Remove {@code listener} from being notified when the color of nav bar elements changes.
+     */
+    public void removeDarkIntensityListener(DarkIntensityListener listener) {
+        mDarkIntensityListeners.remove(listener);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
index 5f61975..3ec1609 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -22,6 +22,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
+import static com.android.systemui.shared.system.QuickStepContract.isGesturalMode;
 import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE;
 
 import android.animation.LayoutTransition;
@@ -130,7 +131,6 @@
     private boolean mImeVisible;
 
     private final SparseArray<ButtonDispatcher> mButtonDispatchers = new SparseArray<>();
-    private final ContextualButtonGroup mStartContextualButtonGroup;
     private final ContextualButtonGroup mContextualButtonGroup;
     private Configuration mConfiguration;
     private Configuration mTmpLastConfiguration;
@@ -138,6 +138,8 @@
     private NavigationBarInflaterView mNavigationInflaterView;
     private RecentsOnboarding mRecentsOnboarding;
     private NotificationPanelView mPanelView;
+    private FloatingRotationButton mFloatingRotationButton;
+    private RotationButtonController mRotationButtonController;
 
     private NavBarTintController mTintController;
 
@@ -232,24 +234,12 @@
     private final OnComputeInternalInsetsListener mOnComputeInternalInsetsListener = info -> {
         // When the nav bar is in 2-button or 3-button mode, or when IME is visible in fully
         // gestural mode, the entire nav bar should be touchable.
-        if (!QuickStepContract.isGesturalMode(mNavBarMode) || mImeVisible) {
+        if (!isGesturalMode(mNavBarMode) || mImeVisible) {
             info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
             return;
         }
         info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
-        RotationContextButton rotationContextButton = getRotateSuggestionButton();
-        // If the rotate suggestion button is not visible in fully gestural mode, the entire nav bar
-        // is not touchable so that the app underneath can be clicked through.
-        if (rotationContextButton.getVisibility() != VISIBLE) {
-            info.touchableRegion.setEmpty();
-        } else {
-            // Set the rotate suggestion button area to be touchable.
-            rotationContextButton.getCurrentView().getLocationInWindow(mTmpPosition);
-            Rect rect = new Rect(mTmpPosition[0], mTmpPosition[1],
-                    mTmpPosition[0] + mRotationButtonBounds.width(),
-                    mTmpPosition[1] + mRotationButtonBounds.height());
-            info.touchableRegion.union(rect);
-        }
+        info.touchableRegion.setEmpty();
     };
 
     public NavigationBarView(Context context, AttributeSet attrs) {
@@ -258,15 +248,14 @@
         mIsVertical = false;
         mLongClickableAccessibilityButton = false;
         mNavBarMode = Dependency.get(NavigationModeController.class).addListener(this);
-        boolean isGesturalMode = QuickStepContract.isGesturalMode(mNavBarMode);
+        boolean isGesturalMode = isGesturalMode(mNavBarMode);
 
         // Set up the context group of buttons
         mContextualButtonGroup = new ContextualButtonGroup(R.id.menu_container);
         final ContextualButton imeSwitcherButton = new ContextualButton(R.id.ime_switcher,
                 R.drawable.ic_ime_switcher_default);
         final RotationContextButton rotateSuggestionButton = new RotationContextButton(
-                R.id.rotate_suggestion, R.drawable.ic_sysbar_rotate_button, getContext(),
-                R.style.RotateButtonCCWStart90);
+                R.id.rotate_suggestion, R.drawable.ic_sysbar_rotate_button);
         final ContextualButton accessibilityButton =
                 new ContextualButton(R.id.accessibility_button,
                         R.drawable.ic_sysbar_accessibility_button);
@@ -278,13 +267,12 @@
 
         mOverviewProxyService = Dependency.get(OverviewProxyService.class);
         mRecentsOnboarding = new RecentsOnboarding(context, mOverviewProxyService);
+        mFloatingRotationButton = new FloatingRotationButton(context);
+        mRotationButtonController = new RotationButtonController(context,
+                R.style.RotateButtonCCWStart90,
+                isGesturalMode ? mFloatingRotationButton : rotateSuggestionButton);
 
         final ContextualButton backButton = new ContextualButton(R.id.back, 0);
-        mStartContextualButtonGroup = new ContextualButtonGroup(R.id.start_menu_container);
-        if (isGesturalMode) {
-            mStartContextualButtonGroup.addButton(rotateSuggestionButton);
-        }
-        mStartContextualButtonGroup.addButton(backButton);
 
         mConfiguration = new Configuration();
         mTmpLastConfiguration = new Configuration();
@@ -301,7 +289,6 @@
         mButtonDispatchers.put(R.id.accessibility_button, accessibilityButton);
         mButtonDispatchers.put(R.id.rotate_suggestion, rotateSuggestionButton);
         mButtonDispatchers.put(R.id.menu_container, mContextualButtonGroup);
-        mButtonDispatchers.put(R.id.start_menu_container, mStartContextualButtonGroup);
         mDeadZone = new DeadZone(this);
 
         mEdgeBackGestureHandler = new EdgeBackGestureHandler(context, mOverviewProxyService);
@@ -312,7 +299,7 @@
         return mTintController;
     }
 
-    public BarTransitions getBarTransitions() {
+    public NavigationBarTransitions getBarTransitions() {
         return mBarTransitions;
     }
 
@@ -390,6 +377,14 @@
         return mCurrentView;
     }
 
+    public RotationButtonController getRotationButtonController() {
+        return mRotationButtonController;
+    }
+
+    public FloatingRotationButton getFloatingRotationButton() {
+        return mFloatingRotationButton;
+    }
+
     public ButtonDispatcher getRecentsButton() {
         return mButtonDispatchers.get(R.id.recent_apps);
     }
@@ -414,10 +409,6 @@
         return (RotationContextButton) mButtonDispatchers.get(R.id.rotate_suggestion);
     }
 
-    public ContextualButtonGroup getStartContextualButtonGroup() {
-        return mStartContextualButtonGroup;
-    }
-
     public ButtonDispatcher getHomeHandle() {
         return mButtonDispatchers.get(R.id.home_handle);
     }
@@ -454,7 +445,6 @@
         if (densityChange || dirChange) {
             mRecentIcon = getDrawable(R.drawable.ic_sysbar_recent);
             mContextualButtonGroup.updateIcons();
-            mStartContextualButtonGroup.updateIcons();
         }
         if (orientationChange || densityChange || dirChange) {
             mBackIcon = getBackDrawable();
@@ -490,7 +480,7 @@
             return;
         }
 
-        if (QuickStepContract.isGesturalMode(mNavBarMode)) {
+        if (isGesturalMode(mNavBarMode)) {
             drawable.setRotation(degrees);
             return;
         }
@@ -561,6 +551,7 @@
             mTransitionListener.onBackAltCleared();
         }
         mImeVisible = visible;
+        mRotationButtonController.getRotationButton().setCanShowRotationButton(!mImeVisible);
     }
 
     public void setDisabledFlags(int disabledFlags) {
@@ -603,17 +594,13 @@
 
         mBarTransitions.reapplyDarkIntensity();
 
-        boolean disableHome = QuickStepContract.isGesturalMode(mNavBarMode)
+        boolean disableHome = isGesturalMode(mNavBarMode)
                 || ((mDisabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0);
 
-        // TODO(b/113914868): investigation log for disappearing home button
-        Log.i(TAG, "updateNavButtonIcons (b/113914868): home disabled=" + disableHome
-                + " mDisabledFlags=" + mDisabledFlags);
-
         // Always disable recents when alternate car mode UI is active and for secondary displays.
         boolean disableRecent = isRecentsButtonDisabled();
 
-        boolean disableBack = !useAltBack && (QuickStepContract.isGesturalMode(mNavBarMode)
+        boolean disableBack = !useAltBack && (isGesturalMode(mNavBarMode)
                 || ((mDisabledFlags & View.STATUS_BAR_DISABLE_BACK) != 0));
 
         // When screen pinning, don't hide back and home when connected service or back and
@@ -641,7 +628,6 @@
         }
 
         getBackButton().setVisibility(disableBack      ? View.INVISIBLE : View.VISIBLE);
-        mStartContextualButtonGroup.setButtonVisibility(R.id.back, !disableBack);
         getHomeButton().setVisibility(disableHome      ? View.INVISIBLE : View.VISIBLE);
         getRecentsButton().setVisibility(disableRecent ? View.INVISIBLE : View.VISIBLE);
     }
@@ -782,7 +768,7 @@
 
         // Color adaption is tied with showing home handle, only avaliable if visible
         mTintController.onNavigationModeChanged(mNavBarMode);
-        if (QuickStepContract.isGesturalMode(mNavBarMode)) {
+        if (isGesturalMode(mNavBarMode)) {
             mTintController.start();
         } else {
             mTintController.stop();
@@ -939,7 +925,7 @@
                 "onMeasure: (%dx%d) old: (%dx%d)", w, h, getMeasuredWidth(), getMeasuredHeight()));
 
         final boolean newVertical = w > 0 && h > w
-                && !QuickStepContract.isGesturalMode(mNavBarMode);
+                && !isGesturalMode(mNavBarMode);
         if (newVertical != mIsVertical) {
             mIsVertical = newVertical;
             if (DEBUG) {
@@ -950,7 +936,7 @@
             notifyVerticalChangedListener(newVertical);
         }
 
-        if (QuickStepContract.isGesturalMode(mNavBarMode)) {
+        if (isGesturalMode(mNavBarMode)) {
             // Update the nav bar background to match the height of the visible nav bar
             int height = mIsVertical
                     ? getResources().getDimensionPixelSize(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
index bb490f5..e8ca3ee 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
@@ -1,7 +1,5 @@
 package com.android.systemui.statusbar.phone;
 
-import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
-
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Color;
@@ -17,7 +15,6 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.internal.util.ContrastColorUtil;
-import com.android.internal.widget.ViewClippingUtil;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.plugins.DarkIconDispatcher;
@@ -86,17 +83,6 @@
      * Ratio representing being awake or in ambient mode, where 1 is dark and 0 awake.
      */
     private float mDarkAmount;
-    /**
-     * Maximum translation to avoid burn in.
-     */
-    private int mBurnInOffset;
-    /**
-     * Height of the keyguard status bar (not the one after unlocking.)
-     */
-    private int mKeyguardStatusBarHeight;
-
-    private final ViewClippingUtil.ClippingParameters mClippingParameters =
-            view -> view instanceof StatusBarWindowView;
 
     public NotificationIconAreaController(Context context, StatusBar statusBar,
             StatusBarStateController statusBarStateController,
@@ -166,9 +152,6 @@
         Resources res = context.getResources();
         mIconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size);
         mIconHPadding = res.getDimensionPixelSize(R.dimen.status_bar_icon_padding);
-        mBurnInOffset = res.getDimensionPixelSize(R.dimen.default_burn_in_prevention_offset);
-        mKeyguardStatusBarHeight = res
-                .getDimensionPixelSize(R.dimen.status_bar_header_height_keyguard);
     }
 
     /**
@@ -479,49 +462,9 @@
         mNotificationIcons.setIsolatedIconLocation(iconDrawingRect, requireStateUpdate);
     }
 
-    /**
-     * Moves icons whenever the device wakes up in AOD, to avoid burn in.
-     */
-    public void dozeTimeTick() {
-        if (mNotificationIcons.getVisibility() != View.VISIBLE) {
-            return;
-        }
-
-        if (mDarkAmount == 0 && !mStatusBarStateController.isDozing()) {
-            mNotificationIcons.setTranslationX(0);
-            mNotificationIcons.setTranslationY(0);
-            mCenteredIcon.setTranslationX(0);
-            mCenteredIcon.setTranslationY(0);
-            return;
-        }
-
-        int yOffset = (mKeyguardStatusBarHeight - getHeight()) / 2;
-        int translationX = getBurnInOffset(mBurnInOffset, true /* xAxis */);
-        int translationY = getBurnInOffset(mBurnInOffset, false /* xAxis */) + yOffset;
-        mNotificationIcons.setTranslationX(translationX);
-        mNotificationIcons.setTranslationY(translationY);
-        mCenteredIcon.setTranslationX(translationX);
-        mCenteredIcon.setTranslationY(translationY);
-    }
-
-    @Override
-    public void onDozingChanged(boolean isDozing) {
-        dozeTimeTick();
-    }
-
     @Override
     public void onDozeAmountChanged(float linear, float eased) {
-        boolean wasOrIsAwake = mDarkAmount == 0 || linear == 0;
-        boolean wasOrIsDozing = mDarkAmount == 1 || linear == 1;
         mDarkAmount = linear;
-        if (wasOrIsAwake) {
-            ViewClippingUtil.setClippingDeactivated(mNotificationIcons, mDarkAmount != 0,
-                    mClippingParameters);
-        }
-        if (wasOrIsAwake || wasOrIsDozing) {
-            dozeTimeTick();
-        }
-
         boolean fullyDark = mDarkAmount == 1f;
         if (mFullyDark != fullyDark) {
             mFullyDark = fullyDark;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
index 7623dee..92aa884 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -100,6 +100,7 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.function.Consumer;
 
@@ -141,6 +142,7 @@
     private static final String COUNTER_PANEL_OPEN_PEEK = "panel_open_peek";
 
     private static final Rect mDummyDirtyRect = new Rect(0, 0, 1, 1);
+    private static final Rect mEmptyRect = new Rect();
 
     private static final AnimationProperties CLOCK_ANIMATION_PROPERTIES = new AnimationProperties()
             .setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
@@ -596,6 +598,25 @@
             mQs.setHeightOverride(mQs.getDesiredHeight());
         }
         updateMaxHeadsUpTranslation();
+        updateGestureExclusionRect();
+    }
+
+    private void updateGestureExclusionRect() {
+        Rect exclusionRect = calculateGestureExclusionRect();
+        setSystemGestureExclusionRects(exclusionRect.isEmpty()
+                ? Collections.EMPTY_LIST
+                : Collections.singletonList(exclusionRect));
+    }
+
+    private Rect calculateGestureExclusionRect() {
+        Rect exclusionRect = null;
+        if (isFullyCollapsed()) {
+            // Note: The heads up manager also calculates the non-pinned touchable region
+            exclusionRect = mHeadsUpManager.calculateTouchableRegion();
+        }
+        return exclusionRect != null
+                ? exclusionRect
+                : mEmptyRect;
     }
 
     private void setIsFullWidth(boolean isFullWidth) {
@@ -1798,6 +1819,7 @@
         updateHeader();
         updateNotificationTranslucency();
         updatePanelExpanded();
+        updateGestureExclusionRect();
         if (DEBUG) {
             invalidate();
         }
@@ -2568,6 +2590,7 @@
             mNotificationStackScroller.runAfterAnimationFinished(
                     mHeadsUpExistenceChangedRunnable);
         }
+        updateGestureExclusionRect();
     }
 
     public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) {
@@ -2992,6 +3015,7 @@
     @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         super.dump(fd, pw, args);
+        pw.println("    gestureExclusionRect: " + calculateGestureExclusionRect());
         if (mKeyguardStatusBar != null) {
             mKeyguardStatusBar.dump(fd, pw, args);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationButton.java
new file mode 100644
index 0000000..2580c0e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationButton.java
@@ -0,0 +1,39 @@
+/*
+ * 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.phone;
+
+import android.view.View;
+
+import com.android.systemui.statusbar.policy.KeyButtonDrawable;
+
+/** Interface of a rotation button that interacts {@link RotationButtonController}. */
+interface RotationButton {
+    void setRotationButtonController(RotationButtonController rotationButtonController);
+    View getCurrentView();
+    boolean show();
+    boolean hide();
+    boolean isVisible();
+    void updateIcon();
+    void setOnClickListener(View.OnClickListener onClickListener);
+    void setOnHoverListener(View.OnHoverListener onHoverListener);
+    KeyButtonDrawable getImageDrawable();
+    void setDarkIntensity(float darkIntensity);
+    default void setCanShowRotationButton(boolean canShow) {}
+    default boolean acceptRotationProposal() {
+        return getCurrentView() != null;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationButtonController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationButtonController.java
new file mode 100644
index 0000000..1e5406f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationButtonController.java
@@ -0,0 +1,478 @@
+/*
+ * 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.phone;
+
+import static com.android.internal.view.RotationPolicy.NATURAL_ROTATION;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.annotation.StyleRes;
+import android.app.StatusBarManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.view.IRotationWatcher.Stub;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.View;
+import android.view.WindowManagerGlobal;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.Dependency;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.statusbar.policy.KeyButtonDrawable;
+import com.android.systemui.statusbar.policy.RotationLockController;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+
+/** Contains logic that deals with showing a rotate suggestion button with animation. */
+public class RotationButtonController {
+
+    private static final int BUTTON_FADE_IN_OUT_DURATION_MS = 100;
+    private static final int NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS = 20000;
+
+    private static final int NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION = 3;
+
+    private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
+    private final ViewRippler mViewRippler = new ViewRippler();
+
+    private @StyleRes int mStyleRes;
+    private int mLastRotationSuggestion;
+    private boolean mPendingRotationSuggestion;
+    private boolean mHoveringRotationSuggestion;
+    private RotationLockController mRotationLockController;
+    private TaskStackListenerImpl mTaskStackListener;
+    private Consumer<Integer> mRotWatcherListener;
+    private boolean mIsNavigationBarShowing;
+
+    private final Runnable mRemoveRotationProposal =
+            () -> setRotateSuggestionButtonState(false /* visible */);
+    private final Runnable mCancelPendingRotationProposal =
+            () -> mPendingRotationSuggestion = false;
+    private Animator mRotateHideAnimator;
+    private boolean mAccessibilityFeedbackEnabled;
+
+    private final Context mContext;
+    private final RotationButton mRotationButton;
+
+    private final Stub mRotationWatcher = new Stub() {
+        @Override
+        public void onRotationChanged(final int rotation) throws RemoteException {
+            if (mRotationButton.getCurrentView() == null) {
+                return;
+            }
+
+            // We need this to be scheduled as early as possible to beat the redrawing of
+            // window in response to the orientation change.
+            Handler h = mRotationButton.getCurrentView().getHandler();
+            Message msg = Message.obtain(h, () -> {
+                // If the screen rotation changes while locked, potentially update lock to flow with
+                // new screen rotation and hide any showing suggestions.
+                if (mRotationLockController.isRotationLocked()) {
+                    if (shouldOverrideUserLockPrefs(rotation)) {
+                        setRotationLockedAtAngle(rotation);
+                    }
+                    setRotateSuggestionButtonState(false /* visible */, true /* forced */);
+                }
+
+                if (mRotWatcherListener != null) {
+                    mRotWatcherListener.accept(rotation);
+                }
+            });
+            msg.setAsynchronous(true);
+            h.sendMessageAtFrontOfQueue(msg);
+        }
+    };
+
+    /**
+     * Determines if rotation suggestions disabled2 flag exists in flag
+     * @param disable2Flags see if rotation suggestion flag exists in this flag
+     * @return whether flag exists
+     */
+    static boolean hasDisable2RotateSuggestionFlag(int disable2Flags) {
+        return (disable2Flags & StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS) != 0;
+    }
+
+    RotationButtonController(Context context, @StyleRes int style, RotationButton rotationButton) {
+        mContext = context;
+        mRotationButton = rotationButton;
+        mRotationButton.setRotationButtonController(this);
+
+        mStyleRes = style;
+        mIsNavigationBarShowing = true;
+        mRotationLockController = Dependency.get(RotationLockController.class);
+
+        // Register the task stack listener
+        mTaskStackListener = new TaskStackListenerImpl();
+        ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
+        mRotationButton.setOnClickListener(this::onRotateSuggestionClick);
+        mRotationButton.setOnHoverListener(this::onRotateSuggestionHover);
+
+        try {
+            WindowManagerGlobal.getWindowManagerService()
+                    .watchRotation(mRotationWatcher, mContext.getDisplay().getDisplayId());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    void cleanUp() {
+        // Unregister the task stack listener
+        ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener);
+
+        try {
+            WindowManagerGlobal.getWindowManagerService().removeRotationWatcher(mRotationWatcher);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    void addRotationCallback(Consumer<Integer> watcher) {
+        mRotWatcherListener = watcher;
+    }
+
+    void setAccessibilityFeedbackEnabled(boolean flag) {
+        mAccessibilityFeedbackEnabled = flag;
+    }
+
+    void setRotationLockedAtAngle(int rotationSuggestion) {
+        mRotationLockController.setRotationLockedAtAngle(true /* locked */, rotationSuggestion);
+    }
+
+    public boolean isRotationLocked() {
+        return mRotationLockController.isRotationLocked();
+    }
+
+    void setRotateSuggestionButtonState(boolean visible) {
+        setRotateSuggestionButtonState(visible, false /* force */);
+    }
+
+    void setRotateSuggestionButtonState(final boolean visible, final boolean force) {
+        // At any point the the button can become invisible because an a11y service became active.
+        // Similarly, a call to make the button visible may be rejected because an a11y service is
+        // active. Must account for this.
+        // Rerun a show animation to indicate change but don't rerun a hide animation
+        if (!visible && !mRotationButton.isVisible()) return;
+
+        final View view = mRotationButton.getCurrentView();
+        if (view == null) return;
+
+        final KeyButtonDrawable currentDrawable = mRotationButton.getImageDrawable();
+        if (currentDrawable == null) return;
+
+        // Clear any pending suggestion flag as it has either been nullified or is being shown
+        mPendingRotationSuggestion = false;
+        view.removeCallbacks(mCancelPendingRotationProposal);
+
+        // Handle the visibility change and animation
+        if (visible) { // Appear and change (cannot force)
+            // Stop and clear any currently running hide animations
+            if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) {
+                mRotateHideAnimator.cancel();
+            }
+            mRotateHideAnimator = null;
+
+            // Reset the alpha if any has changed due to hide animation
+            view.setAlpha(1f);
+
+            // Run the rotate icon's animation if it has one
+            if (currentDrawable.canAnimate()) {
+                currentDrawable.resetAnimation();
+                currentDrawable.startAnimation();
+            }
+
+            if (!isRotateSuggestionIntroduced()) mViewRippler.start(view);
+
+            // Set visibility unless a11y service is active.
+            mRotationButton.show();
+        } else { // Hide
+            mViewRippler.stop(); // Prevent any pending ripples, force hide or not
+
+            if (force) {
+                // If a hide animator is running stop it and make invisible
+                if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) {
+                    mRotateHideAnimator.pause();
+                }
+                mRotationButton.hide();
+                return;
+            }
+
+            // Don't start any new hide animations if one is running
+            if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return;
+
+            ObjectAnimator fadeOut = ObjectAnimator.ofFloat(view, "alpha", 0f);
+            fadeOut.setDuration(BUTTON_FADE_IN_OUT_DURATION_MS);
+            fadeOut.setInterpolator(Interpolators.LINEAR);
+            fadeOut.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mRotationButton.hide();
+                }
+            });
+
+            mRotateHideAnimator = fadeOut;
+            fadeOut.start();
+        }
+    }
+
+    void setDarkIntensity(float darkIntensity) {
+        mRotationButton.setDarkIntensity(darkIntensity);
+    }
+
+    void onRotationProposal(int rotation, int windowRotation, boolean isValid) {
+        if (!mRotationButton.acceptRotationProposal()) {
+            return;
+        }
+
+        // This method will be called on rotation suggestion changes even if the proposed rotation
+        // is not valid for the top app. Use invalid rotation choices as a signal to remove the
+        // rotate button if shown.
+        if (!isValid) {
+            setRotateSuggestionButtonState(false /* visible */);
+            return;
+        }
+
+        final View currentView = mRotationButton.getCurrentView();
+
+        // If window rotation matches suggested rotation, remove any current suggestions
+        if (rotation == windowRotation) {
+            if (currentView != null) {
+                currentView.removeCallbacks(mRemoveRotationProposal);
+            }
+            setRotateSuggestionButtonState(false /* visible */);
+            return;
+        }
+
+        // Prepare to show the navbar icon by updating the icon style to change anim params
+        mLastRotationSuggestion = rotation; // Remember rotation for click
+        final boolean rotationCCW = isRotationAnimationCCW(windowRotation, rotation);
+        int style;
+        if (windowRotation == Surface.ROTATION_0 || windowRotation == Surface.ROTATION_180) {
+            style = rotationCCW ? R.style.RotateButtonCCWStart90 : R.style.RotateButtonCWStart90;
+        } else { // 90 or 270
+            style = rotationCCW ? R.style.RotateButtonCCWStart0 : R.style.RotateButtonCWStart0;
+        }
+        mStyleRes = style;
+        mRotationButton.updateIcon();
+
+        if (mIsNavigationBarShowing) {
+            // The navbar is visible so show the icon right away
+            showAndLogRotationSuggestion();
+        } else {
+            // If the navbar isn't shown, flag the rotate icon to be shown should the navbar become
+            // visible given some time limit.
+            mPendingRotationSuggestion = true;
+            if (currentView != null) {
+                currentView.removeCallbacks(mCancelPendingRotationProposal);
+                currentView.postDelayed(mCancelPendingRotationProposal,
+                        NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS);
+            }
+        }
+    }
+
+    void onDisable2FlagChanged(int state2) {
+        final boolean rotateSuggestionsDisabled = hasDisable2RotateSuggestionFlag(state2);
+        if (rotateSuggestionsDisabled) onRotationSuggestionsDisabled();
+    }
+
+    void onNavigationBarWindowVisibilityChange(boolean showing) {
+        if (mIsNavigationBarShowing != showing) {
+            mIsNavigationBarShowing = showing;
+
+            // If the navbar is visible, show the rotate button if there's a pending suggestion
+            if (showing && mPendingRotationSuggestion) {
+                showAndLogRotationSuggestion();
+            }
+        }
+    }
+
+    @StyleRes int getStyleRes() {
+        return mStyleRes;
+    }
+
+    RotationButton getRotationButton() {
+        return mRotationButton;
+    }
+
+    private void onRotateSuggestionClick(View v) {
+        mMetricsLogger.action(MetricsEvent.ACTION_ROTATION_SUGGESTION_ACCEPTED);
+        incrementNumAcceptedRotationSuggestionsIfNeeded();
+        setRotationLockedAtAngle(mLastRotationSuggestion);
+    }
+
+    private boolean onRotateSuggestionHover(View v, MotionEvent event) {
+        final int action = event.getActionMasked();
+        mHoveringRotationSuggestion = (action == MotionEvent.ACTION_HOVER_ENTER)
+                || (action == MotionEvent.ACTION_HOVER_MOVE);
+        rescheduleRotationTimeout(true /* reasonHover */);
+        return false; // Must return false so a11y hover events are dispatched correctly.
+    }
+
+    private void onRotationSuggestionsDisabled() {
+        // Immediately hide the rotate button and clear any planned removal
+        setRotateSuggestionButtonState(false /* visible */, true /* force */);
+        if (mRotationButton.getCurrentView() != null) {
+            mRotationButton.getCurrentView().removeCallbacks(mRemoveRotationProposal);
+        }
+    }
+
+    private void showAndLogRotationSuggestion() {
+        setRotateSuggestionButtonState(true /* visible */);
+        rescheduleRotationTimeout(false /* reasonHover */);
+        mMetricsLogger.visible(MetricsEvent.ROTATION_SUGGESTION_SHOWN);
+    }
+
+    private boolean shouldOverrideUserLockPrefs(final int rotation) {
+        // Only override user prefs when returning to the natural rotation (normally portrait).
+        // Don't let apps that force landscape or 180 alter user lock.
+        return rotation == NATURAL_ROTATION;
+    }
+
+    private boolean isRotationAnimationCCW(int from, int to) {
+        // All 180deg WM rotation animations are CCW, match that
+        if (from == Surface.ROTATION_0 && to == Surface.ROTATION_90) return false;
+        if (from == Surface.ROTATION_0 && to == Surface.ROTATION_180) return true; //180d so CCW
+        if (from == Surface.ROTATION_0 && to == Surface.ROTATION_270) return true;
+        if (from == Surface.ROTATION_90 && to == Surface.ROTATION_0) return true;
+        if (from == Surface.ROTATION_90 && to == Surface.ROTATION_180) return false;
+        if (from == Surface.ROTATION_90 && to == Surface.ROTATION_270) return true; //180d so CCW
+        if (from == Surface.ROTATION_180 && to == Surface.ROTATION_0) return true; //180d so CCW
+        if (from == Surface.ROTATION_180 && to == Surface.ROTATION_90) return true;
+        if (from == Surface.ROTATION_180 && to == Surface.ROTATION_270) return false;
+        if (from == Surface.ROTATION_270 && to == Surface.ROTATION_0) return false;
+        if (from == Surface.ROTATION_270 && to == Surface.ROTATION_90) return true; //180d so CCW
+        if (from == Surface.ROTATION_270 && to == Surface.ROTATION_180) return true;
+        return false; // Default
+    }
+
+    private void rescheduleRotationTimeout(final boolean reasonHover) {
+        if (mRotationButton.getCurrentView() == null) {
+            return;
+        }
+
+        // May be called due to a new rotation proposal or a change in hover state
+        if (reasonHover) {
+            // Don't reschedule if a hide animator is running
+            if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return;
+            // Don't reschedule if not visible
+            if (!mRotationButton.isVisible()) return;
+        }
+
+        // Stop any pending removal
+        mRotationButton.getCurrentView().removeCallbacks(mRemoveRotationProposal);
+        // Schedule timeout
+        mRotationButton.getCurrentView().postDelayed(mRemoveRotationProposal,
+                computeRotationProposalTimeout());
+    }
+
+    private int computeRotationProposalTimeout() {
+        if (mAccessibilityFeedbackEnabled) return 10000;
+        if (mHoveringRotationSuggestion) return 8000;
+        return 5000;
+    }
+
+    private boolean isRotateSuggestionIntroduced() {
+        ContentResolver cr = mContext.getContentResolver();
+        return Settings.Secure.getInt(cr, Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 0)
+                >= NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION;
+    }
+
+    private void incrementNumAcceptedRotationSuggestionsIfNeeded() {
+        // Get the number of accepted suggestions
+        ContentResolver cr = mContext.getContentResolver();
+        final int numSuggestions = Settings.Secure.getInt(cr,
+                Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 0);
+
+        // Increment the number of accepted suggestions only if it would change intro mode
+        if (numSuggestions < NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION) {
+            Settings.Secure.putInt(cr, Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED,
+                    numSuggestions + 1);
+        }
+    }
+
+    private class TaskStackListenerImpl extends TaskStackChangeListener {
+        // Invalidate any rotation suggestion on task change or activity orientation change
+        // Note: all callbacks happen on main thread
+
+        @Override
+        public void onTaskStackChanged() {
+            setRotateSuggestionButtonState(false /* visible */);
+        }
+
+        @Override
+        public void onTaskRemoved(int taskId) {
+            setRotateSuggestionButtonState(false /* visible */);
+        }
+
+        @Override
+        public void onTaskMovedToFront(int taskId) {
+            setRotateSuggestionButtonState(false /* visible */);
+        }
+
+        @Override
+        public void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) {
+            // Only hide the icon if the top task changes its requestedOrientation
+            // Launcher can alter its requestedOrientation while it's not on top, don't hide on this
+            Optional.ofNullable(ActivityManagerWrapper.getInstance())
+                    .map(ActivityManagerWrapper::getRunningTask)
+                    .ifPresent(a -> {
+                        if (a.id == taskId) setRotateSuggestionButtonState(false /* visible */);
+                    });
+        }
+    }
+
+    private class ViewRippler {
+        private static final int RIPPLE_OFFSET_MS = 50;
+        private static final int RIPPLE_INTERVAL_MS = 2000;
+        private View mRoot;
+
+        public void start(View root) {
+            stop(); // Stop any pending ripple animations
+
+            mRoot = root;
+
+            // Schedule pending ripples, offset the 1st to avoid problems with visibility change
+            mRoot.postOnAnimationDelayed(mRipple, RIPPLE_OFFSET_MS);
+            mRoot.postOnAnimationDelayed(mRipple, RIPPLE_INTERVAL_MS);
+            mRoot.postOnAnimationDelayed(mRipple, 2 * RIPPLE_INTERVAL_MS);
+            mRoot.postOnAnimationDelayed(mRipple, 3 * RIPPLE_INTERVAL_MS);
+            mRoot.postOnAnimationDelayed(mRipple, 4 * RIPPLE_INTERVAL_MS);
+        }
+
+        public void stop() {
+            if (mRoot != null) mRoot.removeCallbacks(mRipple);
+        }
+
+        private final Runnable mRipple = new Runnable() {
+            @Override
+            public void run() { // Cause the ripple to fire via false presses
+                if (!mRoot.isAttachedToWindow()) return;
+                mRoot.setPressed(true /* pressed */);
+                mRoot.setPressed(false /* pressed */);
+            }
+        };
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationContextButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationContextButton.java
index 7203e57..b117dec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationContextButton.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationContextButton.java
@@ -18,281 +18,30 @@
 
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON;
 
-import static com.android.internal.view.RotationPolicy.NATURAL_ROTATION;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
 import android.annotation.DrawableRes;
 import android.annotation.IdRes;
-import android.annotation.NonNull;
-import android.annotation.StyleRes;
-import android.app.StatusBarManager;
-import android.content.ContentResolver;
 import android.content.Context;
-import android.os.Handler;
-import android.os.Message;
-import android.os.RemoteException;
-import android.provider.Settings;
 import android.view.ContextThemeWrapper;
-import android.view.IRotationWatcher.Stub;
-import android.view.MotionEvent;
-import android.view.Surface;
 import android.view.View;
-import android.view.WindowManagerGlobal;
 
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.systemui.Dependency;
-import com.android.systemui.Interpolators;
-import com.android.systemui.R;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
-import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.statusbar.policy.KeyButtonDrawable;
-import com.android.systemui.statusbar.policy.RotationLockController;
-
-import java.util.Optional;
-import java.util.function.Consumer;
 
 /** Containing logic for the rotation button in nav bar. */
 public class RotationContextButton extends ContextualButton implements
-        NavigationModeController.ModeChangedListener {
+        NavigationModeController.ModeChangedListener, RotationButton {
     public static final boolean DEBUG_ROTATION = false;
 
-    private static final int BUTTON_FADE_IN_OUT_DURATION_MS = 100;
-    private static final int NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS = 20000;
-
-    private static final int NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION = 3;
-
-    private @StyleRes int mStyleRes;
-
-    private int mLastRotationSuggestion;
-    private boolean mPendingRotationSuggestion;
-    private boolean mHoveringRotationSuggestion;
-    private RotationLockController mRotationLockController;
-    private TaskStackListenerImpl mTaskStackListener;
-    private Consumer<Integer> mRotWatcherListener;
-    private boolean mIsNavigationBarShowing;
-
-    private final Runnable mRemoveRotationProposal =
-            () -> setRotateSuggestionButtonState(false /* visible */);
-    private final Runnable mCancelPendingRotationProposal =
-            () -> mPendingRotationSuggestion = false;
-    private Animator mRotateHideAnimator;
-    private boolean mAccessibilityFeedbackEnabled;
     private int mNavBarMode = NAV_BAR_MODE_3BUTTON;
+    private RotationButtonController mRotationButtonController;
 
-    private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
-    private final ViewRippler mViewRippler = new ViewRippler();
-
-    private final Stub mRotationWatcher = new Stub() {
-        @Override
-        public void onRotationChanged(final int rotation) throws RemoteException {
-            if (getCurrentView() == null) {
-                return;
-            }
-
-            // We need this to be scheduled as early as possible to beat the redrawing of
-            // window in response to the orientation change.
-            Handler h = getCurrentView().getHandler();
-            Message msg = Message.obtain(h, () -> {
-                // If the screen rotation changes while locked, potentially update lock to flow with
-                // new screen rotation and hide any showing suggestions.
-                if (mRotationLockController.isRotationLocked()) {
-                    if (shouldOverrideUserLockPrefs(rotation)) {
-                        setRotationLockedAtAngle(rotation);
-                    }
-                    setRotateSuggestionButtonState(false /* visible */, true /* forced */);
-                }
-
-                if (mRotWatcherListener != null) {
-                    mRotWatcherListener.accept(rotation);
-                }
-            });
-            msg.setAsynchronous(true);
-            h.sendMessageAtFrontOfQueue(msg);
-        }
-    };
-
-    /**
-     * Determines if rotation suggestions disabled2 flag exists in flag
-     * @param disable2Flags see if rotation suggestion flag exists in this flag
-     * @return whether flag exists
-     */
-    static boolean hasDisable2RotateSuggestionFlag(int disable2Flags) {
-        return (disable2Flags & StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS) != 0;
-    }
-
-    public RotationContextButton(@IdRes int buttonResId, @DrawableRes int iconResId,
-            @NonNull Context context, @StyleRes int style) {
+    public RotationContextButton(@IdRes int buttonResId, @DrawableRes int iconResId) {
         super(buttonResId, iconResId);
-
-        mStyleRes = style;
-        mIsNavigationBarShowing = true;
-        mRotationLockController = Dependency.get(RotationLockController.class);
-
-        // Register the task stack listener
-        mTaskStackListener = new TaskStackListenerImpl();
-        ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
-        setOnClickListener(this::onRotateSuggestionClick);
-        setOnHoverListener(this::onRotateSuggestionHover);
-
-        try {
-            WindowManagerGlobal.getWindowManagerService()
-                    .watchRotation(mRotationWatcher, context.getDisplay().getDisplayId());
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
     }
 
-    public void addRotationCallback(Consumer<Integer> watcher) {
-        mRotWatcherListener = watcher;
-    }
-
-    public void setRotateSuggestionButtonState(boolean visible) {
-        setRotateSuggestionButtonState(visible, false /* force */);
-    }
-
-    public void setRotateSuggestionButtonState(final boolean visible, final boolean force) {
-        // At any point the the button can become invisible because an a11y service became active.
-        // Similarly, a call to make the button visible may be rejected because an a11y service is
-        // active. Must account for this.
-        // Rerun a show animation to indicate change but don't rerun a hide animation
-        if (!visible && !isVisible()) return;
-
-        final View view = getCurrentView();
-        if (view == null) return;
-
-        final KeyButtonDrawable currentDrawable = getImageDrawable();
-        if (currentDrawable == null) return;
-
-        // Clear any pending suggestion flag as it has either been nullified or is being shown
-        mPendingRotationSuggestion = false;
-        view.removeCallbacks(mCancelPendingRotationProposal);
-
-        // Handle the visibility change and animation
-        if (visible) { // Appear and change (cannot force)
-            // Stop and clear any currently running hide animations
-            if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) {
-                mRotateHideAnimator.cancel();
-            }
-            mRotateHideAnimator = null;
-
-            // Reset the alpha if any has changed due to hide animation
-            view.setAlpha(1f);
-
-            // Run the rotate icon's animation if it has one
-            if (currentDrawable.canAnimate()) {
-                currentDrawable.resetAnimation();
-                currentDrawable.startAnimation();
-            }
-
-            if (!isRotateSuggestionIntroduced()) mViewRippler.start(view);
-
-            // Set visibility unless a11y service is active.
-            show();
-        } else { // Hide
-            mViewRippler.stop(); // Prevent any pending ripples, force hide or not
-
-            if (force) {
-                // If a hide animator is running stop it and make invisible
-                if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) {
-                    mRotateHideAnimator.pause();
-                }
-                hide();
-                return;
-            }
-
-            // Don't start any new hide animations if one is running
-            if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return;
-
-            ObjectAnimator fadeOut = ObjectAnimator.ofFloat(view, "alpha", 0f);
-            fadeOut.setDuration(BUTTON_FADE_IN_OUT_DURATION_MS);
-            fadeOut.setInterpolator(Interpolators.LINEAR);
-            fadeOut.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    hide();
-                }
-            });
-
-            mRotateHideAnimator = fadeOut;
-            fadeOut.start();
-        }
-    }
-
-    public void setAccessibilityFeedbackEnabled(boolean flag) {
-        mAccessibilityFeedbackEnabled = flag;
-    }
-
-    public void setRotationLockedAtAngle(int rotationSuggestion) {
-        mRotationLockController.setRotationLockedAtAngle(true /* locked */, rotationSuggestion);
-    }
-
-    public boolean isRotationLocked() {
-        return mRotationLockController.isRotationLocked();
-    }
-
-    public void onRotationProposal(int rotation, int windowRotation, boolean isValid) {
-        // This method will be called on rotation suggestion changes even if the proposed rotation
-        // is not valid for the top app. Use invalid rotation choices as a signal to remove the
-        // rotate button if shown.
-        if (!isValid) {
-            setRotateSuggestionButtonState(false /* visible */);
-            return;
-        }
-
-        // If window rotation matches suggested rotation, remove any current suggestions
-        if (rotation == windowRotation) {
-            if (getCurrentView() != null) {
-                getCurrentView().removeCallbacks(mRemoveRotationProposal);
-            }
-            setRotateSuggestionButtonState(false /* visible */);
-            return;
-        }
-
-        // Prepare to show the navbar icon by updating the icon style to change anim params
-        mLastRotationSuggestion = rotation; // Remember rotation for click
-        final boolean rotationCCW = isRotationAnimationCCW(windowRotation, rotation);
-        int style;
-        if (windowRotation == Surface.ROTATION_0 || windowRotation == Surface.ROTATION_180) {
-            style = rotationCCW ? R.style.RotateButtonCCWStart90 : R.style.RotateButtonCWStart90;
-        } else { // 90 or 270
-            style = rotationCCW ? R.style.RotateButtonCCWStart0 : R.style.RotateButtonCWStart0;
-        }
-        mStyleRes = style;
-        updateIcon();
-
-        if (mIsNavigationBarShowing) {
-            // The navbar is visible so show the icon right away
-            showAndLogRotationSuggestion();
-        } else {
-            // If the navbar isn't shown, flag the rotate icon to be shown should the navbar become
-            // visible given some time limit.
-            mPendingRotationSuggestion = true;
-            if (getCurrentView() != null) {
-                getCurrentView().removeCallbacks(mCancelPendingRotationProposal);
-                getCurrentView().postDelayed(mCancelPendingRotationProposal,
-                        NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS);
-            }
-        }
-    }
-
-    public void onDisable2FlagChanged(int state2) {
-        final boolean rotateSuggestionsDisabled = hasDisable2RotateSuggestionFlag(state2);
-        if (rotateSuggestionsDisabled) onRotationSuggestionsDisabled();
-    }
-
-    public void onNavigationBarWindowVisibilityChange(boolean showing) {
-        if (mIsNavigationBarShowing != showing) {
-            mIsNavigationBarShowing = showing;
-
-            // If the navbar is visible, show the rotate button if there's a pending suggestion
-            if (showing && mPendingRotationSuggestion) {
-                showAndLogRotationSuggestion();
-            }
-        }
+    @Override
+    public void setRotationButtonController(RotationButtonController rotationButtonController) {
+        mRotationButtonController = rotationButtonController;
     }
 
     @Override
@@ -309,115 +58,16 @@
 
     @Override
     protected KeyButtonDrawable getNewDrawable() {
-        Context context = new ContextThemeWrapper(getContext().getApplicationContext(), mStyleRes);
+        Context context = new ContextThemeWrapper(getContext().getApplicationContext(),
+                mRotationButtonController.getStyleRes());
         return KeyButtonDrawable.create(context, mIconResId, false /* shadow */,
                 QuickStepContract.isGesturalMode(mNavBarMode));
     }
 
     @Override
     public void onDestroy() {
-        // Unregister the task stack listener
-        ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener);
-
-        try {
-            WindowManagerGlobal.getWindowManagerService().removeRotationWatcher(mRotationWatcher);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    private void onRotateSuggestionClick(View v) {
-        mMetricsLogger.action(MetricsEvent.ACTION_ROTATION_SUGGESTION_ACCEPTED);
-        incrementNumAcceptedRotationSuggestionsIfNeeded();
-        setRotationLockedAtAngle(mLastRotationSuggestion);
-    }
-
-    private boolean onRotateSuggestionHover(View v, MotionEvent event) {
-        final int action = event.getActionMasked();
-        mHoveringRotationSuggestion = (action == MotionEvent.ACTION_HOVER_ENTER)
-                || (action == MotionEvent.ACTION_HOVER_MOVE);
-        rescheduleRotationTimeout(true /* reasonHover */);
-        return false; // Must return false so a11y hover events are dispatched correctly.
-    }
-
-    private void onRotationSuggestionsDisabled() {
-        // Immediately hide the rotate button and clear any planned removal
-        setRotateSuggestionButtonState(false /* visible */, true /* force */);
-        if (getCurrentView() != null) {
-            getCurrentView().removeCallbacks(mRemoveRotationProposal);
-        }
-    }
-
-    private void showAndLogRotationSuggestion() {
-        setRotateSuggestionButtonState(true /* visible */);
-        rescheduleRotationTimeout(false /* reasonHover */);
-        mMetricsLogger.visible(MetricsEvent.ROTATION_SUGGESTION_SHOWN);
-    }
-
-    private boolean shouldOverrideUserLockPrefs(final int rotation) {
-        // Only override user prefs when returning to the natural rotation (normally portrait).
-        // Don't let apps that force landscape or 180 alter user lock.
-        return rotation == NATURAL_ROTATION;
-    }
-
-    private boolean isRotationAnimationCCW(int from, int to) {
-        // All 180deg WM rotation animations are CCW, match that
-        if (from == Surface.ROTATION_0 && to == Surface.ROTATION_90) return false;
-        if (from == Surface.ROTATION_0 && to == Surface.ROTATION_180) return true; //180d so CCW
-        if (from == Surface.ROTATION_0 && to == Surface.ROTATION_270) return true;
-        if (from == Surface.ROTATION_90 && to == Surface.ROTATION_0) return true;
-        if (from == Surface.ROTATION_90 && to == Surface.ROTATION_180) return false;
-        if (from == Surface.ROTATION_90 && to == Surface.ROTATION_270) return true; //180d so CCW
-        if (from == Surface.ROTATION_180 && to == Surface.ROTATION_0) return true; //180d so CCW
-        if (from == Surface.ROTATION_180 && to == Surface.ROTATION_90) return true;
-        if (from == Surface.ROTATION_180 && to == Surface.ROTATION_270) return false;
-        if (from == Surface.ROTATION_270 && to == Surface.ROTATION_0) return false;
-        if (from == Surface.ROTATION_270 && to == Surface.ROTATION_90) return true; //180d so CCW
-        if (from == Surface.ROTATION_270 && to == Surface.ROTATION_180) return true;
-        return false; // Default
-    }
-
-    private void rescheduleRotationTimeout(final boolean reasonHover) {
-        if (getCurrentView() == null) {
-            return;
-        }
-
-        // May be called due to a new rotation proposal or a change in hover state
-        if (reasonHover) {
-            // Don't reschedule if a hide animator is running
-            if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return;
-            // Don't reschedule if not visible
-            if (!isVisible()) return;
-        }
-
-        // Stop any pending removal
-        getCurrentView().removeCallbacks(mRemoveRotationProposal);
-        // Schedule timeout
-        getCurrentView().postDelayed(mRemoveRotationProposal, computeRotationProposalTimeout());
-    }
-
-    private int computeRotationProposalTimeout() {
-        if (mAccessibilityFeedbackEnabled) return 10000;
-        if (mHoveringRotationSuggestion) return 8000;
-        return 5000;
-    }
-
-    private boolean isRotateSuggestionIntroduced() {
-        ContentResolver cr = getContext().getContentResolver();
-        return Settings.Secure.getInt(cr, Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 0)
-                >= NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION;
-    }
-
-    private void incrementNumAcceptedRotationSuggestionsIfNeeded() {
-        // Get the number of accepted suggestions
-        ContentResolver cr = getContext().getContentResolver();
-        final int numSuggestions = Settings.Secure.getInt(cr,
-                Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 0);
-
-        // Increment the number of accepted suggestions only if it would change intro mode
-        if (numSuggestions < NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION) {
-            Settings.Secure.putInt(cr, Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED,
-                    numSuggestions + 1);
+        if (mRotationButtonController != null) {
+            mRotationButtonController.cleanUp();
         }
     }
 
@@ -426,66 +76,9 @@
         mNavBarMode = mode;
     }
 
-    private class TaskStackListenerImpl extends TaskStackChangeListener {
-        // Invalidate any rotation suggestion on task change or activity orientation change
-        // Note: all callbacks happen on main thread
-
-        @Override
-        public void onTaskStackChanged() {
-            setRotateSuggestionButtonState(false /* visible */);
-        }
-
-        @Override
-        public void onTaskRemoved(int taskId) {
-            setRotateSuggestionButtonState(false /* visible */);
-        }
-
-        @Override
-        public void onTaskMovedToFront(int taskId) {
-            setRotateSuggestionButtonState(false /* visible */);
-        }
-
-        @Override
-        public void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) {
-            // Only hide the icon if the top task changes its requestedOrientation
-            // Launcher can alter its requestedOrientation while it's not on top, don't hide on this
-            Optional.ofNullable(ActivityManagerWrapper.getInstance())
-                    .map(ActivityManagerWrapper::getRunningTask)
-                    .ifPresent(a -> {
-                        if (a.id == taskId) setRotateSuggestionButtonState(false /* visible */);
-                    });
-        }
-    }
-
-    private class ViewRippler {
-        private static final int RIPPLE_OFFSET_MS = 50;
-        private static final int RIPPLE_INTERVAL_MS = 2000;
-        private View mRoot;
-
-        public void start(View root) {
-            stop(); // Stop any pending ripple animations
-
-            mRoot = root;
-
-            // Schedule pending ripples, offset the 1st to avoid problems with visibility change
-            mRoot.postOnAnimationDelayed(mRipple, RIPPLE_OFFSET_MS);
-            mRoot.postOnAnimationDelayed(mRipple, RIPPLE_INTERVAL_MS);
-            mRoot.postOnAnimationDelayed(mRipple, 2 * RIPPLE_INTERVAL_MS);
-            mRoot.postOnAnimationDelayed(mRipple, 3 * RIPPLE_INTERVAL_MS);
-            mRoot.postOnAnimationDelayed(mRipple, 4 * RIPPLE_INTERVAL_MS);
-        }
-
-        public void stop() {
-            if (mRoot != null) mRoot.removeCallbacks(mRipple);
-        }
-
-        private final Runnable mRipple = new Runnable() {
-            @Override
-            public void run() { // Cause the ripple to fire via false presses
-                if (!mRoot.isAttachedToWindow()) return;
-                mRoot.setPressed(true /* pressed */);
-                mRoot.setPressed(false /* pressed */);
-            }
-        };
+    @Override
+    public boolean acceptRotationProposal() {
+        View currentView = getCurrentView();
+        return currentView != null && currentView.isAttachedToWindow();
     }
 }
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 1da819f..05a23fa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -1311,7 +1311,6 @@
                 && !mDozing
                 && !ONLY_CORE_APPS;
         mNotificationPanel.setQsExpansionEnabled(expandEnabled);
-        // STOPSHIP(kozynski, b/129405675) Remove log
         Log.d(TAG, "updateQsExpansionEnabled - QS Expand enabled: " + expandEnabled);
     }
 
@@ -4013,7 +4012,6 @@
         @Override
         public void dozeTimeTick() {
             mNotificationPanel.dozeTimeTick();
-            mNotificationIconAreaController.dozeTimeTick();
             if (mAmbientIndicationContainer instanceof DozeReceiver) {
                 ((DozeReceiver) mAmbientIndicationContainer).dozeTimeTick();
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java
index c08390f..2afe485 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java
@@ -38,6 +38,7 @@
 import com.android.internal.telephony.cdma.EriInfo;
 import com.android.settingslib.Utils;
 import com.android.settingslib.graph.SignalDrawable;
+import com.android.settingslib.net.SignalStrengthUtil;
 import com.android.systemui.R;
 import com.android.systemui.statusbar.policy.NetworkController.IconState;
 import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
@@ -248,9 +249,8 @@
     }
 
     private void updateInflateSignalStrength() {
-        mInflateSignalStrengths = SubscriptionManager.getResourcesForSubId(mContext,
-               mSubscriptionInfo.getSubscriptionId())
-               .getBoolean(R.bool.config_inflateSignalStrength);
+        mInflateSignalStrengths = SignalStrengthUtil.shouldInflateSignalStrength(mContext,
+                mSubscriptionInfo.getSubscriptionId());
     }
 
     private int getNumLevels() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
index b8a14ef..b2972fc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
@@ -1140,7 +1140,8 @@
                     res.getBoolean(com.android.internal.R.bool.config_alwaysUseCdmaRssi);
             config.hspaDataDistinguishable =
                     res.getBoolean(R.bool.config_hspa_data_distinguishable);
-            config.inflateSignalStrengths = res.getBoolean(R.bool.config_inflateSignalStrength);
+            config.inflateSignalStrengths = res.getBoolean(
+                    com.android.internal.R.bool.config_inflateSignalStrength);
 
             CarrierConfigManager configMgr = (CarrierConfigManager)
                     context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
index f726321..395add7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
@@ -111,7 +111,9 @@
     public UserSwitcherController(Context context, KeyguardMonitor keyguardMonitor,
             @Named(MAIN_HANDLER_NAME) Handler handler, ActivityStarter activityStarter) {
         mContext = context;
-        mGuestResumeSessionReceiver.register(context);
+        if (!UserManager.isGuestUserEphemeral()) {
+            mGuestResumeSessionReceiver.register(context);
+        }
         mKeyguardMonitor = keyguardMonitor;
         mHandler = handler;
         mActivityStarter = activityStarter;
diff --git a/packages/SystemUI/src/com/android/systemui/util/AutoMarqueeTextView.java b/packages/SystemUI/src/com/android/systemui/util/AutoMarqueeTextView.java
index d9d410d..09dbfee 100644
--- a/packages/SystemUI/src/com/android/systemui/util/AutoMarqueeTextView.java
+++ b/packages/SystemUI/src/com/android/systemui/util/AutoMarqueeTextView.java
@@ -49,6 +49,11 @@
     }
 
     @Override
+    protected void onFinishInflate() {
+        onVisibilityAggregated(isVisibleToUser());
+    }
+
+    @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         setSelected(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java
index b9afea1..cc31531 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java
@@ -16,7 +16,10 @@
 
 package com.android.systemui.appops;
 
+import static junit.framework.TestCase.assertFalse;
+
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -189,4 +192,21 @@
                 AppOpsManager.MODE_ALLOWED);
         verify(mMockHandler).scheduleRemoval(any(AppOpItem.class), anyLong());
     }
+
+    @Test
+    public void noItemsAfterStopListening() {
+        mController.setBGHandler(mMockHandler);
+
+        mController.setListening(true);
+        mController.onOpActiveChanged(AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME,
+                true);
+        mController.onOpNoted(AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME,
+                AppOpsManager.MODE_ALLOWED);
+        assertFalse(mController.getActiveAppOps().isEmpty());
+
+        mController.setListening(false);
+
+        verify(mMockHandler).removeCallbacksAndMessages(null);
+        assertTrue(mController.getActiveAppOps().isEmpty());
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/assist/AssistHandleBehaviorControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/assist/AssistHandleBehaviorControllerTest.java
index 13c92b6..18f114a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/assist/AssistHandleBehaviorControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/assist/AssistHandleBehaviorControllerTest.java
@@ -18,21 +18,27 @@
 
 import static org.mockito.AdditionalAnswers.answerVoid;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
 
+import android.content.ComponentName;
 import android.os.Handler;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper.RunWithLooper;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.app.AssistUtils;
 import com.android.systemui.ScreenDecorations;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.recents.OverviewProxyService;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -46,29 +52,35 @@
 @RunWithLooper
 public class AssistHandleBehaviorControllerTest extends SysuiTestCase {
 
-    private final AssistHandleBehavior mTestBehavior = AssistHandleBehavior.TEST;
+    private static final ComponentName COMPONENT_NAME = new ComponentName("", "");
 
     private AssistHandleBehaviorController mAssistHandleBehaviorController;
 
     @Mock private ScreenDecorations mMockScreenDecorations;
+    @Mock private AssistUtils mMockAssistUtils;
     @Mock private Handler mMockHandler;
     @Mock private AssistHandleBehaviorController.BehaviorController mMockBehaviorController;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
+        mDependency.injectMockDependency(StatusBarStateController.class);
+        mDependency.injectMockDependency(OverviewProxyService.class);
         doAnswer(answerVoid(Runnable::run)).when(mMockHandler).post(any(Runnable.class));
         doAnswer(answerVoid(Runnable::run)).when(mMockHandler)
                 .postDelayed(any(Runnable.class), anyLong());
-        mTestBehavior.setTestController(mMockBehaviorController);
         mAssistHandleBehaviorController =
                 new AssistHandleBehaviorController(
-                        mContext, mMockHandler, () -> mMockScreenDecorations);
+                        mContext,
+                        mMockAssistUtils,
+                        mMockHandler, () -> mMockScreenDecorations,
+                        mMockBehaviorController);
     }
 
     @Test
     public void hide_hidesHandlesWhenShowing() {
         // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
         mAssistHandleBehaviorController.showAndStay();
         reset(mMockScreenDecorations);
 
@@ -83,6 +95,7 @@
     @Test
     public void hide_doesNothingWhenHiding() {
         // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
         mAssistHandleBehaviorController.hide();
         reset(mMockScreenDecorations);
 
@@ -96,6 +109,7 @@
     @Test
     public void showAndStay_showsHandlesWhenHiding() {
         // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
         mAssistHandleBehaviorController.hide();
         reset(mMockScreenDecorations);
 
@@ -110,6 +124,7 @@
     @Test
     public void showAndStay_doesNothingWhenShowing() {
         // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
         mAssistHandleBehaviorController.showAndStay();
         reset(mMockScreenDecorations);
 
@@ -121,8 +136,23 @@
     }
 
     @Test
+    public void showAndStay_doesNothingWhenThereIsNoAssistant() {
+        // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(null);
+        mAssistHandleBehaviorController.hide();
+        reset(mMockScreenDecorations);
+
+        // Act
+        mAssistHandleBehaviorController.showAndStay();
+
+        // Assert
+        verifyNoMoreInteractions(mMockScreenDecorations);
+    }
+
+    @Test
     public void showAndGo_showsThenHidesHandlesWhenHiding() {
         // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
         mAssistHandleBehaviorController.hide();
         reset(mMockScreenDecorations);
 
@@ -139,6 +169,7 @@
     @Test
     public void showAndGo_hidesHandlesAfterTimeoutWhenShowing() {
         // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
         mAssistHandleBehaviorController.showAndStay();
         reset(mMockScreenDecorations);
 
@@ -153,6 +184,7 @@
     @Test
     public void showAndGo_doesNothingIfRecentlyHidden() {
         // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
         mAssistHandleBehaviorController.showAndGo();
         reset(mMockScreenDecorations);
 
@@ -164,12 +196,27 @@
     }
 
     @Test
+    public void showAndGo_doesNothingWhenThereIsNoAssistant() {
+        // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(null);
+        mAssistHandleBehaviorController.hide();
+        reset(mMockScreenDecorations);
+
+        // Act
+        mAssistHandleBehaviorController.showAndGo();
+
+        // Assert
+        verifyNoMoreInteractions(mMockScreenDecorations);
+    }
+
+    @Test
     public void setBehavior_activatesTheBehaviorWhenInGesturalMode() {
         // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
         mAssistHandleBehaviorController.setInGesturalModeForTest(true);
 
         // Act
-        mAssistHandleBehaviorController.setBehavior(mTestBehavior);
+        mAssistHandleBehaviorController.setBehavior(AssistHandleBehavior.TEST);
 
         // Assert
         verify(mMockBehaviorController).onModeActivated(mContext, mAssistHandleBehaviorController);
@@ -179,8 +226,10 @@
     @Test
     public void setBehavior_deactivatesThePreviousBehaviorWhenInGesturalMode() {
         // Arrange
-        mAssistHandleBehaviorController.setBehavior(mTestBehavior);
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
+        mAssistHandleBehaviorController.setBehavior(AssistHandleBehavior.TEST);
         mAssistHandleBehaviorController.setInGesturalModeForTest(true);
+        reset(mMockBehaviorController);
 
         // Act
         mAssistHandleBehaviorController.setBehavior(AssistHandleBehavior.OFF);
@@ -193,10 +242,11 @@
     @Test
     public void setBehavior_doesNothingWhenNotInGesturalMode() {
         // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
         mAssistHandleBehaviorController.setInGesturalModeForTest(false);
 
         // Act
-        mAssistHandleBehaviorController.setBehavior(mTestBehavior);
+        mAssistHandleBehaviorController.setBehavior(AssistHandleBehavior.TEST);
 
         // Assert
         verifyNoMoreInteractions(mMockBehaviorController);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java
index 756cf3e..b324235 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java
@@ -60,8 +60,8 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        addOneMoreThanRenderLimitBubbles();
-        mLayout.setController(mExpandedController);
+        addOneMoreThanBubbleLimitBubbles();
+        mLayout.setActiveController(mExpandedController);
 
         Resources res = mLayout.getResources();
         mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
@@ -73,14 +73,14 @@
     @Test
     public void testExpansionAndCollapse() throws InterruptedException {
         Runnable afterExpand = Mockito.mock(Runnable.class);
-        mExpandedController.expandFromStack(mExpansionPoint, afterExpand);
+        mExpandedController.expandFromStack(afterExpand);
         waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
 
         testBubblesInCorrectExpandedPositions();
         verify(afterExpand).run();
 
         Runnable afterCollapse = Mockito.mock(Runnable.class);
-        mExpandedController.collapseBackToStack(afterCollapse);
+        mExpandedController.collapseBackToStack(mExpansionPoint, afterCollapse);
         waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
 
         testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y, -1);
@@ -139,7 +139,6 @@
         assertEquals(500f, draggedBubble.getTranslationY(), 1f);
 
         // Snap it back and make sure it made it back correctly.
-        mExpandedController.prepareForDismissalWithVelocity(draggedBubble, 0f, 0f);
         mLayout.removeView(draggedBubble);
         waitForLayoutMessageQueue();
         waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
@@ -169,7 +168,7 @@
 
         // Dismiss the now-magneted bubble, verify that the callback was called.
         final Runnable afterDismiss = Mockito.mock(Runnable.class);
-        mExpandedController.dismissDraggedOutBubble(afterDismiss);
+        mExpandedController.dismissDraggedOutBubble(draggedOutView, afterDismiss);
         waitForPropertyAnimations(DynamicAnimation.ALPHA);
         verify(after).run();
 
@@ -224,7 +223,7 @@
 
     /** Expand the stack and wait for animations to finish. */
     private void expand() throws InterruptedException {
-        mExpandedController.expandFromStack(mExpansionPoint, Mockito.mock(Runnable.class));
+        mExpandedController.expandFromStack(Mockito.mock(Runnable.class));
         waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
     }
 
@@ -236,26 +235,19 @@
             assertEquals(x + i * offsetMultiplier * mStackOffset,
                     mLayout.getChildAt(i).getTranslationX(), 2f);
             assertEquals(y, mLayout.getChildAt(i).getTranslationY(), 2f);
-
-            if (i < mMaxRenderedBubbles) {
-                assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f);
-            }
+            assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f);
         }
     }
 
     /** Check that children are in the correct positions for being expanded. */
     private void testBubblesInCorrectExpandedPositions() {
         // Check all the visible bubbles to see if they're in the right place.
-        for (int i = 0; i < Math.min(mLayout.getChildCount(), mMaxRenderedBubbles); i++) {
+        for (int i = 0; i < mLayout.getChildCount(); i++) {
             assertEquals(getBubbleLeft(i),
                     mLayout.getChildAt(i).getTranslationX(),
                     2f);
             assertEquals(mExpandedController.getExpandedY(),
                     mLayout.getChildAt(i).getTranslationY(), 2f);
-
-            if (i < mMaxRenderedBubbles) {
-                assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f);
-            }
         }
     }
 
@@ -273,9 +265,7 @@
             return 0;
         }
         int bubbleCount = mLayout.getChildCount();
-        if (bubbleCount > mMaxRenderedBubbles) {
-            bubbleCount = mMaxRenderedBubbles;
-        }
+
         // Width calculations.
         double bubble = bubbleCount * mBubbleSize;
         float gap = (bubbleCount - 1) * mBubblePadding;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTest.java
index eef6ddc..f8b32c2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTest.java
@@ -23,7 +23,6 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
@@ -77,21 +76,9 @@
     }
 
     @Test
-    public void testRenderVisibility() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
-
-        // The last child should be GONE, the rest VISIBLE.
-        for (int i = 0; i < mMaxRenderedBubbles + 1; i++) {
-            assertEquals(i == mMaxRenderedBubbles ? View.GONE : View.VISIBLE,
-                    mLayout.getChildAt(i).getVisibility());
-        }
-    }
-
-    @Test
     public void testHierarchyChanges() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
 
         // Make sure the controller was notified of all the views we added.
         for (View mView : mViews) {
@@ -115,8 +102,8 @@
 
     @Test
     public void testUpdateValueNotChained() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
 
         // Don't chain any values.
         mTestableController.setChainedProperties(Sets.newHashSet());
@@ -146,8 +133,8 @@
 
     @Test
     public void testSetEndActions() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
         mTestableController.setChainedProperties(Sets.newHashSet());
 
         final CountDownLatch xLatch = new CountDownLatch(1);
@@ -189,8 +176,8 @@
 
     @Test
     public void testRemoveEndListeners() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
         mTestableController.setChainedProperties(Sets.newHashSet());
 
         final CountDownLatch xLatch = new CountDownLatch(1);
@@ -229,8 +216,8 @@
     public void testSetController() throws InterruptedException {
         // Add the bubbles, then set the controller, to make sure that a controller added to an
         // already-initialized view works correctly.
-        addOneMoreThanRenderLimitBubbles();
-        mLayout.setController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
+        mLayout.setActiveController(mTestableController);
         testChainedTranslationAnimations();
 
         TestableAnimationController secondController =
@@ -243,7 +230,7 @@
                 DynamicAnimation.SCALE_X, 10f);
         secondController.setRemoveImmediately(true);
 
-        mLayout.setController(secondController);
+        mLayout.setActiveController(secondController);
         mTestableController.animationForChildAtIndex(0)
                 .scaleX(1.5f)
                 .start();
@@ -266,7 +253,7 @@
         Mockito.verify(secondController, Mockito.atLeastOnce())
                 .getOffsetForChainedPropertyAnimation(eq(DynamicAnimation.SCALE_X));
 
-        mLayout.setController(mTestableController);
+        mLayout.setActiveController(mTestableController);
         mTestableController.animationForChildAtIndex(0)
                 .translationX(100f)
                 .start();
@@ -283,8 +270,8 @@
 
     @Test
     public void testArePropertiesAnimating() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
 
         assertFalse(mLayout.arePropertiesAnimating(
                 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y));
@@ -307,8 +294,8 @@
 
     @Test
     public void testCancelAllAnimations() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
 
         mTestableController.animationForChildAtIndex(0)
                 .position(1000, 1000)
@@ -321,29 +308,10 @@
         assertTrue(mViews.get(0).getTranslationY() < 1000);
     }
 
-    @Test
-    public void testSetChildVisibility() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
-
-        // The last view should have been set to GONE by the controller, since we added one more
-        // than the limit and it got pushed off. None of the first children should have been set
-        // VISIBLE, since they would have been animated in by onChildAdded.
-        Mockito.verify(mTestableController).setChildVisibility(
-                mViews.get(mViews.size() - 1), 5, View.GONE);
-        Mockito.verify(mTestableController, never()).setChildVisibility(
-                any(View.class), anyInt(), eq(View.VISIBLE));
-
-        // Remove the first view, which should cause the last view to become visible again.
-        mLayout.removeView(mViews.get(0));
-        Mockito.verify(mTestableController).setChildVisibility(
-                mViews.get(mViews.size() - 1), 4, View.VISIBLE);
-    }
-
     /** Standard test of chained translation animations. */
     private void testChainedTranslationAnimations() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
 
         assertEquals(0, mLayout.getChildAt(0).getTranslationX(), .1f);
         assertEquals(0, mLayout.getChildAt(1).getTranslationX(), .1f);
@@ -354,11 +322,7 @@
 
         waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X);
 
-        // Since we enabled chaining, animating the first view to 100 should animate the second to
-        // 115 (since we set the offset to 15) and the third to 130, etc. Despite the sixth bubble
-        // not being visible, or animated, make sure that it has the appropriate chained
-        // translation.
-        for (int i = 0; i < mMaxRenderedBubbles + 1; i++) {
+        for (int i = 0; i < mLayout.getChildCount(); i++) {
             assertEquals(
                     100 + i * TEST_TRANSLATION_X_OFFSET,
                     mLayout.getChildAt(i).getTranslationX(), .1f);
@@ -383,8 +347,8 @@
 
     @Test
     public void testPhysicsAnimator() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
 
         Runnable afterAll = Mockito.mock(Runnable.class);
         Runnable after = Mockito.spy(new Runnable() {
@@ -430,9 +394,9 @@
         // Don't chain since we're going to invoke each animation independently.
         mTestableController.setChainedProperties(new HashSet<>());
 
-        mLayout.setController(mTestableController);
+        mLayout.setActiveController(mTestableController);
 
-        addOneMoreThanRenderLimitBubbles();
+        addOneMoreThanBubbleLimitBubbles();
 
         Runnable allEnd = Mockito.mock(Runnable.class);
 
@@ -452,7 +416,7 @@
 
     @Test
     public void testAnimationsForChildrenFromIndex_noChildren() {
-        mLayout.setController(mTestableController);
+        mLayout.setActiveController(mTestableController);
 
         final Runnable after = Mockito.mock(Runnable.class);
         mTestableController
@@ -523,8 +487,9 @@
         }
 
         @Override
-        protected void setChildVisibility(View child, int index, int visibility) {
-            super.setChildVisibility(child, index, visibility);
-        }
+        void onChildReordered(View child, int oldIndex, int newIndex) {}
+
+        @Override
+        void onActiveControllerForLayout(PhysicsAnimationLayout layout) {}
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java
index c6acef5d..f633f39 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java
@@ -56,7 +56,6 @@
 
     Handler mMainThreadHandler;
 
-    int mMaxRenderedBubbles;
     int mSystemWindowInsetSize = 50;
     int mCutoutInsetSize = 100;
 
@@ -69,6 +68,8 @@
     @Mock
     private DisplayCutout mCutout;
 
+    private int mMaxBubbles;
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -79,7 +80,7 @@
         mLayout.setTop(0);
         mLayout.setBottom(mHeight);
 
-        mMaxRenderedBubbles =
+        mMaxBubbles =
                 getContext().getResources().getInteger(R.integer.bubbles_max_rendered);
         mMainThreadHandler = new Handler(Looper.getMainLooper());
 
@@ -96,8 +97,8 @@
     }
 
     /** Add one extra bubble over the limit, so we can make sure it's gone/chains appropriately. */
-    void addOneMoreThanRenderLimitBubbles() throws InterruptedException {
-        for (int i = 0; i < mMaxRenderedBubbles + 1; i++) {
+    void addOneMoreThanBubbleLimitBubbles() throws InterruptedException {
+        for (int i = 0; i < mMaxBubbles + 1; i++) {
             final View newView = new FrameLayout(mContext);
             mLayout.addView(newView, 0);
             mViews.add(0, newView);
@@ -138,6 +139,13 @@
         }
 
         @Override
+        protected boolean isActiveController(PhysicsAnimationController controller) {
+            // Return true since otherwise all test controllers will be seen as inactive since they
+            // are wrapped by MainThreadAnimationControllerWrapper.
+            return true;
+        }
+
+        @Override
         public boolean post(Runnable action) {
             return mMainThreadHandler.post(action);
         }
@@ -148,9 +156,9 @@
         }
 
         @Override
-        public void setController(PhysicsAnimationController controller) {
+        public void setActiveController(PhysicsAnimationController controller) {
             runOnMainThreadAndBlock(
-                    () -> super.setController(
+                    () -> super.setActiveController(
                             new MainThreadAnimationControllerWrapper(controller)));
         }
 
@@ -267,8 +275,15 @@
             }
 
             @Override
-            protected void setChildVisibility(View child, int index, int visibility) {
-                mWrappedController.setChildVisibility(child, index, visibility);
+            void onChildReordered(View child, int oldIndex, int newIndex) {
+                runOnMainThreadAndBlock(
+                        () -> mWrappedController.onChildReordered(child, oldIndex, newIndex));
+            }
+
+            @Override
+            void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
+                runOnMainThreadAndBlock(
+                        () -> mWrappedController.onActiveControllerForLayout(layout));
             }
 
             @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java
index 9218a8b..31a7d5a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java
@@ -54,8 +54,8 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        mLayout.setController(mStackController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mStackController);
+        addOneMoreThanBubbleLimitBubbles();
         mStackOffset = mLayout.getResources().getDimensionPixelSize(R.dimen.bubble_stack_offset);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/colorextraction/SysuiColorExtractorTests.java b/packages/SystemUI/tests/src/com/android/systemui/colorextraction/SysuiColorExtractorTests.java
index 67df60a..9c2c822 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/colorextraction/SysuiColorExtractorTests.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/colorextraction/SysuiColorExtractorTests.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.reset;
@@ -36,8 +37,10 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
 
 /**
  * Tests color extraction generation.
@@ -54,62 +57,68 @@
             ColorExtractor.TYPE_DARK,
             ColorExtractor.TYPE_EXTRA_DARK};
 
+    private ColorExtractor.GradientColors mColors;
+    private SysuiColorExtractor mColorExtractor;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mColors = new ColorExtractor.GradientColors();
+        mColors.setMainColor(Color.RED);
+        mColors.setSecondaryColor(Color.RED);
+        mColorExtractor = new SysuiColorExtractor(getContext(),
+                (inWallpaperColors, outGradientColorsNormal, outGradientColorsDark,
+                        outGradientColorsExtraDark) -> {
+                    outGradientColorsNormal.set(mColors);
+                    outGradientColorsDark.set(mColors);
+                    outGradientColorsExtraDark.set(mColors);
+                }, mock(ConfigurationController.class), false);
+    }
+
     @Test
     public void getColors_usesGreyIfWallpaperNotVisible() {
-        ColorExtractor.GradientColors colors = new ColorExtractor.GradientColors();
-        colors.setMainColor(Color.RED);
-        colors.setSecondaryColor(Color.RED);
+        simulateEvent(mColorExtractor);
+        mColorExtractor.setWallpaperVisible(false);
 
-        SysuiColorExtractor extractor = getTestableExtractor(colors);
-        simulateEvent(extractor);
-        extractor.setWallpaperVisible(false);
-
-        ColorExtractor.GradientColors fallbackColors = extractor.getNeutralColors();
+        ColorExtractor.GradientColors fallbackColors = mColorExtractor.getNeutralColors();
 
         for (int type : sTypes) {
             assertEquals("Not using fallback!",
-                    extractor.getColors(WallpaperManager.FLAG_SYSTEM, type), fallbackColors);
+                    mColorExtractor.getColors(WallpaperManager.FLAG_SYSTEM, type), fallbackColors);
             assertNotEquals("Wallpaper visibility event should not affect lock wallpaper.",
-                    extractor.getColors(WallpaperManager.FLAG_LOCK, type), fallbackColors);
+                    mColorExtractor.getColors(WallpaperManager.FLAG_LOCK, type), fallbackColors);
         }
     }
 
     @Test
     public void getColors_doesntUseFallbackIfVisible() {
-        ColorExtractor.GradientColors colors = new ColorExtractor.GradientColors();
-        colors.setMainColor(Color.RED);
-        colors.setSecondaryColor(Color.RED);
+        mColors.setMainColor(Color.RED);
+        mColors.setSecondaryColor(Color.RED);
 
-        SysuiColorExtractor extractor = getTestableExtractor(colors);
-        simulateEvent(extractor);
-        extractor.setWallpaperVisible(true);
+        simulateEvent(mColorExtractor);
+        mColorExtractor.setWallpaperVisible(true);
 
         for (int which : sWhich) {
             for (int type : sTypes) {
                 assertEquals("Not using extracted colors!",
-                        extractor.getColors(which, type), colors);
+                        mColorExtractor.getColors(which, type), mColors);
             }
         }
     }
 
     @Test
     public void getColors_fallbackWhenMediaIsVisible() {
-        ColorExtractor.GradientColors colors = new ColorExtractor.GradientColors();
-        colors.setMainColor(Color.RED);
-        colors.setSecondaryColor(Color.RED);
+        simulateEvent(mColorExtractor);
+        mColorExtractor.setWallpaperVisible(true);
+        mColorExtractor.setHasBackdrop(true);
 
-        SysuiColorExtractor extractor = getTestableExtractor(colors);
-        simulateEvent(extractor);
-        extractor.setWallpaperVisible(true);
-        extractor.setHasBackdrop(true);
-
-        ColorExtractor.GradientColors fallbackColors = extractor.getNeutralColors();
+        ColorExtractor.GradientColors fallbackColors = mColorExtractor.getNeutralColors();
 
         for (int type : sTypes) {
             assertEquals("Not using fallback!",
-                    extractor.getColors(WallpaperManager.FLAG_LOCK, type), fallbackColors);
+                    mColorExtractor.getColors(WallpaperManager.FLAG_LOCK, type), fallbackColors);
             assertNotEquals("Media visibility should not affect system wallpaper.",
-                    extractor.getColors(WallpaperManager.FLAG_SYSTEM, type), fallbackColors);
+                    mColorExtractor.getColors(WallpaperManager.FLAG_SYSTEM, type), fallbackColors);
         }
     }
 
@@ -126,14 +135,13 @@
         verify(tonal).applyFallback(any(), any());
     }
 
-    private SysuiColorExtractor getTestableExtractor(ColorExtractor.GradientColors colors) {
-        return new SysuiColorExtractor(getContext(),
-                (inWallpaperColors, outGradientColorsNormal, outGradientColorsDark,
-                        outGradientColorsExtraDark) -> {
-                    outGradientColorsNormal.set(colors);
-                    outGradientColorsDark.set(colors);
-                    outGradientColorsExtraDark.set(colors);
-                }, mock(ConfigurationController.class), false);
+    @Test
+    public void onUiModeChanged_notifiesListener() {
+        ColorExtractor.OnColorsChangedListener listener = mock(
+                ColorExtractor.OnColorsChangedListener.class);
+        mColorExtractor.addOnColorsChangedListener(listener);
+        mColorExtractor.onUiModeChanged();
+        verify(listener).onColorsChanged(any(), anyInt());
     }
 
     private void simulateEvent(SysuiColorExtractor extractor) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsGridLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsGridLayoutTest.java
index a396f3e..ea47859 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsGridLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsGridLayoutTest.java
@@ -48,8 +48,9 @@
 
     @Before
     public void setUp() throws Exception {
-        mGridLayout = spy((GlobalActionsGridLayout)
-                LayoutInflater.from(mContext).inflate(R.layout.global_actions_grid, null));
+        mGridLayout = spy(LayoutInflater.from(mContext)
+                .inflate(R.layout.global_actions_grid, null)
+                .requireViewById(R.id.global_actions_view));
         mListGrid = spy(mGridLayout.getListView());
         doReturn(mListGrid).when(mGridLayout).getListView();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/ListGridLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/ListGridLayoutTest.java
index 746140f..74e8cc2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/ListGridLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/ListGridLayoutTest.java
@@ -43,8 +43,9 @@
 
     @Before
     public void setUp() throws Exception {
-        GlobalActionsGridLayout globalActions = (GlobalActionsGridLayout)
-                LayoutInflater.from(mContext).inflate(R.layout.global_actions_grid, null);
+        GlobalActionsGridLayout globalActions = LayoutInflater.from(mContext)
+                .inflate(R.layout.global_actions_grid, null)
+                .requireViewById(R.id.global_actions_view);
         mListGridLayout = globalActions.getListView();
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
index 4d593c1..72f3a62 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
@@ -402,6 +402,7 @@
         verify(mRow).setEntry(eq(mEntry));
         assertEquals(1, mEntry.systemGeneratedSmartActions.size());
         assertEquals("action", mEntry.systemGeneratedSmartActions.get(0).title);
+        verify(mEntryListener).onNotificationRankingUpdated(mRankingMap);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationDataTest.java
index 79a6ac4..6e0ddbf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationDataTest.java
@@ -28,6 +28,7 @@
 
 import static com.android.systemui.statusbar.notification.collection.NotificationDataTest.TestableNotificationData.OVERRIDE_CHANNEL;
 import static com.android.systemui.statusbar.notification.collection.NotificationDataTest.TestableNotificationData.OVERRIDE_IMPORTANCE;
+import static com.android.systemui.statusbar.notification.collection.NotificationDataTest.TestableNotificationData.OVERRIDE_RANK;
 import static com.android.systemui.statusbar.notification.collection.NotificationDataTest.TestableNotificationData.OVERRIDE_VIS_EFFECTS;
 
 import static junit.framework.Assert.assertEquals;
@@ -61,16 +62,12 @@
 import android.testing.TestableLooper.RunWithLooper;
 import android.util.ArraySet;
 
-import androidx.test.filters.SmallTest;
-
 import com.android.systemui.Dependency;
 import com.android.systemui.ForegroundServiceController;
 import com.android.systemui.InitController;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.statusbar.NotificationTestHelper;
-import com.android.systemui.statusbar.notification.collection.NotificationData;
 import com.android.systemui.statusbar.notification.collection.NotificationData.KeyguardEnvironment;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.statusbar.phone.ShadeController;
@@ -83,7 +80,11 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+
+import androidx.test.filters.SmallTest;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
@@ -137,7 +138,9 @@
 
     @Test
     public void testChannelSetWhenAdded() {
-        mNotificationData.rankingOverrides.putParcelable(OVERRIDE_CHANNEL, NOTIFICATION_CHANNEL);
+        Bundle override = new Bundle();
+        override.putParcelable(OVERRIDE_CHANNEL, NOTIFICATION_CHANNEL);
+        mNotificationData.rankingOverrides.put(mRow.getEntry().key, override);
         mNotificationData.add(mRow.getEntry());
         assertEquals(NOTIFICATION_CHANNEL, mRow.getEntry().channel);
     }
@@ -229,7 +232,9 @@
         n.flags = Notification.FLAG_FOREGROUND_SERVICE;
         NotificationEntry entry = new NotificationEntry(mMockStatusBarNotification);
         mNotificationData.add(entry);
-        mNotificationData.rankingOverrides.putInt(OVERRIDE_VIS_EFFECTS, 255);
+        Bundle override = new Bundle();
+        override.putInt(OVERRIDE_VIS_EFFECTS, 255);
+        mNotificationData.rankingOverrides.put(entry.key, override);
 
         assertTrue(entry.isExemptFromDndVisualSuppression());
         assertFalse(entry.shouldSuppressAmbient());
@@ -245,7 +250,9 @@
         when(mMockStatusBarNotification.getNotification()).thenReturn(n);
         NotificationEntry entry = new NotificationEntry(mMockStatusBarNotification);
         mNotificationData.add(entry);
-        mNotificationData.rankingOverrides.putInt(OVERRIDE_VIS_EFFECTS, 255);
+        Bundle override = new Bundle();
+        override.putInt(OVERRIDE_VIS_EFFECTS, 255);
+        mNotificationData.rankingOverrides.put(entry.key, override);
 
         assertTrue(entry.isExemptFromDndVisualSuppression());
         assertFalse(entry.shouldSuppressAmbient());
@@ -257,7 +264,9 @@
         NotificationEntry entry = new NotificationEntry(mMockStatusBarNotification);
         entry.mIsSystemNotification = true;
         mNotificationData.add(entry);
-        mNotificationData.rankingOverrides.putInt(OVERRIDE_VIS_EFFECTS, 255);
+        Bundle override = new Bundle();
+        override.putInt(OVERRIDE_VIS_EFFECTS, 255);
+        mNotificationData.rankingOverrides.put(entry.key, override);
 
         assertTrue(entry.isExemptFromDndVisualSuppression());
         assertFalse(entry.shouldSuppressAmbient());
@@ -268,8 +277,9 @@
         initStatusBarNotification(false);
         NotificationEntry entry = new NotificationEntry(mMockStatusBarNotification);
         entry.mIsSystemNotification = true;
-        mNotificationData.rankingOverrides.putInt(OVERRIDE_VIS_EFFECTS,
-                NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT);
+        Bundle override = new Bundle();
+        override.putInt(OVERRIDE_VIS_EFFECTS, NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT);
+        mNotificationData.rankingOverrides.put(entry.key, override);
         mNotificationData.add(entry);
 
         when(mMockStatusBarNotification.getNotification()).thenReturn(
@@ -395,11 +405,13 @@
         Notification notification = mock(Notification.class);
         when(notification.isForegroundService()).thenReturn(true);
 
-        mNotificationData.rankingOverrides.putInt(OVERRIDE_IMPORTANCE, IMPORTANCE_MIN);
-
         StatusBarNotification sbn = new StatusBarNotification("pkg", "pkg", 0, "tag", 0, 0,
                 notification, mContext.getUser(), "", 0);
 
+        Bundle override = new Bundle();
+        override.putInt(OVERRIDE_IMPORTANCE, IMPORTANCE_MIN);
+        mNotificationData.rankingOverrides.put(sbn.getKey(), override);
+
         assertFalse(mNotificationData.isHighPriority(sbn));
     }
 
@@ -408,12 +420,112 @@
         Notification notification = mock(Notification.class);
         when(notification.isForegroundService()).thenReturn(true);
 
-        mNotificationData.rankingOverrides.putInt(OVERRIDE_IMPORTANCE, IMPORTANCE_LOW);
+        StatusBarNotification sbn = new StatusBarNotification("pkg", "pkg", 0, "tag", 0, 0,
+                notification, mContext.getUser(), "", 0);
+
+        Bundle override = new Bundle();
+        override.putInt(OVERRIDE_IMPORTANCE, IMPORTANCE_LOW);
+        mNotificationData.rankingOverrides.put(sbn.getKey(), override);
+
+        assertTrue(mNotificationData.isHighPriority(sbn));
+    }
+
+    @Test
+    public void userChangeTrumpsHighPriorityCharacteristics() {
+        Person person = new Person.Builder()
+                .setName("name")
+                .setKey("abc")
+                .setUri("uri")
+                .setBot(true)
+                .build();
+
+        Notification notification = new Notification.Builder(mContext, "test")
+                .addPerson(person)
+                .setStyle(new Notification.MessagingStyle(""))
+                .setFlag(Notification.FLAG_FOREGROUND_SERVICE, true)
+                .build();
 
         StatusBarNotification sbn = new StatusBarNotification("pkg", "pkg", 0, "tag", 0, 0,
                 notification, mContext.getUser(), "", 0);
 
-        assertTrue(mNotificationData.isHighPriority(sbn));
+        NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW);
+        channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
+
+        Bundle override = new Bundle();
+        override.putParcelable(OVERRIDE_CHANNEL, channel);
+        mNotificationData.rankingOverrides.put(sbn.getKey(), override);
+
+        assertFalse(mNotificationData.isHighPriority(sbn));
+    }
+
+    @Test
+    public void testSort_highPriorityTrumpsNMSRank() {
+        // NMS rank says A and then B. But A is not high priority and B is, so B should sort in
+        // front
+        Notification aN = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle(""))
+                .build();
+        StatusBarNotification aSbn = new StatusBarNotification("pkg", "pkg", 0, "tag", 0, 0,
+                aN, mContext.getUser(), "", 0);
+        NotificationEntry a = new NotificationEntry(aSbn);
+        a.setRow(mock(ExpandableNotificationRow.class));
+        a.setIsHighPriority(false);
+
+        Bundle override = new Bundle();
+        override.putInt(OVERRIDE_IMPORTANCE, IMPORTANCE_LOW);
+        override.putInt(OVERRIDE_RANK, 1);
+        mNotificationData.rankingOverrides.put(a.key, override);
+
+        Notification bN = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle(""))
+                .build();
+        StatusBarNotification bSbn = new StatusBarNotification("pkg2", "pkg2", 0, "tag", 0, 0,
+                bN, mContext.getUser(), "", 0);
+        NotificationEntry b = new NotificationEntry(bSbn);
+        b.setIsHighPriority(true);
+        b.setRow(mock(ExpandableNotificationRow.class));
+
+        Bundle bOverride = new Bundle();
+        bOverride.putInt(OVERRIDE_IMPORTANCE, IMPORTANCE_LOW);
+        bOverride.putInt(OVERRIDE_RANK, 2);
+        mNotificationData.rankingOverrides.put(b.key, bOverride);
+
+        assertEquals(1, mNotificationData.mRankingComparator.compare(a, b));
+    }
+
+    @Test
+    public void testSort_samePriorityUsesNMSRank() {
+        // NMS rank says A and then B. But A is not high priority and B is, so B should sort in
+        // front
+        Notification aN = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle(""))
+                .build();
+        StatusBarNotification aSbn = new StatusBarNotification("pkg", "pkg", 0, "tag", 0, 0,
+                aN, mContext.getUser(), "", 0);
+        NotificationEntry a = new NotificationEntry(aSbn);
+        a.setRow(mock(ExpandableNotificationRow.class));
+        a.setIsHighPriority(false);
+
+        Bundle override = new Bundle();
+        override.putInt(OVERRIDE_IMPORTANCE, IMPORTANCE_LOW);
+        override.putInt(OVERRIDE_RANK, 1);
+        mNotificationData.rankingOverrides.put(a.key, override);
+
+        Notification bN = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle(""))
+                .build();
+        StatusBarNotification bSbn = new StatusBarNotification("pkg2", "pkg2", 0, "tag", 0, 0,
+                bN, mContext.getUser(), "", 0);
+        NotificationEntry b = new NotificationEntry(bSbn);
+        b.setRow(mock(ExpandableNotificationRow.class));
+        b.setIsHighPriority(false);
+
+        Bundle bOverride = new Bundle();
+        bOverride.putInt(OVERRIDE_IMPORTANCE, IMPORTANCE_LOW);
+        bOverride.putInt(OVERRIDE_RANK, 2);
+        mNotificationData.rankingOverrides.put(b.key, bOverride);
+
+        assertEquals(-1, mNotificationData.mRankingComparator.compare(a, b));
     }
 
     private void initStatusBarNotification(boolean allowDuringSetup) {
@@ -449,7 +561,7 @@
         public static final String OVERRIDE_SMART_REPLIES = "sr";
         public static final String OVERRIDE_BUBBLE = "cb";
 
-        public Bundle rankingOverrides = new Bundle();
+        public Map<String, Bundle> rankingOverrides = new HashMap<>();
 
         @Override
         protected boolean getRanking(String key, Ranking outRanking) {
@@ -475,40 +587,41 @@
                 currentReplies.addAll(outRanking.getSmartReplies());
             }
 
-            outRanking.populate(key,
-                    rankingOverrides.getInt(OVERRIDE_RANK, outRanking.getRank()),
-                    rankingOverrides.getBoolean(OVERRIDE_DND,
-                            outRanking.matchesInterruptionFilter()),
-                    rankingOverrides.getInt(OVERRIDE_VIS_OVERRIDE,
-                            outRanking.getVisibilityOverride()),
-                    rankingOverrides.getInt(OVERRIDE_VIS_EFFECTS,
-                            outRanking.getSuppressedVisualEffects()),
-                    rankingOverrides.getInt(OVERRIDE_IMPORTANCE, outRanking.getImportance()),
-                    rankingOverrides.getCharSequence(OVERRIDE_IMP_EXP,
-                            outRanking.getImportanceExplanation()),
-                    rankingOverrides.getString(OVERRIDE_GROUP, outRanking.getOverrideGroupKey()),
-                    rankingOverrides.containsKey(OVERRIDE_CHANNEL)
-                            ? (NotificationChannel) rankingOverrides.getParcelable(OVERRIDE_CHANNEL)
-                            : outRanking.getChannel(),
-                    rankingOverrides.containsKey(OVERRIDE_PEOPLE)
-                            ? rankingOverrides.getStringArrayList(OVERRIDE_PEOPLE)
-                            : currentAdditionalPeople,
-                    rankingOverrides.containsKey(OVERRIDE_SNOOZE_CRITERIA)
-                            ? rankingOverrides.getParcelableArrayList(OVERRIDE_SNOOZE_CRITERIA)
-                            : currentSnooze,
-                    rankingOverrides.getBoolean(OVERRIDE_BADGE, outRanking.canShowBadge()),
-                    rankingOverrides.getInt(OVERRIDE_USER_SENTIMENT, outRanking.getUserSentiment()),
-                    rankingOverrides.getBoolean(OVERRIDE_HIDDEN, outRanking.isSuspended()),
-                    rankingOverrides.getLong(OVERRIDE_LAST_ALERTED,
-                            outRanking.getLastAudiblyAlertedMillis()),
-                    rankingOverrides.getBoolean(OVERRIDE_NOISY, outRanking.isNoisy()),
-                    rankingOverrides.containsKey(OVERRIDE_SMART_ACTIONS)
-                            ? rankingOverrides.getParcelableArrayList(OVERRIDE_SMART_ACTIONS)
-                            : currentActions,
-                    rankingOverrides.containsKey(OVERRIDE_SMART_REPLIES)
-                            ? rankingOverrides.getCharSequenceArrayList(OVERRIDE_SMART_REPLIES)
-                            : currentReplies,
-                    rankingOverrides.getBoolean(OVERRIDE_BUBBLE, outRanking.canBubble()));
+            if (rankingOverrides.get(key) != null) {
+                Bundle overrides = rankingOverrides.get(key);
+                outRanking.populate(key,
+                        overrides.getInt(OVERRIDE_RANK, outRanking.getRank()),
+                        overrides.getBoolean(OVERRIDE_DND, outRanking.matchesInterruptionFilter()),
+                        overrides.getInt(OVERRIDE_VIS_OVERRIDE, outRanking.getVisibilityOverride()),
+                        overrides.getInt(OVERRIDE_VIS_EFFECTS,
+                                outRanking.getSuppressedVisualEffects()),
+                        overrides.getInt(OVERRIDE_IMPORTANCE, outRanking.getImportance()),
+                        overrides.getCharSequence(OVERRIDE_IMP_EXP,
+                                outRanking.getImportanceExplanation()),
+                        overrides.getString(OVERRIDE_GROUP, outRanking.getOverrideGroupKey()),
+                        overrides.containsKey(OVERRIDE_CHANNEL)
+                                ? (NotificationChannel) overrides.getParcelable(OVERRIDE_CHANNEL)
+                                : outRanking.getChannel(),
+                        overrides.containsKey(OVERRIDE_PEOPLE)
+                                ? overrides.getStringArrayList(OVERRIDE_PEOPLE)
+                                : currentAdditionalPeople,
+                        overrides.containsKey(OVERRIDE_SNOOZE_CRITERIA)
+                                ? overrides.getParcelableArrayList(OVERRIDE_SNOOZE_CRITERIA)
+                                : currentSnooze,
+                        overrides.getBoolean(OVERRIDE_BADGE, outRanking.canShowBadge()),
+                        overrides.getInt(OVERRIDE_USER_SENTIMENT, outRanking.getUserSentiment()),
+                        overrides.getBoolean(OVERRIDE_HIDDEN, outRanking.isSuspended()),
+                        overrides.getLong(OVERRIDE_LAST_ALERTED,
+                                outRanking.getLastAudiblyAlertedMillis()),
+                        overrides.getBoolean(OVERRIDE_NOISY, outRanking.isNoisy()),
+                        overrides.containsKey(OVERRIDE_SMART_ACTIONS)
+                                ? overrides.getParcelableArrayList(OVERRIDE_SMART_ACTIONS)
+                                : currentActions,
+                        overrides.containsKey(OVERRIDE_SMART_REPLIES)
+                                ? overrides.getCharSequenceArrayList(OVERRIDE_SMART_REPLIES)
+                                : currentReplies,
+                        overrides.getBoolean(OVERRIDE_BUBBLE, outRanking.canBubble()));
+            }
             return true;
         }
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
index 06acc73..78970d9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
@@ -646,7 +646,7 @@
         doCallRealMethod().when(guts).closeControls(anyInt(), anyInt(), anyBoolean(), anyBoolean());
         mNotificationInfo.setGutsParent(guts);
 
-        mNotificationInfo.closeControls(mNotificationInfo);
+        mNotificationInfo.closeControls(mNotificationInfo, true);
 
         verify(mBlockingHelperManager).dismissCurrentBlockingHelper();
     }
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 43ea92f..13b9d56 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
@@ -15,6 +15,7 @@
 package com.android.systemui.statusbar.notification.row;
 
 import static android.provider.Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL;
+import static android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
@@ -100,8 +101,31 @@
 
     @Test
     public void testNoAppOpsInSlowSwipe() {
-        Settings.Secure.putInt(mContext.getContentResolver(),
-                NOTIFICATION_NEW_INTERRUPTION_MODEL, 0);
+        Settings.Secure.putInt(mContext.getContentResolver(), SHOW_NOTIFICATION_SNOOZE, 0);
+
+        NotificationMenuRow row = new NotificationMenuRow(mContext);
+        row.createMenu(mRow, null);
+
+        ViewGroup container = (ViewGroup) row.getMenuView();
+        // noti blocking
+        assertEquals(1, container.getChildCount());
+    }
+
+    @Test
+    public void testNoSnoozeInSlowSwipe() {
+        Settings.Secure.putInt(mContext.getContentResolver(), SHOW_NOTIFICATION_SNOOZE, 0);
+
+        NotificationMenuRow row = new NotificationMenuRow(mContext);
+        row.createMenu(mRow, null);
+
+        ViewGroup container = (ViewGroup) row.getMenuView();
+        // just for noti blocking
+        assertEquals(1, container.getChildCount());
+    }
+
+    @Test
+    public void testSnoozeInSlowSwipe() {
+        Settings.Secure.putInt(mContext.getContentResolver(), SHOW_NOTIFICATION_SNOOZE, 1);
 
         NotificationMenuRow row = new NotificationMenuRow(mContext);
         row.createMenu(mRow, null);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapperTest.java
new file mode 100644
index 0000000..df41a84
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapperTest.java
@@ -0,0 +1,173 @@
+/*
+ * 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.row.wrapper;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.Notification;
+import android.media.MediaMetadata;
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableLooper.RunWithLooper;
+import android.view.View;
+import android.widget.RemoteViews;
+import android.widget.SeekBar;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.NotificationTestHelper;
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@RunWithLooper
+public class NotificationMediaTemplateViewWrapperTest extends SysuiTestCase {
+
+    private ExpandableNotificationRow mRow;
+    private Notification mNotif;
+    private View mView;
+    private NotificationMediaTemplateViewWrapper mWrapper;
+
+    @Mock
+    private MetricsLogger mMetricsLogger;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        com.android.systemui.util.Assert.sMainLooper = TestableLooper.get(this).getLooper();
+
+        mDependency.injectTestDependency(MetricsLogger.class, mMetricsLogger);
+    }
+
+    private void makeTestNotification(long duration, boolean allowSeeking) throws Exception {
+        Notification.Builder builder = new Notification.Builder(mContext)
+                .setSmallIcon(R.drawable.ic_person)
+                .setContentTitle("Title")
+                .setContentText("Text");
+
+        MediaMetadata metadata = new MediaMetadata.Builder()
+                .putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
+                .build();
+        MediaSession session = new MediaSession(mContext, "TEST_CHANNEL");
+        session.setMetadata(metadata);
+
+        PlaybackState playbackState = new PlaybackState.Builder()
+                .setActions(allowSeeking ? PlaybackState.ACTION_SEEK_TO : 0)
+                .build();
+
+        session.setPlaybackState(playbackState);
+
+        builder.setStyle(new Notification.MediaStyle()
+                .setMediaSession(session.getSessionToken())
+        );
+
+        mNotif = builder.build();
+        assertTrue(mNotif.hasMediaSession());
+
+        mRow = new NotificationTestHelper(mContext).createRow(mNotif);
+
+        RemoteViews views = new RemoteViews(mContext.getPackageName(),
+                com.android.internal.R.layout.notification_template_material_big_media);
+        mView = views.apply(mContext, null);
+        mWrapper = new NotificationMediaTemplateViewWrapper(mContext,
+                mView, mRow);
+        mWrapper.onContentUpdated(mRow);
+    }
+
+    @Test
+    public void testLogging_NoSeekbar() throws Exception {
+        // Media sessions with duration <= 0 should not include a seekbar
+        makeTestNotification(0, false);
+
+        verify(mMetricsLogger).write(argThat(logMaker ->
+                logMaker.getCategory() == MetricsEvent.MEDIA_NOTIFICATION_SEEKBAR
+                        && logMaker.getType() == MetricsEvent.TYPE_CLOSE
+        ));
+
+        verify(mMetricsLogger, times(0)).write(argThat(logMaker ->
+                logMaker.getCategory() == MetricsEvent.MEDIA_NOTIFICATION_SEEKBAR
+                        && logMaker.getType() == MetricsEvent.TYPE_OPEN
+        ));
+    }
+
+    @Test
+    public void testLogging_HasSeekbarNoScrubber() throws Exception {
+        // Media sessions that do not support seeking should have a seekbar, but no scrubber
+        makeTestNotification(1000, false);
+
+        verify(mMetricsLogger).write(argThat(logMaker ->
+                logMaker.getCategory() == MetricsEvent.MEDIA_NOTIFICATION_SEEKBAR
+                        && logMaker.getType() == MetricsEvent.TYPE_OPEN
+        ));
+
+        // Ensure the callback runs at least once
+        mWrapper.mUpdatePlaybackUi.run();
+
+        verify(mMetricsLogger).write(argThat(logMaker ->
+                logMaker.getCategory() == MetricsEvent.MEDIA_NOTIFICATION_SEEKBAR
+                && logMaker.getType() == MetricsEvent.TYPE_DETAIL
+                && logMaker.getSubtype() == 0
+        ));
+    }
+
+    @Test
+    public void testLogging_HasSeekbarAndScrubber() throws Exception {
+        makeTestNotification(1000, true);
+
+        verify(mMetricsLogger).write(argThat(logMaker ->
+                logMaker.getCategory() == MetricsEvent.MEDIA_NOTIFICATION_SEEKBAR
+                        && logMaker.getType() == MetricsEvent.TYPE_OPEN
+        ));
+
+        verify(mMetricsLogger).write(argThat(logMaker ->
+                logMaker.getCategory() == MetricsEvent.MEDIA_NOTIFICATION_SEEKBAR
+                        && logMaker.getType() == MetricsEvent.TYPE_DETAIL
+                        && logMaker.getSubtype() == 1
+        ));
+    }
+
+    @Test
+    public void testLogging_UpdateSeekbar() throws Exception {
+        makeTestNotification(1000, true);
+
+        SeekBar seekbar = mView.findViewById(
+                com.android.internal.R.id.notification_media_progress_bar);
+        assertTrue(seekbar != null);
+
+        mWrapper.mSeekListener.onStopTrackingTouch(seekbar);
+
+        verify(mMetricsLogger).write(argThat(logMaker ->
+                logMaker.getCategory() == MetricsEvent.MEDIA_NOTIFICATION_SEEKBAR
+                        && logMaker.getType() == MetricsEvent.TYPE_UPDATE));
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java
index ad9c729..faf5a97 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java
@@ -268,9 +268,11 @@
             when(view.getRecentsButton()).thenReturn(mock(ButtonDispatcher.class));
             when(view.getAccessibilityButton()).thenReturn(mock(ButtonDispatcher.class));
             when(view.getRotateSuggestionButton()).thenReturn(mock(RotationContextButton.class));
-            when(view.getBarTransitions()).thenReturn(mock(BarTransitions.class));
+            when(view.getBarTransitions()).thenReturn(mock(NavigationBarTransitions.class));
             when(view.getLightTransitionsController()).thenReturn(
                     mock(LightBarTransitionsController.class));
+            when(view.getRotationButtonController()).thenReturn(
+                    mock(RotationButtonController.class));
             return view;
         }
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarRotationContextTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarRotationContextTest.java
index 28e91ea..cf08428 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarRotationContextTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarRotationContextTest.java
@@ -51,7 +51,8 @@
     public final SysuiTestableContext mContext = new SysuiTestableContext(
             InstrumentationRegistry.getContext(), getLeakCheck());
     private final TestableDependency mDependency = new TestableDependency(mContext);
-    private RotationContextButton mButton;
+    private RotationButtonController mRotationButtonController;
+    private RotationButton mRotationButton;
 
     @Before
     public void setup() {
@@ -59,50 +60,66 @@
         mDependency.injectMockDependency(RotationLockController.class);
 
         final View view = new View(mContext);
-        mButton = spy(new RotationContextButton(RES_UNDEF, RES_UNDEF, mContext, RES_UNDEF));
+        mRotationButton = mock(RotationButton.class);
+        mRotationButtonController = spy(
+                new RotationButtonController(mContext, RES_UNDEF, mRotationButton));
         final KeyButtonDrawable kbd = mock(KeyButtonDrawable.class);
-        doReturn(view).when(mButton).getCurrentView();
-        doReturn(kbd).when(mButton).getNewDrawable();
+        doReturn(view).when(mRotationButton).getCurrentView();
+        doReturn(true).when(mRotationButton).acceptRotationProposal();
     }
 
     @Test
     public void testOnInvalidRotationProposal() {
-        mButton.onRotationProposal(DEFAULT_ROTATE, DEFAULT_ROTATE + 1, false /* isValid */);
-        verify(mButton, times(1)).setRotateSuggestionButtonState(false /* visible */);
+        mRotationButtonController.onRotationProposal(DEFAULT_ROTATE, DEFAULT_ROTATE + 1,
+                false /* isValid */);
+        verify(mRotationButtonController, times(1)).setRotateSuggestionButtonState(
+                false /* visible */);
     }
 
     @Test
     public void testOnSameRotationProposal() {
-        mButton.onRotationProposal(DEFAULT_ROTATE, DEFAULT_ROTATE, true /* isValid */);
-        verify(mButton, times(1)).setRotateSuggestionButtonState(false /* visible */);
+        mRotationButtonController.onRotationProposal(DEFAULT_ROTATE, DEFAULT_ROTATE,
+                true /* isValid */);
+        verify(mRotationButtonController, times(1)).setRotateSuggestionButtonState(
+                false /* visible */);
     }
 
     @Test
     public void testOnRotationProposalShowButtonShowNav() {
         // No navigation bar should not call to set visibility state
-        mButton.onNavigationBarWindowVisibilityChange(false /* showing */);
-        verify(mButton, times(0)).setRotateSuggestionButtonState(false /* visible */);
-        verify(mButton, times(0)).setRotateSuggestionButtonState(true /* visible */);
+        mRotationButtonController.onNavigationBarWindowVisibilityChange(false /* showing */);
+        verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
+                false /* visible */);
+        verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
+                true /* visible */);
 
         // No navigation bar with rotation change should not call to set visibility state
-        mButton.onRotationProposal(DEFAULT_ROTATE, DEFAULT_ROTATE + 1, true /* isValid */);
-        verify(mButton, times(0)).setRotateSuggestionButtonState(false /* visible */);
-        verify(mButton, times(0)).setRotateSuggestionButtonState(true /* visible */);
+        mRotationButtonController.onRotationProposal(DEFAULT_ROTATE, DEFAULT_ROTATE + 1,
+                true /* isValid */);
+        verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
+                false /* visible */);
+        verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
+                true /* visible */);
 
         // Since rotation has changed rotation should be pending, show mButton when showing nav bar
-        mButton.onNavigationBarWindowVisibilityChange(true /* showing */);
-        verify(mButton, times(1)).setRotateSuggestionButtonState(true /* visible */);
+        mRotationButtonController.onNavigationBarWindowVisibilityChange(true /* showing */);
+        verify(mRotationButtonController, times(1)).setRotateSuggestionButtonState(
+                true /* visible */);
     }
 
     @Test
     public void testOnRotationProposalShowButton() {
         // Navigation bar being visible should not call to set visibility state
-        mButton.onNavigationBarWindowVisibilityChange(true /* showing */);
-        verify(mButton, times(0)).setRotateSuggestionButtonState(false /* visible */);
-        verify(mButton, times(0)).setRotateSuggestionButtonState(true /* visible */);
+        mRotationButtonController.onNavigationBarWindowVisibilityChange(true /* showing */);
+        verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
+                false /* visible */);
+        verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
+                true /* visible */);
 
         // Navigation bar is visible and rotation requested
-        mButton.onRotationProposal(DEFAULT_ROTATE, DEFAULT_ROTATE + 1, true /* isValid */);
-        verify(mButton, times(1)).setRotateSuggestionButtonState(true /* visible */);
+        mRotationButtonController.onRotationProposal(DEFAULT_ROTATE, DEFAULT_ROTATE + 1,
+                true /* isValid */);
+        verify(mRotationButtonController, times(1)).setRotateSuggestionButtonState(
+                true /* visible */);
     }
 }
diff --git a/packages/overlays/Android.mk b/packages/overlays/Android.mk
index 2675e90..3f16c12 100644
--- a/packages/overlays/Android.mk
+++ b/packages/overlays/Android.mk
@@ -49,7 +49,10 @@
 	IconShapeTeardropOverlay \
 	NavigationBarMode3ButtonOverlay \
 	NavigationBarMode2ButtonOverlay \
-	NavigationBarModeGesturalOverlay
+	NavigationBarModeGesturalOverlay \
+	NavigationBarModeGesturalOverlayNarrowBack \
+	NavigationBarModeGesturalOverlayWideBack \
+	NavigationBarModeGesturalOverlayExtraWideBack
 
 include $(BUILD_PHONY_PACKAGE)
 include $(CLEAR_VARS)
diff --git a/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml
index 987d203..1232201 100644
--- a/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml
+++ b/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml
@@ -25,4 +25,6 @@
     <dimen name="navigation_bar_width">16dp</dimen>
     <!-- Height of the bottom navigation / system bar. -->
     <dimen name="navigation_bar_frame_height">48dp</dimen>
+        <!-- The height of the bottom navigation gesture area. -->
+    <dimen name="navigation_bar_gesture_height">32dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/Android.mk b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/Android.mk
new file mode 100644
index 0000000..9a38efa
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/Android.mk
@@ -0,0 +1,30 @@
+#
+#  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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_RRO_THEME := NavigationBarModeGesturalExtraWideBack
+
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_PACKAGE_NAME := NavigationBarModeGesturalOverlayExtraWideBack
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_RRO_PACKAGE)
\ No newline at end of file
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/AndroidManifest.xml b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/AndroidManifest.xml
new file mode 100644
index 0000000..ba7beba
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<!--
+/**
+ * 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.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.internal.systemui.navbar.gestural_extra_wide_back"
+        android:versionCode="1"
+        android:versionName="1.0">
+    <overlay android:targetPackage="android"
+        android:category="com.android.internal.navigation_bar_mode"
+        android:priority="1"/>
+
+    <application android:label="@string/navigation_bar_mode_title" android:hasCode="false"/>
+</manifest>
\ No newline at end of file
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/config.xml
new file mode 100644
index 0000000..c8f994c
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/config.xml
@@ -0,0 +1,48 @@
+<?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.
+ */
+-->
+<resources>
+    <!-- Controls the navigation bar interaction mode:
+         0: 3 button mode (back, home, overview buttons)
+         1: 2 button mode (back, home buttons + swipe up for overview)
+         2: gestures only for back, home and overview -->
+    <integer name="config_navBarInteractionMode">2</integer>
+
+    <!-- Controls whether the nav bar can move from the bottom to the side in landscape.
+         Only applies if the device display is not square. -->
+    <bool name="config_navBarCanMove">false</bool>
+
+    <!-- Controls whether the navigation bar lets through taps. -->
+    <bool name="config_navBarTapThrough">true</bool>
+
+    <!-- Controls the size of the back gesture inset. -->
+    <dimen name="config_backGestureInset">40dp</dimen>
+
+    <!-- Controls whether the navbar needs a scrim with
+     {@link Window#setEnsuringNavigationBarContrastWhenTransparent}. -->
+    <bool name="config_navBarNeedsScrim">false</bool>
+
+    <!-- Controls whether seamless rotation should be allowed even though the navbar can move
+         (which normally prevents seamless rotation). -->
+    <bool name="config_allowSeamlessRotationDespiteNavBarMoving">true</bool>
+
+    <!-- Controls whether the side edge gestures can always trigger the transient nav bar to
+         show. -->
+    <bool name="config_navBarAlwaysShowOnSideEdgeGesture">true</bool>
+
+</resources>
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/dimens.xml
new file mode 100644
index 0000000..987d203
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/dimens.xml
@@ -0,0 +1,28 @@
+<?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.
+ */
+-->
+<resources>
+    <!-- Height of the bottom navigation / system bar. -->
+    <dimen name="navigation_bar_height">16dp</dimen>
+    <!-- Height of the bottom navigation bar in portrait; often the same as @dimen/navigation_bar_height -->
+    <dimen name="navigation_bar_height_landscape">16dp</dimen>
+    <!-- Width of the navigation bar when it is placed vertically on the screen -->
+    <dimen name="navigation_bar_width">16dp</dimen>
+    <!-- Height of the bottom navigation / system bar. -->
+    <dimen name="navigation_bar_frame_height">48dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/strings.xml b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/strings.xml
new file mode 100644
index 0000000..bbab5e047
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/strings.xml
@@ -0,0 +1,22 @@
+<?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.
+ */
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Name of overlay [CHAR LIMIT=64] -->
+    <string name="navigation_bar_mode_title" translatable="false">Gestural Navigation Bar</string>
+</resources>
\ No newline at end of file
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/Android.mk b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/Android.mk
new file mode 100644
index 0000000..1d004c8
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/Android.mk
@@ -0,0 +1,30 @@
+#
+#  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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_RRO_THEME := NavigationBarModeGesturalNarrowBack
+
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_PACKAGE_NAME := NavigationBarModeGesturalOverlayNarrowBack
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_RRO_PACKAGE)
\ No newline at end of file
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/AndroidManifest.xml b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/AndroidManifest.xml
new file mode 100644
index 0000000..8de91c0
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<!--
+/**
+ * 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.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.internal.systemui.navbar.gestural_narrow_back"
+        android:versionCode="1"
+        android:versionName="1.0">
+    <overlay android:targetPackage="android"
+        android:category="com.android.internal.navigation_bar_mode"
+        android:priority="1"/>
+
+    <application android:label="@string/navigation_bar_mode_title" android:hasCode="false"/>
+</manifest>
\ No newline at end of file
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/config.xml
new file mode 100644
index 0000000..693110a
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/config.xml
@@ -0,0 +1,48 @@
+<?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.
+ */
+-->
+<resources>
+    <!-- Controls the navigation bar interaction mode:
+         0: 3 button mode (back, home, overview buttons)
+         1: 2 button mode (back, home buttons + swipe up for overview)
+         2: gestures only for back, home and overview -->
+    <integer name="config_navBarInteractionMode">2</integer>
+
+    <!-- Controls whether the nav bar can move from the bottom to the side in landscape.
+         Only applies if the device display is not square. -->
+    <bool name="config_navBarCanMove">false</bool>
+
+    <!-- Controls whether the navigation bar lets through taps. -->
+    <bool name="config_navBarTapThrough">true</bool>
+
+    <!-- Controls the size of the back gesture inset. -->
+    <dimen name="config_backGestureInset">18dp</dimen>
+
+    <!-- Controls whether the navbar needs a scrim with
+     {@link Window#setEnsuringNavigationBarContrastWhenTransparent}. -->
+    <bool name="config_navBarNeedsScrim">false</bool>
+
+    <!-- Controls whether seamless rotation should be allowed even though the navbar can move
+         (which normally prevents seamless rotation). -->
+    <bool name="config_allowSeamlessRotationDespiteNavBarMoving">true</bool>
+
+    <!-- Controls whether the side edge gestures can always trigger the transient nav bar to
+         show. -->
+    <bool name="config_navBarAlwaysShowOnSideEdgeGesture">true</bool>
+
+</resources>
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/dimens.xml
new file mode 100644
index 0000000..987d203
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/dimens.xml
@@ -0,0 +1,28 @@
+<?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.
+ */
+-->
+<resources>
+    <!-- Height of the bottom navigation / system bar. -->
+    <dimen name="navigation_bar_height">16dp</dimen>
+    <!-- Height of the bottom navigation bar in portrait; often the same as @dimen/navigation_bar_height -->
+    <dimen name="navigation_bar_height_landscape">16dp</dimen>
+    <!-- Width of the navigation bar when it is placed vertically on the screen -->
+    <dimen name="navigation_bar_width">16dp</dimen>
+    <!-- Height of the bottom navigation / system bar. -->
+    <dimen name="navigation_bar_frame_height">48dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/strings.xml b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/strings.xml
new file mode 100644
index 0000000..bbab5e047
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/strings.xml
@@ -0,0 +1,22 @@
+<?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.
+ */
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Name of overlay [CHAR LIMIT=64] -->
+    <string name="navigation_bar_mode_title" translatable="false">Gestural Navigation Bar</string>
+</resources>
\ No newline at end of file
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/Android.mk b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/Android.mk
new file mode 100644
index 0000000..0ab463f
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/Android.mk
@@ -0,0 +1,30 @@
+#
+#  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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_RRO_THEME := NavigationBarModeGesturalWideBack
+
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_PACKAGE_NAME := NavigationBarModeGesturalOverlayWideBack
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_RRO_PACKAGE)
\ No newline at end of file
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/AndroidManifest.xml b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/AndroidManifest.xml
new file mode 100644
index 0000000..daf4613
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<!--
+/**
+ * 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.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.internal.systemui.navbar.gestural_wide_back"
+        android:versionCode="1"
+        android:versionName="1.0">
+    <overlay android:targetPackage="android"
+        android:category="com.android.internal.navigation_bar_mode"
+        android:priority="1"/>
+
+    <application android:label="@string/navigation_bar_mode_title" android:hasCode="false"/>
+</manifest>
\ No newline at end of file
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/config.xml
new file mode 100644
index 0000000..5cd6ce3
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/config.xml
@@ -0,0 +1,48 @@
+<?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.
+ */
+-->
+<resources>
+    <!-- Controls the navigation bar interaction mode:
+         0: 3 button mode (back, home, overview buttons)
+         1: 2 button mode (back, home buttons + swipe up for overview)
+         2: gestures only for back, home and overview -->
+    <integer name="config_navBarInteractionMode">2</integer>
+
+    <!-- Controls whether the nav bar can move from the bottom to the side in landscape.
+         Only applies if the device display is not square. -->
+    <bool name="config_navBarCanMove">false</bool>
+
+    <!-- Controls whether the navigation bar lets through taps. -->
+    <bool name="config_navBarTapThrough">true</bool>
+
+    <!-- Controls the size of the back gesture inset. -->
+    <dimen name="config_backGestureInset">32dp</dimen>
+
+    <!-- Controls whether the navbar needs a scrim with
+     {@link Window#setEnsuringNavigationBarContrastWhenTransparent}. -->
+    <bool name="config_navBarNeedsScrim">false</bool>
+
+    <!-- Controls whether seamless rotation should be allowed even though the navbar can move
+         (which normally prevents seamless rotation). -->
+    <bool name="config_allowSeamlessRotationDespiteNavBarMoving">true</bool>
+
+    <!-- Controls whether the side edge gestures can always trigger the transient nav bar to
+         show. -->
+    <bool name="config_navBarAlwaysShowOnSideEdgeGesture">true</bool>
+
+</resources>
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/dimens.xml
new file mode 100644
index 0000000..987d203
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/dimens.xml
@@ -0,0 +1,28 @@
+<?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.
+ */
+-->
+<resources>
+    <!-- Height of the bottom navigation / system bar. -->
+    <dimen name="navigation_bar_height">16dp</dimen>
+    <!-- Height of the bottom navigation bar in portrait; often the same as @dimen/navigation_bar_height -->
+    <dimen name="navigation_bar_height_landscape">16dp</dimen>
+    <!-- Width of the navigation bar when it is placed vertically on the screen -->
+    <dimen name="navigation_bar_width">16dp</dimen>
+    <!-- Height of the bottom navigation / system bar. -->
+    <dimen name="navigation_bar_frame_height">48dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/strings.xml b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/strings.xml
new file mode 100644
index 0000000..bbab5e047
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/strings.xml
@@ -0,0 +1,22 @@
+<?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.
+ */
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Name of overlay [CHAR LIMIT=64] -->
+    <string name="navigation_bar_mode_title" translatable="false">Gestural Navigation Bar</string>
+</resources>
\ No newline at end of file
diff --git a/proto/src/metrics_constants/metrics_constants.proto b/proto/src/metrics_constants/metrics_constants.proto
index 71d03a8..0f0e6f9 100644
--- a/proto/src/metrics_constants/metrics_constants.proto
+++ b/proto/src/metrics_constants/metrics_constants.proto
@@ -7376,6 +7376,18 @@
     // OS: Q
     FIELD_BIOMETRIC_AUTH_ERROR = 1741;
 
+    // Custom tag for NotificationItem. Hash of the NAS that made adjustments.
+    FIELD_NOTIFICATION_ASSISTANT_SERVICE_HASH = 1742;
+
+    // Report interactions with seekbar on media notifications
+    // OPEN: Seekbar is visible
+    // CLOSE: Seekbar is not visible
+    // DETAIL: Seekbar scrubber enabled / disabled
+    //  Subtype: 0 disabled, cannot seek; 1 enabled, can seek
+    // UPDATE: Scrubber was moved by user
+    // CATEGORY: NOTIFICATION
+    MEDIA_NOTIFICATION_SEEKBAR = 1743;
+
     // ---- End Q Constants, all Q constants go above this line ----
     // Add new aosp constants above this line.
     // END OF AOSP CONSTANTS
diff --git a/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
index 430abf5..10ba9a5 100644
--- a/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
+++ b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
@@ -28,13 +28,17 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ParceledListSlice;
 import android.content.pm.ServiceInfo;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.service.appprediction.AppPredictionService;
+import android.util.ArrayMap;
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.infra.AbstractPerUserSystemService;
 
+import java.util.ArrayList;
+
 /**
  * Per-user instance of {@link AppPredictionManagerService}.
  */
@@ -48,6 +52,17 @@
     @GuardedBy("mLock")
     private RemoteAppPredictionService mRemoteService;
 
+    /**
+     * When {@code true}, remote service died but service state is kept so it's restored after
+     * the system re-binds to it.
+     */
+    @GuardedBy("mLock")
+    private boolean mZombie;
+
+    @GuardedBy("mLock")
+    private final ArrayMap<AppPredictionSessionId, AppPredictionSessionInfo> mSessionInfos =
+            new ArrayMap<>();
+
     protected AppPredictionPerUserService(AppPredictionManagerService master,
             Object lock, int userId) {
         super(master, lock, userId);
@@ -92,6 +107,16 @@
         final RemoteAppPredictionService service = getRemoteServiceLocked();
         if (service != null) {
             service.onCreatePredictionSession(context, sessionId);
+
+            mSessionInfos.put(sessionId, new AppPredictionSessionInfo(sessionId, context, () -> {
+                synchronized (mLock) {
+                    AppPredictionSessionInfo sessionInfo = mSessionInfos.get(sessionId);
+                    if (sessionInfo != null) {
+                        sessionInfo.removeAllCallbacksLocked();
+                        mSessionInfos.remove(sessionId);
+                    }
+                }
+            }));
         }
     }
 
@@ -140,6 +165,11 @@
         final RemoteAppPredictionService service = getRemoteServiceLocked();
         if (service != null) {
             service.registerPredictionUpdates(sessionId, callback);
+
+            AppPredictionSessionInfo sessionInfo = mSessionInfos.get(sessionId);
+            if (sessionInfo != null) {
+                sessionInfo.addCallbackLocked(callback);
+            }
         }
     }
 
@@ -152,6 +182,11 @@
         final RemoteAppPredictionService service = getRemoteServiceLocked();
         if (service != null) {
             service.unregisterPredictionUpdates(sessionId, callback);
+
+            AppPredictionSessionInfo sessionInfo = mSessionInfos.get(sessionId);
+            if (sessionInfo != null) {
+                sessionInfo.removeCallbackLocked(callback);
+            }
         }
     }
 
@@ -174,6 +209,12 @@
         final RemoteAppPredictionService service = getRemoteServiceLocked();
         if (service != null) {
             service.onDestroyPredictionSession(sessionId);
+
+            AppPredictionSessionInfo sessionInfo = mSessionInfos.get(sessionId);
+            if (sessionInfo != null) {
+                sessionInfo.removeAllCallbacksLocked();
+                mSessionInfos.remove(sessionId);
+            }
         }
     }
 
@@ -182,17 +223,54 @@
         if (isDebug()) {
             Slog.d(TAG, "onFailureOrTimeout(): timed out=" + timedOut);
         }
-
         // Do nothing, we are just proxying to the prediction service
     }
 
     @Override
+    public void onConnectedStateChanged(boolean connected) {
+        if (isDebug()) {
+            Slog.d(TAG, "onConnectedStateChanged(): connected=" + connected);
+        }
+        if (connected) {
+            synchronized (mLock) {
+                if (mZombie) {
+                    // Sanity check - shouldn't happen
+                    if (mRemoteService == null) {
+                        Slog.w(TAG, "Cannot resurrect sessions because remote service is null");
+                        return;
+                    }
+                    mZombie = false;
+                    resurrectSessionsLocked();
+                }
+            }
+        }
+    }
+
+    @Override
     public void onServiceDied(RemoteAppPredictionService service) {
         if (isDebug()) {
-            Slog.d(TAG, "onServiceDied():");
+            Slog.w(TAG, "onServiceDied(): service=" + service);
+        }
+        synchronized (mLock) {
+            mZombie = true;
+        }
+        // Do nothing, eventually the system will bind to the remote service again...
+    }
+
+    /**
+     * Called after the remote service connected, it's used to restore state from a 'zombie'
+     * service (i.e., after it died).
+     */
+    private void resurrectSessionsLocked() {
+        final int numSessions = mSessionInfos.size();
+        if (isDebug()) {
+            Slog.d(TAG, "Resurrecting remote service (" + mRemoteService + ") on "
+                    + numSessions + " sessions.");
         }
 
-        // Do nothing, we are just proxying to the prediction service
+        for (AppPredictionSessionInfo sessionInfo : mSessionInfos.values()) {
+            sessionInfo.resurrectSessionLocked(this);
+        }
     }
 
     @GuardedBy("mLock")
@@ -215,4 +293,57 @@
 
         return mRemoteService;
     }
+
+    private static final class AppPredictionSessionInfo {
+        private final AppPredictionSessionId mSessionId;
+        private final AppPredictionContext mContext;
+        private final ArrayList<IPredictionCallback> mCallbacks = new ArrayList<>();
+        private final IBinder.DeathRecipient mBinderDeathHandler;
+
+        AppPredictionSessionInfo(AppPredictionSessionId id, AppPredictionContext context,
+                IBinder.DeathRecipient binderDeathHandler) {
+            mSessionId = id;
+            mContext = context;
+            mBinderDeathHandler = binderDeathHandler;
+        }
+
+        void addCallbackLocked(IPredictionCallback callback) {
+            if (mBinderDeathHandler != null) {
+                try {
+                    callback.asBinder().linkToDeath(mBinderDeathHandler, 0);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Failed to link to death: " + e);
+                }
+            }
+            mCallbacks.add(callback);
+        }
+
+        void removeCallbackLocked(IPredictionCallback callback) {
+            if (mBinderDeathHandler != null) {
+                callback.asBinder().unlinkToDeath(mBinderDeathHandler, 0);
+            }
+            mCallbacks.remove(callback);
+        }
+
+        void removeAllCallbacksLocked() {
+            if (mBinderDeathHandler != null) {
+                for (IPredictionCallback callback : mCallbacks) {
+                    callback.asBinder().unlinkToDeath(mBinderDeathHandler, 0);
+                }
+            }
+            mCallbacks.clear();
+        }
+
+        void resurrectSessionLocked(AppPredictionPerUserService service) {
+            if (service.isDebug()) {
+                Slog.d(TAG, "Resurrecting remote service (" + service.getRemoteServiceLocked()
+                        + ") for session Id=" + mSessionId + " and "
+                        + mCallbacks.size() + " callbacks.");
+            }
+            service.onCreatePredictionSessionLocked(mContext, mSessionId);
+            for (IPredictionCallback callback : mCallbacks) {
+                service.registerPredictionUpdatesLocked(mSessionId, callback);
+            }
+        }
+    }
 }
diff --git a/services/appprediction/java/com/android/server/appprediction/RemoteAppPredictionService.java b/services/appprediction/java/com/android/server/appprediction/RemoteAppPredictionService.java
index 19226be..c82e7a0 100644
--- a/services/appprediction/java/com/android/server/appprediction/RemoteAppPredictionService.java
+++ b/services/appprediction/java/com/android/server/appprediction/RemoteAppPredictionService.java
@@ -42,6 +42,8 @@
 
     private static final long TIMEOUT_REMOTE_REQUEST_MILLIS = 2 * DateUtils.SECOND_IN_MILLIS;
 
+    private final RemoteAppPredictionServiceCallbacks mCallback;
+
     public RemoteAppPredictionService(Context context, String serviceInterface,
             ComponentName componentName, int userId,
             RemoteAppPredictionServiceCallbacks callback, boolean bindInstantServiceAllowed,
@@ -50,6 +52,7 @@
                 context.getMainThreadHandler(),
                 bindInstantServiceAllowed ? Context.BIND_ALLOW_INSTANT : 0,
                 verbose, /* initialCapacity= */ 1);
+        mCallback = callback;
     }
 
     @Override
@@ -141,5 +144,17 @@
          * Notifies a the failure or timeout of a remote call.
          */
         void onFailureOrTimeout(boolean timedOut);
+
+        /**
+         * Notifies change in connected state of the remote service.
+         */
+        void onConnectedStateChanged(boolean connected);
+    }
+
+    @Override // from AbstractRemoteService
+    protected void handleOnConnectedStateChanged(boolean connected) {
+        if (mCallback != null) {
+            mCallback.onConnectedStateChanged(connected);
+        }
     }
 }
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 66b5437..af9b0e2 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -2178,6 +2178,7 @@
     private void requestNewFillResponseOnViewEnteredIfNecessaryLocked(@NonNull AutofillId id,
             @NonNull ViewState viewState, int flags) {
         if ((flags & FLAG_MANUAL_REQUEST) != 0) {
+            mForAugmentedAutofillOnly = false;
             if (sDebug) Slog.d(TAG, "Re-starting session on view " + id + " and flags " + flags);
             requestNewFillResponseLocked(viewState, ViewState.STATE_RESTARTED_SESSION, flags);
             return;
diff --git a/services/core/java/com/android/server/AlarmManagerService.java b/services/core/java/com/android/server/AlarmManagerService.java
index dfbb55a..d0edaaa 100644
--- a/services/core/java/com/android/server/AlarmManagerService.java
+++ b/services/core/java/com/android/server/AlarmManagerService.java
@@ -1763,7 +1763,7 @@
                                 + ", callingPackage: " + callingPackage;
                 // STOPSHIP (b/128866264): Just to catch breakages. Remove before final release.
                 Slog.wtf(TAG, errorMsg);
-                throw new UnsupportedOperationException(errorMsg);
+                throw new IllegalStateException(errorMsg);
             }
             setImplLocked(type, triggerAtTime, triggerElapsed, windowLength, maxElapsed,
                     interval, operation, directReceiver, listenerTag, flags, true, workSource,
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 0a1dbff..d6b4043 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -91,6 +91,7 @@
 import android.net.NetworkInfo;
 import android.net.NetworkInfo.DetailedState;
 import android.net.NetworkMisc;
+import android.net.NetworkMonitorManager;
 import android.net.NetworkPolicyManager;
 import android.net.NetworkQuotaInfo;
 import android.net.NetworkRequest;
@@ -169,6 +170,7 @@
 import com.android.internal.util.WakeupMessage;
 import com.android.internal.util.XmlUtils;
 import com.android.server.am.BatteryStatsService;
+import com.android.server.connectivity.AutodestructReference;
 import com.android.server.connectivity.DataConnectionStats;
 import com.android.server.connectivity.DnsManager;
 import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
@@ -1785,8 +1787,7 @@
             // caller type. Need to re-factor NetdEventListenerService to allow multiple
             // NetworkMonitor registrants.
             if (nai != null && nai.satisfies(mDefaultRequest)) {
-                Binder.withCleanCallingIdentity(() ->
-                        nai.networkMonitor().notifyDnsResponse(returnCode));
+                nai.networkMonitor().notifyDnsResponse(returnCode);
             }
         }
 
@@ -2763,29 +2764,31 @@
     }
 
     private class NetworkMonitorCallbacks extends INetworkMonitorCallbacks.Stub {
-        private final NetworkAgentInfo mNai;
+        private final int mNetId;
+        private final AutodestructReference<NetworkAgentInfo> mNai;
 
         private NetworkMonitorCallbacks(NetworkAgentInfo nai) {
-            mNai = nai;
+            mNetId = nai.network.netId;
+            mNai = new AutodestructReference(nai);
         }
 
         @Override
         public void onNetworkMonitorCreated(INetworkMonitor networkMonitor) {
             mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_AGENT,
-                    new Pair<>(mNai, networkMonitor)));
+                    new Pair<>(mNai.getAndDestroy(), networkMonitor)));
         }
 
         @Override
         public void notifyNetworkTested(int testResult, @Nullable String redirectUrl) {
             mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage(EVENT_NETWORK_TESTED,
-                    testResult, mNai.network.netId, redirectUrl));
+                    testResult, mNetId, redirectUrl));
         }
 
         @Override
         public void notifyPrivateDnsConfigResolved(PrivateDnsConfigParcel config) {
             mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage(
                     EVENT_PRIVATE_DNS_CONFIG_RESOLVED,
-                    0, mNai.network.netId, PrivateDnsConfig.fromParcel(config)));
+                    0, mNetId, PrivateDnsConfig.fromParcel(config)));
         }
 
         @Override
@@ -2803,15 +2806,13 @@
             }
             mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage(
                     EVENT_PROVISIONING_NOTIFICATION, PROVISIONING_NOTIFICATION_SHOW,
-                    mNai.network.netId,
-                    pendingIntent));
+                    mNetId, pendingIntent));
         }
 
         @Override
         public void hideProvisioningNotification() {
             mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage(
-                    EVENT_PROVISIONING_NOTIFICATION, PROVISIONING_NOTIFICATION_HIDE,
-                    mNai.network.netId));
+                    EVENT_PROVISIONING_NOTIFICATION, PROVISIONING_NOTIFICATION_HIDE, mNetId));
         }
 
         @Override
@@ -2853,11 +2854,7 @@
         // Notify the NetworkAgentInfo/NetworkMonitor in case NetworkMonitor needs to cancel or
         // schedule DNS resolutions. If a DNS resolution is required the
         // result will be sent back to us.
-        try {
-            nai.networkMonitor().notifyPrivateDnsChanged(cfg.toParcel());
-        } catch (RemoteException e) {
-            e.rethrowAsRuntimeException();
-        }
+        nai.networkMonitor().notifyPrivateDnsChanged(cfg.toParcel());
 
         // With Private DNS bypass support, we can proceed to update the
         // Private DNS config immediately, even if we're in strict mode
@@ -3023,11 +3020,7 @@
             // Disable wakeup packet monitoring for each interface.
             wakeupModifyInterface(iface, nai.networkCapabilities, false);
         }
-        try {
-            nai.networkMonitor().notifyNetworkDisconnected();
-        } catch (RemoteException e) {
-            e.rethrowAsRuntimeException();
-        }
+        nai.networkMonitor().notifyNetworkDisconnected();
         mNetworkAgentInfos.remove(nai.messenger);
         nai.clatd.update();
         synchronized (mNetworkForNetId) {
@@ -3440,11 +3433,7 @@
             //
             // TODO: NetworkMonitor does not refer to the "never ask again" bit. The bit is stored
             // per network. Therefore, NetworkMonitor may still do https probe.
-            try {
-                nai.networkMonitor().setAcceptPartialConnectivity();
-            } catch (RemoteException e) {
-                e.rethrowAsRuntimeException();
-            }
+            nai.networkMonitor().setAcceptPartialConnectivity();
         }
     }
 
@@ -3476,11 +3465,7 @@
             NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
             if (nai == null) return;
             if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) return;
-            try {
-                nai.networkMonitor().launchCaptivePortalApp();
-            } catch (RemoteException e) {
-                e.rethrowAsRuntimeException();
-            }
+            nai.networkMonitor().launchCaptivePortalApp();
         });
     }
 
@@ -3515,7 +3500,7 @@
         }
 
         @Override
-        public void appResponse(final int response) throws RemoteException {
+        public void appResponse(final int response) {
             if (response == CaptivePortal.APP_RETURN_WANTED_AS_IS) {
                 enforceSettingsPermission();
             }
@@ -3525,16 +3510,9 @@
             if (nai == null) return;
 
             // nai.networkMonitor() is thread-safe
-            final INetworkMonitor nm = nai.networkMonitor();
+            final NetworkMonitorManager nm = nai.networkMonitor();
             if (nm == null) return;
-
-            final long token = Binder.clearCallingIdentity();
-            try {
-                nm.notifyCaptivePortalAppFinished(response);
-            } finally {
-                // Not using Binder.withCleanCallingIdentity() to keep the checked RemoteException
-                Binder.restoreCallingIdentity(token);
-            }
+            nm.notifyCaptivePortalAppFinished(response);
         }
 
         @Override
@@ -4105,11 +4083,7 @@
         if (isNetworkWithLinkPropertiesBlocked(lp, uid, false)) {
             return;
         }
-        try {
-            nai.networkMonitor().forceReevaluation(uid);
-        } catch (RemoteException e) {
-            e.rethrowAsRuntimeException();
-        }
+        nai.networkMonitor().forceReevaluation(uid);
     }
 
     /**
@@ -4363,7 +4337,7 @@
 
     /**
      * @return VPN information for accounting, or null if we can't retrieve all required
-     *         information, e.g primary underlying iface.
+     *         information, e.g underlying ifaces.
      */
     @Nullable
     private VpnInfo createVpnInfo(Vpn vpn) {
@@ -4375,17 +4349,24 @@
         // see VpnService.setUnderlyingNetworks()'s javadoc about how to interpret
         // the underlyingNetworks list.
         if (underlyingNetworks == null) {
-            NetworkAgentInfo defaultNetwork = getDefaultNetwork();
-            if (defaultNetwork != null && defaultNetwork.linkProperties != null) {
-                info.primaryUnderlyingIface = getDefaultNetwork().linkProperties.getInterfaceName();
-            }
-        } else if (underlyingNetworks.length > 0) {
-            LinkProperties linkProperties = getLinkProperties(underlyingNetworks[0]);
-            if (linkProperties != null) {
-                info.primaryUnderlyingIface = linkProperties.getInterfaceName();
+            NetworkAgentInfo defaultNai = getDefaultNetwork();
+            if (defaultNai != null) {
+                underlyingNetworks = new Network[] { defaultNai.network };
             }
         }
-        return info.primaryUnderlyingIface == null ? null : info;
+        if (underlyingNetworks != null && underlyingNetworks.length > 0) {
+            List<String> interfaces = new ArrayList<>();
+            for (Network network : underlyingNetworks) {
+                LinkProperties lp = getLinkProperties(network);
+                if (lp != null && !TextUtils.isEmpty(lp.getInterfaceName())) {
+                    interfaces.add(lp.getInterfaceName());
+                }
+            }
+            if (!interfaces.isEmpty()) {
+                info.underlyingIfaces = interfaces.toArray(new String[interfaces.size()]);
+            }
+        }
+        return info.underlyingIfaces == null ? null : info;
     }
 
     /**
@@ -5535,11 +5516,7 @@
             // Start or stop DNS64 detection and 464xlat according to network state.
             networkAgent.clatd.update();
             notifyIfacesChangedForNetworkStats();
-            try {
-                networkAgent.networkMonitor().notifyLinkPropertiesChanged(newLp);
-            } catch (RemoteException e) {
-                e.rethrowAsRuntimeException();
-            }
+            networkAgent.networkMonitor().notifyLinkPropertiesChanged(newLp);
             if (networkAgent.everConnected) {
                 notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_IP_CHANGED);
             }
@@ -6523,15 +6500,11 @@
             // command must be sent after updating LinkProperties to maximize chances of
             // NetworkMonitor seeing the correct LinkProperties when starting.
             // TODO: pass LinkProperties to the NetworkMonitor in the notifyNetworkConnected call.
-            try {
-                if (networkAgent.networkMisc.acceptPartialConnectivity) {
-                    networkAgent.networkMonitor().setAcceptPartialConnectivity();
-                }
-                networkAgent.networkMonitor().notifyNetworkConnected(
-                        networkAgent.linkProperties, networkAgent.networkCapabilities);
-            } catch (RemoteException e) {
-                e.rethrowAsRuntimeException();
+            if (networkAgent.networkMisc.acceptPartialConnectivity) {
+                networkAgent.networkMonitor().setAcceptPartialConnectivity();
             }
+            networkAgent.networkMonitor().notifyNetworkConnected(
+                    networkAgent.linkProperties, networkAgent.networkCapabilities);
             scheduleUnvalidatedPrompt(networkAgent);
 
             // Whether a particular NetworkRequest listen should cause signal strength thresholds to
diff --git a/services/core/java/com/android/server/LocationManagerService.java b/services/core/java/com/android/server/LocationManagerService.java
index d52fe81..ae04f76 100644
--- a/services/core/java/com/android/server/LocationManagerService.java
+++ b/services/core/java/com/android/server/LocationManagerService.java
@@ -82,6 +82,7 @@
 import android.os.WorkSource;
 import android.os.WorkSource.WorkChain;
 import android.provider.Settings;
+import android.stats.location.LocationStatsEnums;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -269,10 +270,14 @@
     @PowerManager.LocationPowerSaveMode
     private int mBatterySaverMode;
 
+    @GuardedBy("mLock")
+    private final LocationUsageLogger mLocationUsageLogger;
+
     public LocationManagerService(Context context) {
         super();
         mContext = context;
         mHandler = FgThread.getHandler();
+        mLocationUsageLogger = new LocationUsageLogger();
 
         // Let the package manager query which are the default location
         // providers as they get certain permissions granted by default.
@@ -2346,7 +2351,18 @@
          * Method to be called when a record will no longer be used.
          */
         private void disposeLocked(boolean removeReceiver) {
-            mRequestStatistics.stopRequesting(mReceiver.mCallerIdentity.mPackageName, mProvider);
+            String packageName = mReceiver.mCallerIdentity.mPackageName;
+            mRequestStatistics.stopRequesting(packageName, mProvider);
+
+            mLocationUsageLogger.logLocationApiUsage(
+                    LocationStatsEnums.USAGE_ENDED,
+                    LocationStatsEnums.API_REQUEST_LOCATION_UPDATES,
+                    packageName,
+                    mRealRequest,
+                    mReceiver.isListener(),
+                    mReceiver.isPendingIntent(),
+                    /* radius= */ 0,
+                    mActivityManager.getPackageImportance(packageName));
 
             // remove from mRecordsByProvider
             ArrayList<UpdateRecord> globalRecords = mRecordsByProvider.get(this.mProvider);
@@ -2521,6 +2537,13 @@
                             "cannot register both listener and intent");
                 }
 
+                mLocationUsageLogger.logLocationApiUsage(
+                        LocationStatsEnums.USAGE_STARTED,
+                        LocationStatsEnums.API_REQUEST_LOCATION_UPDATES,
+                        packageName, request, listener != null, intent != null,
+                        /* radius= */ 0,
+                        mActivityManager.getPackageImportance(packageName));
+
                 Receiver receiver;
                 if (intent != null) {
                     receiver = getReceiverLocked(intent, pid, uid, packageName, workSource,
@@ -2813,6 +2836,18 @@
         }
         long identity = Binder.clearCallingIdentity();
         try {
+            synchronized (mLock) {
+                mLocationUsageLogger.logLocationApiUsage(
+                        LocationStatsEnums.USAGE_STARTED,
+                        LocationStatsEnums.API_REQUEST_GEOFENCE,
+                        packageName,
+                        request,
+                        /* hasListener= */ false,
+                        intent != null,
+                        geofence.getRadius(),
+                        mActivityManager.getPackageImportance(packageName));
+            }
+
             mGeofenceManager.addFence(sanitizedRequest, geofence, intent,
                     allowedResolutionLevel,
                     uid, packageName);
@@ -2833,6 +2868,17 @@
         // geo-fence manager uses the public location API, need to clear identity
         long identity = Binder.clearCallingIdentity();
         try {
+            synchronized (mLock) {
+                mLocationUsageLogger.logLocationApiUsage(
+                        LocationStatsEnums.USAGE_ENDED,
+                        LocationStatsEnums.API_REQUEST_GEOFENCE,
+                        packageName,
+                        /* LocationRequest= */ null,
+                        /* hasListener= */ false,
+                        intent != null,
+                        geofence.getRadius(),
+                        mActivityManager.getPackageImportance(packageName));
+            }
             mGeofenceManager.removeFence(geofence, intent);
         } finally {
             Binder.restoreCallingIdentity(identity);
@@ -2916,6 +2962,20 @@
             gnssDataListeners.put(binder, linkedListener);
             long identity = Binder.clearCallingIdentity();
             try {
+                if (gnssDataProvider == mGnssNavigationMessageProvider
+                        || gnssDataProvider == mGnssStatusProvider) {
+                    mLocationUsageLogger.logLocationApiUsage(
+                            LocationStatsEnums.USAGE_STARTED,
+                            gnssDataProvider == mGnssNavigationMessageProvider
+                                ? LocationStatsEnums.API_ADD_GNSS_MEASUREMENTS_LISTENER
+                                : LocationStatsEnums.API_REGISTER_GNSS_STATUS_CALLBACK,
+                            packageName,
+                            /* LocationRequest= */ null,
+                            /* hasListener= */ true,
+                            /* hasIntent= */ false,
+                            /* radius */ 0,
+                            mActivityManager.getPackageImportance(packageName));
+                }
                 if (isThrottlingExemptLocked(callerIdentity)
                         || isImportanceForeground(
                         mActivityManager.getPackageImportance(packageName))) {
@@ -2941,6 +3001,26 @@
             if (linkedListener == null) {
                 return;
             }
+            long identity = Binder.clearCallingIdentity();
+            try {
+                if (gnssDataProvider == mGnssNavigationMessageProvider
+                        || gnssDataProvider == mGnssStatusProvider) {
+                    mLocationUsageLogger.logLocationApiUsage(
+                            LocationStatsEnums.USAGE_ENDED,
+                            gnssDataProvider == mGnssNavigationMessageProvider
+                                ? LocationStatsEnums.API_ADD_GNSS_MEASUREMENTS_LISTENER
+                                : LocationStatsEnums.API_REGISTER_GNSS_STATUS_CALLBACK,
+                            linkedListener.mCallerIdentity.mPackageName,
+                            /* LocationRequest= */ null,
+                            /* hasListener= */ true,
+                            /* hasIntent= */ false,
+                            /* radius= */ 0,
+                            mActivityManager.getPackageImportance(
+                                    linkedListener.mCallerIdentity.mPackageName));
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
             unlinkFromListenerDeathNotificationLocked(binder, linkedListener);
             gnssDataProvider.removeListener(listener);
         }
@@ -3026,6 +3106,11 @@
             checkResolutionLevelIsSufficientForProviderUseLocked(getCallerAllowedResolutionLevel(),
                     providerName);
 
+            mLocationUsageLogger.logLocationApiUsage(
+                    LocationStatsEnums.USAGE_STARTED,
+                    LocationStatsEnums.API_SEND_EXTRA_COMMAND,
+                    providerName);
+
             // and check for ACCESS_LOCATION_EXTRA_COMMANDS
             if ((mContext.checkCallingOrSelfPermission(ACCESS_LOCATION_EXTRA_COMMANDS)
                     != PERMISSION_GRANTED)) {
@@ -3037,6 +3122,11 @@
                 provider.sendExtraCommandLocked(command, extras);
             }
 
+            mLocationUsageLogger.logLocationApiUsage(
+                    LocationStatsEnums.USAGE_ENDED,
+                    LocationStatsEnums.API_SEND_EXTRA_COMMAND,
+                    providerName);
+
             return true;
         }
     }
diff --git a/services/core/java/com/android/server/LocationUsageLogger.java b/services/core/java/com/android/server/LocationUsageLogger.java
new file mode 100644
index 0000000..c503035
--- /dev/null
+++ b/services/core/java/com/android/server/LocationUsageLogger.java
@@ -0,0 +1,265 @@
+/*
+ * 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.server;
+
+import android.app.ActivityManager;
+import android.location.LocationManager;
+import android.location.LocationRequest;
+import android.os.SystemClock;
+import android.stats.location.LocationStatsEnums;
+import android.util.Log;
+import android.util.StatsLog;
+
+import java.time.Instant;
+
+/**
+ * Logger for Location API usage logging.
+ */
+class LocationUsageLogger {
+    private static final String TAG = "LocationUsageLogger";
+    private static final boolean D = Log.isLoggable(TAG, Log.DEBUG);
+
+    private static final int ONE_SEC_IN_MILLIS = 1000;
+    private static final int ONE_MINUTE_IN_MILLIS = 60000;
+    private static final int ONE_HOUR_IN_MILLIS = 3600000;
+
+    private long mLastApiUsageLogHour = 0;
+
+    private int mApiUsageLogHourlyCount = 0;
+
+    private static final int API_USAGE_LOG_HOURLY_CAP = 60;
+
+    private static int providerNameToStatsdEnum(String provider) {
+        if (LocationManager.NETWORK_PROVIDER.equals(provider)) {
+            return LocationStatsEnums.PROVIDER_NETWORK;
+        } else if (LocationManager.GPS_PROVIDER.equals(provider)) {
+            return LocationStatsEnums.PROVIDER_GPS;
+        } else if (LocationManager.PASSIVE_PROVIDER.equals(provider)) {
+            return LocationStatsEnums.PROVIDER_PASSIVE;
+        } else if (LocationManager.FUSED_PROVIDER.equals(provider)) {
+            return LocationStatsEnums.PROVIDER_FUSED;
+        } else {
+            return LocationStatsEnums.PROVIDER_UNKNOWN;
+        }
+    }
+
+    private static int bucketizeIntervalToStatsdEnum(long interval) {
+        // LocationManager already converts negative values to 0.
+        if (interval < ONE_SEC_IN_MILLIS) {
+            return LocationStatsEnums.INTERVAL_BETWEEN_0_SEC_AND_1_SEC;
+        } else if (interval < ONE_SEC_IN_MILLIS * 5) {
+            return LocationStatsEnums.INTERVAL_BETWEEN_1_SEC_AND_5_SEC;
+        } else if (interval < ONE_MINUTE_IN_MILLIS) {
+            return LocationStatsEnums.INTERVAL_BETWEEN_5_SEC_AND_1_MIN;
+        } else if (interval < ONE_MINUTE_IN_MILLIS * 10) {
+            return LocationStatsEnums.INTERVAL_BETWEEN_1_MIN_AND_10_MIN;
+        } else if (interval < ONE_HOUR_IN_MILLIS) {
+            return LocationStatsEnums.INTERVAL_BETWEEN_10_MIN_AND_1_HOUR;
+        } else {
+            return LocationStatsEnums.INTERVAL_LARGER_THAN_1_HOUR;
+        }
+    }
+
+    private static int bucketizeSmallestDisplacementToStatsdEnum(float smallestDisplacement) {
+        // LocationManager already converts negative values to 0.
+        if (smallestDisplacement == 0) {
+            return LocationStatsEnums.DISTANCE_ZERO;
+        } else if (smallestDisplacement > 0 && smallestDisplacement <= 100) {
+            return LocationStatsEnums.DISTANCE_BETWEEN_0_AND_100;
+        } else {
+            return LocationStatsEnums.DISTANCE_LARGER_THAN_100;
+        }
+    }
+
+    private static int bucketizeRadiusToStatsdEnum(float radius) {
+        if (radius < 0) {
+            return LocationStatsEnums.RADIUS_NEGATIVE;
+        } else if (radius < 100) {
+            return LocationStatsEnums.RADIUS_BETWEEN_0_AND_100;
+        } else if (radius < 200) {
+            return LocationStatsEnums.RADIUS_BETWEEN_100_AND_200;
+        } else if (radius < 300) {
+            return LocationStatsEnums.RADIUS_BETWEEN_200_AND_300;
+        } else if (radius < 1000) {
+            return LocationStatsEnums.RADIUS_BETWEEN_300_AND_1000;
+        } else if (radius < 10000) {
+            return LocationStatsEnums.RADIUS_BETWEEN_1000_AND_10000;
+        } else {
+            return LocationStatsEnums.RADIUS_LARGER_THAN_100000;
+        }
+    }
+
+    private static int getBucketizedExpireIn(long expireAt) {
+        if (expireAt == Long.MAX_VALUE) {
+            return LocationStatsEnums.EXPIRATION_NO_EXPIRY;
+        }
+
+        long elapsedRealtime = SystemClock.elapsedRealtime();
+        long expireIn = Math.max(0, expireAt - elapsedRealtime);
+
+        if (expireIn < 20 * ONE_SEC_IN_MILLIS) {
+            return LocationStatsEnums.EXPIRATION_BETWEEN_0_AND_20_SEC;
+        } else if (expireIn < ONE_MINUTE_IN_MILLIS) {
+            return LocationStatsEnums.EXPIRATION_BETWEEN_20_SEC_AND_1_MIN;
+        } else if (expireIn < ONE_MINUTE_IN_MILLIS * 10) {
+            return LocationStatsEnums.EXPIRATION_BETWEEN_1_MIN_AND_10_MIN;
+        } else if (expireIn < ONE_HOUR_IN_MILLIS) {
+            return LocationStatsEnums.EXPIRATION_BETWEEN_10_MIN_AND_1_HOUR;
+        } else {
+            return LocationStatsEnums.EXPIRATION_LARGER_THAN_1_HOUR;
+        }
+    }
+
+    private static int categorizeActivityImportance(int importance) {
+        if (importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
+            return LocationStatsEnums.IMPORTANCE_TOP;
+        } else if (importance == ActivityManager
+                                    .RunningAppProcessInfo
+                                    .IMPORTANCE_FOREGROUND_SERVICE) {
+            return LocationStatsEnums.IMPORTANCE_FORGROUND_SERVICE;
+        } else {
+            return LocationStatsEnums.IMPORTANCE_BACKGROUND;
+        }
+    }
+
+    private static int getCallbackType(
+            int apiType, boolean hasListener, boolean hasIntent) {
+        if (apiType == LocationStatsEnums.API_SEND_EXTRA_COMMAND) {
+            return LocationStatsEnums.CALLBACK_NOT_APPLICABLE;
+        }
+
+        // Listener and PendingIntent will not be set at
+        // the same time.
+        if (hasIntent) {
+            return LocationStatsEnums.CALLBACK_PENDING_INTENT;
+        } else if (hasListener) {
+            return LocationStatsEnums.CALLBACK_LISTENER;
+        } else {
+            return LocationStatsEnums.CALLBACK_UNKNOWN;
+        }
+    }
+
+    // Update the hourly count of APIUsage log event.
+    // Returns false if hit the hourly log cap.
+    private boolean checkApiUsageLogCap() {
+        if (D) {
+            Log.d(TAG, "checking APIUsage log cap.");
+        }
+
+        long currentHour = Instant.now().toEpochMilli() / ONE_HOUR_IN_MILLIS;
+        if (currentHour > mLastApiUsageLogHour) {
+            mLastApiUsageLogHour = currentHour;
+            mApiUsageLogHourlyCount = 0;
+            return true;
+        } else {
+            mApiUsageLogHourlyCount = Math.min(
+                mApiUsageLogHourlyCount + 1, API_USAGE_LOG_HOURLY_CAP);
+            return mApiUsageLogHourlyCount < API_USAGE_LOG_HOURLY_CAP;
+        }
+    }
+
+    /**
+     * Log a Location API usage event to Statsd.
+     * Logging event is capped at 60 per hour. Usage events exceeding
+     * the cap will be dropped by LocationUsageLogger.
+     */
+    public void logLocationApiUsage(int usageType, int apiInUse,
+            String packageName, LocationRequest locationRequest,
+            boolean hasListener, boolean hasIntent,
+            float radius, int activityImportance) {
+        try {
+            if (!checkApiUsageLogCap()) {
+                return;
+            }
+
+            boolean isLocationRequestNull = locationRequest == null;
+            if (D) {
+                Log.d(TAG, "log API Usage to statsd. usageType: " + usageType + ", apiInUse: "
+                        + apiInUse + ", packageName: " + (packageName == null ? "" : packageName)
+                        + ", locationRequest: "
+                        + (isLocationRequestNull ? "" : locationRequest.toString())
+                        + ", hasListener: " + hasListener
+                        + ", hasIntent: " + hasIntent
+                        + ", radius: " + radius
+                        + ", importance: " + activityImportance);
+            }
+
+            StatsLog.write(StatsLog.LOCATION_MANAGER_API_USAGE_REPORTED, usageType,
+                    apiInUse, packageName,
+                    isLocationRequestNull
+                        ? LocationStatsEnums.PROVIDER_UNKNOWN
+                        : providerNameToStatsdEnum(locationRequest.getProvider()),
+                    isLocationRequestNull
+                        ? LocationStatsEnums.QUALITY_UNKNOWN
+                        : locationRequest.getQuality(),
+                    isLocationRequestNull
+                        ? LocationStatsEnums.INTERVAL_UNKNOWN
+                        : bucketizeIntervalToStatsdEnum(locationRequest.getInterval()),
+                    isLocationRequestNull
+                        ? LocationStatsEnums.DISTANCE_UNKNOWN
+                        : bucketizeSmallestDisplacementToStatsdEnum(
+                                locationRequest.getSmallestDisplacement()),
+                    isLocationRequestNull ? 0 : locationRequest.getNumUpdates(),
+                    // only log expireIn for USAGE_STARTED
+                    isLocationRequestNull || usageType == LocationStatsEnums.USAGE_ENDED
+                        ? LocationStatsEnums.EXPIRATION_UNKNOWN
+                        : getBucketizedExpireIn(locationRequest.getExpireAt()),
+                    getCallbackType(apiInUse, hasListener, hasIntent),
+                    bucketizeRadiusToStatsdEnum(radius),
+                    categorizeActivityImportance(activityImportance));
+        } catch (Exception e) {
+            // Swallow exceptions to avoid crashing LMS.
+            Log.w(TAG, "Failed to log API usage to statsd.", e);
+        }
+    }
+
+    /**
+     * Log a Location API usage event to Statsd.
+     * Logging event is capped at 60 per hour. Usage events exceeding
+     * the cap will be dropped by LocationUsageLogger.
+     */
+    public void logLocationApiUsage(int usageType, int apiInUse, String providerName) {
+        try {
+            if (!checkApiUsageLogCap()) {
+                return;
+            }
+
+            if (D) {
+                Log.d(TAG, "log API Usage to statsd. usageType: " + usageType + ", apiInUse: "
+                        + apiInUse + ", providerName: " + providerName);
+            }
+
+            StatsLog.write(StatsLog.LOCATION_MANAGER_API_USAGE_REPORTED, usageType, apiInUse,
+                    /* package_name= */ null,
+                    providerNameToStatsdEnum(providerName),
+                    LocationStatsEnums.QUALITY_UNKNOWN,
+                    LocationStatsEnums.INTERVAL_UNKNOWN,
+                    LocationStatsEnums.DISTANCE_UNKNOWN,
+                    /* numUpdates= */ 0,
+                    LocationStatsEnums.EXPIRATION_UNKNOWN,
+                    getCallbackType(
+                            apiInUse,
+                            /* isListenerNull= */ true,
+                            /* isIntentNull= */ true),
+                    /* bucketizedRadius= */ 0,
+                    LocationStatsEnums.IMPORTANCE_UNKNOWN);
+        } catch (Exception e) {
+            // Swallow exceptions to avoid crashing LMS.
+            Log.w(TAG, "Failed to log API usage to statsd.", e);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java
index 3267114..dee89e5 100644
--- a/services/core/java/com/android/server/PackageWatchdog.java
+++ b/services/core/java/com/android/server/PackageWatchdog.java
@@ -25,6 +25,7 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.pm.VersionedPackage;
+import android.net.NetworkStackClient;
 import android.os.Environment;
 import android.os.Handler;
 import android.os.Looper;
@@ -115,6 +116,7 @@
     // File containing the XML data of monitored packages /data/system/package-watchdog.xml
     private final AtomicFile mPolicyFile;
     private final ExplicitHealthCheckController mHealthCheckController;
+    private final NetworkStackClient mNetworkStackClient;
     @GuardedBy("mLock")
     private boolean mIsPackagesReady;
     // Flag to control whether explicit health checks are supported or not
@@ -135,7 +137,8 @@
                         new File(new File(Environment.getDataDirectory(), "system"),
                                 "package-watchdog.xml")),
                 new Handler(Looper.myLooper()), BackgroundThread.getHandler(),
-                new ExplicitHealthCheckController(context));
+                new ExplicitHealthCheckController(context),
+                NetworkStackClient.getInstance());
     }
 
     /**
@@ -143,12 +146,14 @@
      */
     @VisibleForTesting
     PackageWatchdog(Context context, AtomicFile policyFile, Handler shortTaskHandler,
-            Handler longTaskHandler, ExplicitHealthCheckController controller) {
+            Handler longTaskHandler, ExplicitHealthCheckController controller,
+            NetworkStackClient networkStackClient) {
         mContext = context;
         mPolicyFile = policyFile;
         mShortTaskHandler = shortTaskHandler;
         mLongTaskHandler = longTaskHandler;
         mHealthCheckController = controller;
+        mNetworkStackClient = networkStackClient;
         loadFromFile();
     }
 
@@ -174,6 +179,7 @@
                     () -> syncRequestsAsync());
             setPropertyChangedListenerLocked();
             updateConfigs();
+            registerNetworkStackHealthListener();
         }
     }
 
@@ -630,29 +636,40 @@
             synchronized (mLock) {
                 PackageHealthObserver registeredObserver = observer.mRegisteredObserver;
                 if (registeredObserver != null) {
-                    PackageManager pm = mContext.getPackageManager();
                     Iterator<MonitoredPackage> it = failedPackages.iterator();
                     while (it.hasNext()) {
                         String failedPackage = it.next().getName();
-                        long versionCode = 0;
                         Slog.i(TAG, "Explicit health check failed for package " + failedPackage);
-                        try {
-                            versionCode = pm.getPackageInfo(
-                                    failedPackage, 0 /* flags */).getLongVersionCode();
-                        } catch (PackageManager.NameNotFoundException e) {
+                        VersionedPackage versionedPkg = getVersionedPackage(failedPackage);
+                        if (versionedPkg == null) {
                             Slog.w(TAG, "Explicit health check failed but could not find package "
                                     + failedPackage);
                             // TODO(b/120598832): Skip. We only continue to pass tests for now since
                             // the tests don't install any packages
+                            versionedPkg = new VersionedPackage(failedPackage, 0L);
                         }
-                        registeredObserver.execute(
-                                new VersionedPackage(failedPackage, versionCode));
+                        registeredObserver.execute(versionedPkg);
                     }
                 }
             }
         });
     }
 
+    @Nullable
+    private VersionedPackage getVersionedPackage(String packageName) {
+        final PackageManager pm = mContext.getPackageManager();
+        if (pm == null) {
+            return null;
+        }
+        try {
+            final long versionCode = pm.getPackageInfo(
+                    packageName, 0 /* flags */).getLongVersionCode();
+            return new VersionedPackage(packageName, versionCode);
+        } catch (PackageManager.NameNotFoundException e) {
+            return null;
+        }
+    }
+
     /**
      * Loads mAllObservers from file.
      *
@@ -726,6 +743,27 @@
         }
     }
 
+    private void registerNetworkStackHealthListener() {
+        // TODO: have an internal method to trigger a rollback by reporting high severity errors,
+        // and rely on ActivityManager to inform the watchdog of severe network stack crashes
+        // instead of having this listener in parallel.
+        mNetworkStackClient.registerHealthListener(
+                packageName -> {
+                    final VersionedPackage pkg = getVersionedPackage(packageName);
+                    if (pkg == null) {
+                        Slog.wtf(TAG, "NetworkStack failed but could not find its package");
+                        return;
+                    }
+                    // This is a severe failure and recovery should be attempted immediately.
+                    // TODO: have a better way to handle such failures.
+                    final List<VersionedPackage> pkgList = Collections.singletonList(pkg);
+                    final long failureCount = getTriggerFailureCount();
+                    for (int i = 0; i < failureCount; i++) {
+                        onPackageFailure(pkgList);
+                    }
+                });
+    }
+
     /**
      * Persists mAllObservers to file. Threshold information is ignored.
      */
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index bbbec66..d2b992b 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -2842,8 +2842,9 @@
         }
     }
 
+    /** Not thread safe */
     class AppFuseMountScope extends AppFuseBridge.MountScope {
-        boolean opened = false;
+        private boolean mMounted = false;
 
         public AppFuseMountScope(int uid, int mountId) {
             super(uid, mountId);
@@ -2852,8 +2853,9 @@
         @Override
         public ParcelFileDescriptor open() throws NativeDaemonConnectorException {
             try {
-                return new ParcelFileDescriptor(
-                        mVold.mountAppFuse(uid, mountId));
+                final FileDescriptor fd = mVold.mountAppFuse(uid, mountId);
+                mMounted = true;
+                return new ParcelFileDescriptor(fd);
             } catch (Exception e) {
                 throw new NativeDaemonConnectorException("Failed to mount", e);
             }
@@ -2872,9 +2874,9 @@
 
         @Override
         public void close() throws Exception {
-            if (opened) {
+            if (mMounted) {
                 mVold.unmountAppFuse(uid, mountId);
-                opened = false;
+                mMounted = false;
             }
         }
     }
@@ -3582,6 +3584,10 @@
             }
 
             final String[] packagesForUid = mIPackageManager.getPackagesForUid(uid);
+            if (ArrayUtils.isEmpty(packagesForUid)) {
+                // It's possible the package got uninstalled already, so just ignore.
+                return Zygote.MOUNT_EXTERNAL_NONE;
+            }
             if (packageName == null) {
                 packageName = packagesForUid[0];
             }
diff --git a/services/core/java/com/android/server/VibratorService.java b/services/core/java/com/android/server/VibratorService.java
index 64bcaa0..7a947f1 100644
--- a/services/core/java/com/android/server/VibratorService.java
+++ b/services/core/java/com/android/server/VibratorService.java
@@ -54,6 +54,7 @@
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.os.WorkSource;
+import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.provider.Settings.SettingNotFoundException;
 import android.util.DebugUtils;
@@ -78,6 +79,7 @@
     private static final boolean DEBUG = false;
     private static final String SYSTEM_UI_PACKAGE = "com.android.systemui";
     private static final String EXTERNAL_VIBRATOR_SERVICE = "external_vibrator_service";
+    private static final String RAMPING_RINGER_ENABLED = "ramping_ringer_enabled";
 
     private static final long[] DOUBLE_CLICK_EFFECT_FALLBACK_TIMINGS = { 0, 30, 100, 30 };
 
@@ -874,6 +876,11 @@
         if (Settings.System.getInt(
                 mContext.getContentResolver(), Settings.System.VIBRATE_WHEN_RINGING, 0) != 0) {
             return ringerMode != AudioManager.RINGER_MODE_SILENT;
+        } else if (Settings.Global.getInt(
+                    mContext.getContentResolver(), Settings.Global.APPLY_RAMPING_RINGER, 0) != 0
+                && DeviceConfig.getBoolean(
+                    DeviceConfig.NAMESPACE_TELEPHONY, RAMPING_RINGER_ENABLED, false)) {
+            return ringerMode != AudioManager.RINGER_MODE_SILENT;
         } else {
             return ringerMode == AudioManager.RINGER_MODE_VIBRATE;
         }
diff --git a/services/core/java/com/android/server/adb/AdbDebuggingManager.java b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
index bdbff3d..4b48ef9 100644
--- a/services/core/java/com/android/server/adb/AdbDebuggingManager.java
+++ b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
@@ -661,12 +661,6 @@
         return mTestUserKeyFile == null ? getAdbFile(ADB_KEYS_FILE) : mTestUserKeyFile;
     }
 
-    private void createKeyFile(File keyFile) throws IOException {
-        keyFile.createNewFile();
-        FileUtils.setPermissions(keyFile.toString(),
-                FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP, -1, -1);
-    }
-
     private void writeKey(String key) {
         try {
             File keyFile = getUserKeyFile();
@@ -675,14 +669,13 @@
                 return;
             }
 
-            if (!keyFile.exists()) {
-                createKeyFile(keyFile);
-            }
-
             FileOutputStream fo = new FileOutputStream(keyFile, true);
             fo.write(key.getBytes());
             fo.write('\n');
             fo.close();
+
+            FileUtils.setPermissions(keyFile.toString(),
+                    FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP, -1, -1);
         } catch (IOException ex) {
             Slog.e(TAG, "Error writing key:" + ex);
         }
@@ -698,10 +691,6 @@
                 return;
             }
 
-            if (!keyFile.exists()) {
-                createKeyFile(keyFile);
-            }
-
             atomicKeyFile = new AtomicFile(keyFile);
             fo = atomicKeyFile.startWrite();
             for (String key : keys) {
@@ -709,6 +698,9 @@
                 fo.write('\n');
             }
             atomicKeyFile.finishWrite(fo);
+
+            FileUtils.setPermissions(keyFile.toString(),
+                    FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP, -1, -1);
         } catch (IOException ex) {
             Slog.e(TAG, "Error writing keys: " + ex);
             if (atomicKeyFile != null) {
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 90266f1..fae853c 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -3250,6 +3250,7 @@
             int memFactor = mAm.mProcessStats.getMemFactorLocked();
             long now = SystemClock.uptimeMillis();
             r.tracker.setExecuting(false, memFactor, now);
+            r.tracker.setForeground(false, memFactor, now);
             r.tracker.setBound(false, memFactor, now);
             r.tracker.setStarted(false, memFactor, now);
         }
@@ -3293,8 +3294,10 @@
             }
             r.executeFg = false;
             if (r.tracker != null) {
-                r.tracker.setExecuting(false, mAm.mProcessStats.getMemFactorLocked(),
-                        SystemClock.uptimeMillis());
+                final int memFactor = mAm.mProcessStats.getMemFactorLocked();
+                final long now = SystemClock.uptimeMillis();
+                r.tracker.setExecuting(false, memFactor, now);
+                r.tracker.setForeground(false, memFactor, now);
                 if (finishing) {
                     r.tracker.clearCurrentOwner(r, false);
                     r.tracker = null;
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 0619eb9..2321031 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -18156,13 +18156,20 @@
             }
         }
 
+        // The arguments here are untyped because the base ActivityManagerInternal class
+        // doesn't have compile-time visiblity into ActivityServiceConnectionHolder or
+        // ConnectionRecord.
         @Override
-        public void disconnectActivityFromServices(Object connectionHolder) {
+        public void disconnectActivityFromServices(Object connectionHolder, Object conns) {
+            // 'connectionHolder' is an untyped ActivityServiceConnectionsHolder
+            // 'conns' is an untyped HashSet<ConnectionRecord>
+            final ActivityServiceConnectionsHolder holder =
+                    (ActivityServiceConnectionsHolder) connectionHolder;
+            final HashSet<ConnectionRecord> toDisconnect = (HashSet<ConnectionRecord>) conns;
             synchronized(ActivityManagerService.this) {
-                final ActivityServiceConnectionsHolder c =
-                        (ActivityServiceConnectionsHolder) connectionHolder;
-                c.forEachConnection(cr -> mServices.removeConnectionLocked(
-                        (ConnectionRecord) cr, null, c));
+                for (ConnectionRecord cr : toDisconnect) {
+                    mServices.removeConnectionLocked(cr, null, holder);
+                }
             }
         }
 
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index a0522e3..884ecba 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -241,15 +241,6 @@
         sendLMsgNoDelay(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT, SENDMSG_QUEUE, info);
     }
 
-    /*package*/ void postBluetoothA2dpDeviceConfigChangeExt(
-            @NonNull BluetoothDevice device,
-            @AudioService.BtProfileConnectionState int state, int profile,
-            boolean suppressNoisyIntent, int a2dpVolume) {
-        final BtDeviceConnectionInfo info = new BtDeviceConnectionInfo(device, state, profile,
-                suppressNoisyIntent, a2dpVolume);
-        sendLMsgNoDelay(MSG_L_A2DP_ACTIVE_DEVICE_CHANGE_EXT, SENDMSG_QUEUE, info);
-    }
-
     private static final class HearingAidDeviceConnectionInfo {
         final @NonNull BluetoothDevice mDevice;
         final @AudioService.BtProfileConnectionState int mState;
@@ -578,9 +569,8 @@
         return mBrokerHandler.hasMessages(MSG_IL_BTA2DP_DOCK_TIMEOUT);
     }
 
-    //###
     // must be called synchronized on mConnectedDevices
-    /*package*/  boolean hasScheduledA2dpSinkConnectionState(BluetoothDevice btDevice) {
+    /*package*/ boolean hasScheduledA2dpSinkConnectionState(BluetoothDevice btDevice) {
         return mBrokerHandler.hasMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE,
                 new BtHelper.BluetoothA2dpDeviceInfo(btDevice));
     }
@@ -862,22 +852,6 @@
                                 info.mDevice, info.mState, info.mSupprNoisy, info.mMusicDevice);
                     }
                 } break;
-                case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE_EXT: {
-                    final BtDeviceConnectionInfo info = (BtDeviceConnectionInfo) msg.obj;
-                    AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
-                            "handleBluetoothA2dpActiveDeviceChangeExt "
-                                    + " state=" + info.mState
-                                    // only querying address as this is the only readily available
-                                    // field on the device
-                                    + " addr=" + info.mDevice.getAddress()
-                                    + " prof=" + info.mProfile + " supprNoisy=" + info.mSupprNoisy
-                                    + " vol=" + info.mVolume)).printLog(TAG));
-                    synchronized (mDeviceStateLock) {
-                        mDeviceInventory.handleBluetoothA2dpActiveDeviceChangeExt(
-                                info.mDevice, info.mState, info.mProfile,
-                                info.mSupprNoisy, info.mVolume);
-                    }
-                } break;
                 default:
                     Log.wtf(TAG, "Invalid message " + msg.what);
             }
@@ -925,10 +899,8 @@
     private static final int MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT = 27;
     // process external command to (dis)connect a hearing aid device
     private static final int MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT = 28;
-    // process external command to (dis)connect or change active A2DP device
-    private static final int MSG_L_A2DP_ACTIVE_DEVICE_CHANGE_EXT = 29;
     // a ScoClient died in BtHelper
-    private static final int MSG_L_SCOCLIENT_DIED = 30;
+    private static final int MSG_L_SCOCLIENT_DIED = 29;
 
 
     private static boolean isMessageHandledUnderWakelock(int msgId) {
@@ -943,7 +915,6 @@
             case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE:
             case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT:
             case MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT:
-            case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE_EXT:
                 return true;
             default:
                 return false;
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 887c908..a9a8ef2 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -146,6 +146,7 @@
         }
     }
 
+    @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
     /*package*/ void onSetA2dpSinkConnectionState(@NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo,
             @AudioService.BtProfileConnectionState int state) {
         final BluetoothDevice btDevice = btInfo.getBtDevice();
@@ -259,6 +260,7 @@
         }
     }
 
+    @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
     /*package*/ void onBluetoothA2dpActiveDeviceChange(
             @NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo, int event) {
         final BluetoothDevice btDevice = btInfo.getBtDevice();
@@ -532,6 +534,7 @@
         return mCurAudioRoutes;
     }
 
+    @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
     /*package*/ void setBluetoothA2dpDeviceConnectionState(
             @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state,
             int profile, boolean suppressNoisyIntent, int musicDevice, int a2dpVolume) {
@@ -559,9 +562,13 @@
             final BtHelper.BluetoothA2dpDeviceInfo a2dpDeviceInfo =
                     new BtHelper.BluetoothA2dpDeviceInfo(device, a2dpVolume, a2dpCodec);
             if (profile == BluetoothProfile.A2DP) {
-                mDeviceBroker.postA2dpSinkConnection(state,
-                        a2dpDeviceInfo,
-                        delay);
+                if (delay == 0) {
+                    onSetA2dpSinkConnectionState(a2dpDeviceInfo, state);
+                } else {
+                    mDeviceBroker.postA2dpSinkConnection(state,
+                            a2dpDeviceInfo,
+                            delay);
+                }
             } else { //profile == BluetoothProfile.A2DP_SINK
                 mDeviceBroker.postA2dpSourceConnection(state,
                         a2dpDeviceInfo,
@@ -570,49 +577,6 @@
         }
     }
 
-    /*package*/ void handleBluetoothA2dpActiveDeviceChangeExt(
-            @NonNull BluetoothDevice device,
-            @AudioService.BtProfileConnectionState int state, int profile,
-            boolean suppressNoisyIntent, int a2dpVolume) {
-        if (state == BluetoothProfile.STATE_DISCONNECTED) {
-            mDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
-                           device, state, profile, suppressNoisyIntent, a2dpVolume);
-            return;
-        }
-        // state == BluetoothProfile.STATE_CONNECTED
-        synchronized (mConnectedDevices) {
-            final String address = device.getAddress();
-            final int a2dpCodec = mDeviceBroker.getA2dpCodec(device);
-            final String deviceKey = DeviceInfo.makeDeviceListKey(
-                        AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address);
-            DeviceInfo deviceInfo = mConnectedDevices.get(deviceKey);
-            if (deviceInfo != null) {
-                // Device config change for matching A2DP device
-                mDeviceBroker.postBluetoothA2dpDeviceConfigChange(device);
-                return;
-            }
-            for (int i = 0; i < mConnectedDevices.size(); i++) {
-                deviceInfo = mConnectedDevices.valueAt(i);
-                if (deviceInfo.mDeviceType != AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP) {
-                    continue;
-                }
-                // A2DP device exists, handle active device change
-                final String existingDevicekey = mConnectedDevices.keyAt(i);
-                mConnectedDevices.remove(existingDevicekey);
-                mConnectedDevices.put(deviceKey, new DeviceInfo(
-                        AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, BtHelper.getName(device),
-                        address, a2dpCodec));
-                mDeviceBroker.postA2dpActiveDeviceChange(
-                        new BtHelper.BluetoothA2dpDeviceInfo(
-                            device, a2dpVolume, a2dpCodec));
-                return;
-            }
-        }
-        // New A2DP device connection
-        mDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
-                           device, state, profile, suppressNoisyIntent, a2dpVolume);
-    }
-
     /*package*/ int setWiredDeviceConnectionState(int type, @AudioService.ConnectionState int state,
                                                   String address, String name, String caller) {
         synchronized (mConnectedDevices) {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 15e8851..376e9b5 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -1275,8 +1275,6 @@
                             System.VOLUME_SETTINGS_INT[a11yStreamAlias];
                     mStreamStates[AudioSystem.STREAM_ACCESSIBILITY].setAllIndexes(
                             mStreamStates[a11yStreamAlias], caller);
-                    mStreamStates[AudioSystem.STREAM_ACCESSIBILITY].refreshRange(
-                            mStreamVolumeAlias[AudioSystem.STREAM_ACCESSIBILITY]);
                 }
             }
             if (sIndependentA11yVolume) {
@@ -1577,20 +1575,19 @@
     }
 
     private int rescaleIndex(int index, int srcStream, int dstStream) {
-        int max = mStreamStates[srcStream].getMaxIndex();
-        if (max == 0) {
-            Log.e(TAG, "rescaleIndex : Max index should not be zero");
-            return mStreamStates[srcStream].getMinIndex();
-        }
-        final int rescaled =
-                (index * mStreamStates[dstStream].getMaxIndex()
-                        + mStreamStates[srcStream].getMaxIndex() / 2)
-                / mStreamStates[srcStream].getMaxIndex();
-        if (rescaled < mStreamStates[dstStream].getMinIndex()) {
+        int srcRange =
+                mStreamStates[srcStream].getMaxIndex() - mStreamStates[srcStream].getMinIndex();
+        int dstRange =
+                mStreamStates[dstStream].getMaxIndex() - mStreamStates[dstStream].getMinIndex();
+
+        if (srcRange == 0) {
+            Log.e(TAG, "rescaleIndex : index range should not be zero");
             return mStreamStates[dstStream].getMinIndex();
-        } else {
-            return rescaled;
         }
+
+        return mStreamStates[dstStream].getMinIndex()
+                + ((index - mStreamStates[srcStream].getMinIndex()) * dstRange + srcRange / 2)
+                / srcRange;
     }
 
     ///////////////////////////////////////////////////////////////////////////
@@ -4382,27 +4379,6 @@
         mDeviceBroker.postBluetoothA2dpDeviceConfigChange(device);
     }
 
-    /**
-     * @see AudioManager#handleBluetoothA2dpActiveDeviceChange(BluetoothDevice, int, int,
-     *                                                          boolean, int)
-     */
-    public void handleBluetoothA2dpActiveDeviceChange(
-            BluetoothDevice device, int state, int profile, boolean suppressNoisyIntent,
-            int a2dpVolume) {
-        if (device == null) {
-            throw new IllegalArgumentException("Illegal null device");
-        }
-        if (profile != BluetoothProfile.A2DP && profile != BluetoothProfile.A2DP_SINK) {
-            throw new IllegalArgumentException("invalid profile " + profile);
-        }
-        if (state != BluetoothProfile.STATE_CONNECTED
-                && state != BluetoothProfile.STATE_DISCONNECTED) {
-            throw new IllegalArgumentException("Invalid state " + state);
-        }
-        mDeviceBroker.postBluetoothA2dpDeviceConfigChangeExt(device, state, profile,
-                suppressNoisyIntent, a2dpVolume);
-    }
-
     private static final int DEVICE_MEDIA_UNMUTED_ON_PLUG =
             AudioSystem.DEVICE_OUT_WIRED_HEADSET | AudioSystem.DEVICE_OUT_WIRED_HEADPHONE |
             AudioSystem.DEVICE_OUT_LINE |
@@ -4751,24 +4727,6 @@
         }
 
         /**
-         * Updates the min/max index values from another stream. Use this when changing the alias
-         * for the current stream type.
-         * @param sourceStreamType
-         */
-        // must be sync'd on mSettingsLock before VolumeStreamState.class
-        @GuardedBy("VolumeStreamState.class")
-        public void refreshRange(int sourceStreamType) {
-            mIndexMin = MIN_STREAM_VOLUME[sourceStreamType] * 10;
-            mIndexMax = MAX_STREAM_VOLUME[sourceStreamType] * 10;
-            // verify all current volumes are within bounds
-            for (int i = 0 ; i < mIndexMap.size(); i++) {
-                final int device = mIndexMap.keyAt(i);
-                final int index = mIndexMap.valueAt(i);
-                mIndexMap.put(device, getValidIndex(index));
-            }
-        }
-
-        /**
          * Copies all device/index pairs from the given VolumeStreamState after initializing
          * them with the volume for DEVICE_OUT_DEFAULT. No-op if the source VolumeStreamState
          * has the same stream type as this instance.
@@ -5529,6 +5487,8 @@
 
     public void avrcpSupportsAbsoluteVolume(String address, boolean support) {
         // address is not used for now, but may be used when multiple a2dp devices are supported
+        sVolumeLogger.log(new AudioEventLogger.StringEvent("avrcpSupportsAbsoluteVolume addr="
+                + address + " support=" + support));
         mDeviceBroker.setAvrcpAbsoluteVolumeSupported(support);
         sendMsg(mAudioHandler, MSG_SET_DEVICE_VOLUME, SENDMSG_QUEUE,
                     AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, 0,
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 934c7bf..1a63f8f 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -200,16 +200,20 @@
 
     /*package*/ synchronized void setAvrcpAbsoluteVolumeSupported(boolean supported) {
         mAvrcpAbsVolSupported = supported;
+        Log.i(TAG, "setAvrcpAbsoluteVolumeSupported supported=" + supported);
     }
 
     /*package*/ synchronized void setAvrcpAbsoluteVolumeIndex(int index) {
         if (mA2dp == null) {
             if (AudioService.DEBUG_VOL) {
-                Log.d(TAG, "setAvrcpAbsoluteVolumeIndex: bailing due to null mA2dp");
+                AudioService.sVolumeLogger.log(new AudioEventLogger.StringEvent(
+                        "setAvrcpAbsoluteVolumeIndex: bailing due to null mA2dp").printLog(TAG));
                 return;
             }
         }
         if (!mAvrcpAbsVolSupported) {
+            AudioService.sVolumeLogger.log(new AudioEventLogger.StringEvent(
+                    "setAvrcpAbsoluteVolumeIndex: abs vol not supported ").printLog(TAG));
             return;
         }
         if (AudioService.DEBUG_VOL) {
diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java
index af25ad5..b279b37 100644
--- a/services/core/java/com/android/server/clipboard/ClipboardService.java
+++ b/services/core/java/com/android/server/clipboard/ClipboardService.java
@@ -775,8 +775,7 @@
                     // if the application has the permission, let it to access user's clipboard.
                     // To passed synthesized uid user#10_app#systemui may not tell the real uid.
                     // userId must pass intending userId. i.e. user#10.
-                    allowed = mContentCaptureInternal.isContentCaptureServiceForUser(
-                            Binder.getCallingUid(), userId);
+                    allowed = mContentCaptureInternal.isContentCaptureServiceForUser(uid, userId);
                 }
                 if (!allowed && mAutofillInternal != null) {
                     // ...or the Augmented Autofill Service
@@ -785,8 +784,7 @@
                     // if the application has the permission, let it to access user's clipboard.
                     // To passed synthesized uid user#10_app#systemui may not tell the real uid.
                     // userId must pass intending userId. i.e. user#10.
-                    allowed = mAutofillInternal.isAugmentedAutofillServiceForUser(
-                            Binder.getCallingUid(), userId);
+                    allowed = mAutofillInternal.isAugmentedAutofillServiceForUser(uid, userId);
                 }
                 if (!allowed) {
                     Slog.e(TAG, "Denying clipboard access to " + callingPackage
diff --git a/services/core/java/com/android/server/connectivity/AutodestructReference.java b/services/core/java/com/android/server/connectivity/AutodestructReference.java
new file mode 100644
index 0000000..009a43e
--- /dev/null
+++ b/services/core/java/com/android/server/connectivity/AutodestructReference.java
@@ -0,0 +1,42 @@
+/*
+ * 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.server.connectivity;
+
+import android.annotation.NonNull;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A ref that autodestructs at the first usage of it.
+ * @param <T> The type of the held object
+ * @hide
+ */
+public class AutodestructReference<T> {
+    private final AtomicReference<T> mHeld;
+    public AutodestructReference(@NonNull T obj) {
+        if (null == obj) throw new NullPointerException("Autodestruct reference to null");
+        mHeld = new AtomicReference<>(obj);
+    }
+
+    /** Get the ref and destruct it. NPE if already destructed. */
+    @NonNull
+    public T getAndDestroy() {
+        final T obj = mHeld.getAndSet(null);
+        if (null == obj) throw new NullPointerException("Already autodestructed");
+        return obj;
+    }
+}
diff --git a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
index 34772d0..864a793 100644
--- a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
@@ -25,12 +25,12 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
 import android.net.NetworkMisc;
+import android.net.NetworkMonitorManager;
 import android.net.NetworkRequest;
 import android.net.NetworkState;
 import android.os.Handler;
 import android.os.INetworkManagementService;
 import android.os.Messenger;
-import android.os.RemoteException;
 import android.os.SystemClock;
 import android.util.Log;
 import android.util.SparseArray;
@@ -247,7 +247,7 @@
     public final Nat464Xlat clatd;
 
     // Set after asynchronous creation of the NetworkMonitor.
-    private volatile INetworkMonitor mNetworkMonitor;
+    private volatile NetworkMonitorManager mNetworkMonitor;
 
     private static final String TAG = ConnectivityService.class.getSimpleName();
     private static final boolean VDBG = false;
@@ -278,7 +278,7 @@
      * Inform NetworkAgentInfo that a new NetworkMonitor was created.
      */
     public void onNetworkMonitorCreated(INetworkMonitor networkMonitor) {
-        mNetworkMonitor = networkMonitor;
+        mNetworkMonitor = new NetworkMonitorManager(networkMonitor);
     }
 
     /**
@@ -290,13 +290,9 @@
      */
     public void setNetworkCapabilities(NetworkCapabilities nc) {
         networkCapabilities = nc;
-        final INetworkMonitor nm = mNetworkMonitor;
+        final NetworkMonitorManager nm = mNetworkMonitor;
         if (nm != null) {
-            try {
-                nm.notifyNetworkCapabilitiesChanged(nc);
-            } catch (RemoteException e) {
-                Log.e(TAG, "Error notifying NetworkMonitor of updated NetworkCapabilities", e);
-            }
+            nm.notifyNetworkCapabilitiesChanged(nc);
         }
     }
 
@@ -317,11 +313,11 @@
     }
 
     /**
-     * Get the INetworkMonitor in this NetworkAgentInfo.
+     * Get the NetworkMonitorManager in this NetworkAgentInfo.
      *
      * <p>This will be null before {@link #onNetworkMonitorCreated(INetworkMonitor)} is called.
      */
-    public INetworkMonitor networkMonitor() {
+    public NetworkMonitorManager networkMonitor() {
         return mNetworkMonitor;
     }
 
diff --git a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
index 0910dac2..f6735d9 100644
--- a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
+++ b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
@@ -237,9 +237,15 @@
                     + getTransportName(transportType));
             return;
         }
-
-        final String channelId = highPriority ? SystemNotificationChannels.NETWORK_ALERTS :
-                SystemNotificationChannels.NETWORK_STATUS;
+        // When replacing an existing notification for a given network, don't alert, just silently
+        // update the existing notification. Note that setOnlyAlertOnce() will only work for the
+        // same id, and the id used here is the NotificationType which is different in every type of
+        // notification. This is required because the notification metrics only track the ID but not
+        // the tag.
+        final boolean hasPreviousNotification = previousNotifyType != null;
+        final String channelId = (highPriority && !hasPreviousNotification)
+                ? SystemNotificationChannels.NETWORK_ALERTS
+                : SystemNotificationChannels.NETWORK_STATUS;
         Notification.Builder builder = new Notification.Builder(mContext, channelId)
                 .setWhen(System.currentTimeMillis())
                 .setShowWhen(notifyType == NotificationType.NETWORK_SWITCH)
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 0271d3b..40a4820 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -953,30 +953,21 @@
             return false;
         }
 
-        LinkProperties lp = makeLinkProperties();
-        final boolean hadInternetCapability = mNetworkCapabilities.hasCapability(
-                NetworkCapabilities.NET_CAPABILITY_INTERNET);
-        final boolean willHaveInternetCapability = providesRoutesToMostDestinations(lp);
-        if (hadInternetCapability != willHaveInternetCapability) {
-            // A seamless handover would have led to a change to INTERNET capability, which
-            // is supposed to be immutable for a given network. In this case bail out and do not
-            // perform handover.
-            Log.i(TAG, "Handover not possible due to changes to INTERNET capability");
-            return false;
-        }
-
-        agent.sendLinkProperties(lp);
+        agent.sendLinkProperties(makeLinkProperties());
         return true;
     }
 
     private void agentConnect() {
         LinkProperties lp = makeLinkProperties();
 
-        if (providesRoutesToMostDestinations(lp)) {
-            mNetworkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
-        } else {
-            mNetworkCapabilities.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
-        }
+        // VPN either provide a default route (IPv4 or IPv6 or both), or they are a split tunnel
+        // that falls back to the default network, which by definition provides INTERNET (unless
+        // there is no default network, in which case none of this matters in any sense).
+        // Also, it guarantees that when a VPN applies to an app, the VPN will always be reported
+        // as the network by getDefaultNetwork and registerDefaultNetworkCallback. This in turn
+        // protects the invariant that apps calling CM#bindProcessToNetwork(getDefaultNetwork())
+        // the same as if they use the default network.
+        mNetworkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
 
         mNetworkInfo.setDetailedState(DetailedState.CONNECTING, null, null);
 
@@ -1846,10 +1837,11 @@
         if (!profile.searchDomains.isEmpty()) {
             config.searchDomains = Arrays.asList(profile.searchDomains.split(" +"));
         }
-        startLegacyVpn(config, racoon, mtpd);
+        startLegacyVpn(config, racoon, mtpd, profile);
     }
 
-    private synchronized void startLegacyVpn(VpnConfig config, String[] racoon, String[] mtpd) {
+    private synchronized void startLegacyVpn(VpnConfig config, String[] racoon, String[] mtpd,
+            VpnProfile profile) {
         stopLegacyVpnPrivileged();
 
         // Prepare for the new request.
@@ -1857,7 +1849,7 @@
         updateState(DetailedState.CONNECTING, "startLegacyVpn");
 
         // Start a new LegacyVpnRunner and we are done!
-        mLegacyVpnRunner = new LegacyVpnRunner(config, racoon, mtpd);
+        mLegacyVpnRunner = new LegacyVpnRunner(config, racoon, mtpd, profile);
         mLegacyVpnRunner.start();
     }
 
@@ -1923,6 +1915,7 @@
         private final String mOuterInterface;
         private final AtomicInteger mOuterConnection =
                 new AtomicInteger(ConnectivityManager.TYPE_NONE);
+        private final VpnProfile mProfile;
 
         private long mBringupStartTime = -1;
 
@@ -1949,7 +1942,7 @@
             }
         };
 
-        public LegacyVpnRunner(VpnConfig config, String[] racoon, String[] mtpd) {
+        LegacyVpnRunner(VpnConfig config, String[] racoon, String[] mtpd, VpnProfile profile) {
             super(TAG);
             mConfig = config;
             mDaemons = new String[] {"racoon", "mtpd"};
@@ -1965,6 +1958,8 @@
             // registering
             mOuterInterface = mConfig.interfaze;
 
+            mProfile = profile;
+
             if (!TextUtils.isEmpty(mOuterInterface)) {
                 final ConnectivityManager cm = ConnectivityManager.from(mContext);
                 for (Network network : cm.getAllNetworks()) {
@@ -2177,7 +2172,7 @@
                 }
 
                 // Add a throw route for the VPN server endpoint, if one was specified.
-                String endpoint = parameters[5];
+                String endpoint = parameters[5].isEmpty() ? mProfile.server : parameters[5];
                 if (!endpoint.isEmpty()) {
                     try {
                         InetAddress addr = InetAddress.parseNumericAddress(endpoint);
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 5fb67dd..5804fc8 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -309,6 +309,13 @@
     private boolean mAppliedTemporaryAutoBrightnessAdjustment;
     private boolean mAppliedBrightnessBoost;
 
+    // Reason for which the brightness was last changed. See {@link BrightnessReason} for more
+    // information.
+    // At the time of this writing, this value is changed within updatePowerState() only, which is
+    // limited to the thread used by DisplayControllerHandler.
+    private BrightnessReason mBrightnessReason = new BrightnessReason();
+    private BrightnessReason mBrightnessReasonTemp = new BrightnessReason();
+
     // Brightness animation ramp rates in brightness units per second
     private final int mBrightnessRampRateFast;
     private final int mBrightnessRampRateSlow;
@@ -733,6 +740,8 @@
         final boolean mustNotify;
         final int previousPolicy;
         boolean mustInitialize = false;
+        int brightnessAdjustmentFlags = 0;
+        mBrightnessReasonTemp.set(null);
 
         synchronized (mLock) {
             mPendingUpdatePowerStateLocked = false;
@@ -786,6 +795,7 @@
                 }
                 if (!mAllowAutoBrightnessWhileDozingConfig) {
                     brightness = mPowerRequest.dozeScreenBrightness;
+                    mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE);
                 }
                 break;
             case DisplayPowerRequest.POLICY_VR:
@@ -839,15 +849,18 @@
         // Use zero brightness when screen is off.
         if (state == Display.STATE_OFF) {
             brightness = PowerManager.BRIGHTNESS_OFF;
+            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_SCREEN_OFF);
         }
 
         // Always use the VR brightness when in the VR state.
         if (state == Display.STATE_VR) {
             brightness = mScreenBrightnessForVr;
+            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_VR);
         }
 
         if (brightness < 0 && mPowerRequest.screenBrightnessOverride > 0) {
             brightness = mPowerRequest.screenBrightnessOverride;
+            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_OVERRIDE);
             mAppliedScreenBrightnessOverride = true;
         } else {
             mAppliedScreenBrightnessOverride = false;
@@ -867,6 +880,7 @@
         if (mTemporaryScreenBrightness > 0) {
             brightness = mTemporaryScreenBrightness;
             mAppliedTemporaryBrightness = true;
+            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_TEMPORARY);
         } else {
             mAppliedTemporaryBrightness = false;
         }
@@ -880,9 +894,11 @@
         final float autoBrightnessAdjustment;
         if (!Float.isNaN(mTemporaryAutoBrightnessAdjustment)) {
             autoBrightnessAdjustment = mTemporaryAutoBrightnessAdjustment;
+            brightnessAdjustmentFlags = BrightnessReason.ADJUSTMENT_AUTO_TEMP;
             mAppliedTemporaryAutoBrightnessAdjustment = true;
         } else {
             autoBrightnessAdjustment = mAutoBrightnessAdjustment;
+            brightnessAdjustmentFlags = BrightnessReason.ADJUSTMENT_AUTO;
             mAppliedTemporaryAutoBrightnessAdjustment = false;
         }
 
@@ -893,6 +909,7 @@
         if (mPowerRequest.boostScreenBrightness
                 && brightness != PowerManager.BRIGHTNESS_OFF) {
             brightness = PowerManager.BRIGHTNESS_ON;
+            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_BOOST);
             mAppliedBrightnessBoost = true;
         } else {
             mAppliedBrightnessBoost = false;
@@ -936,6 +953,7 @@
                 // it means in absolute terms.
                 putScreenBrightnessSetting(brightness);
                 mAppliedAutoBrightness = true;
+                mBrightnessReasonTemp.setReason(BrightnessReason.REASON_AUTOMATIC);
             } else {
                 mAppliedAutoBrightness = false;
             }
@@ -943,19 +961,25 @@
                 // If the autobrightness controller has decided to change the adjustment value
                 // used, make sure that's reflected in settings.
                 putAutoBrightnessAdjustmentSetting(newAutoBrightnessAdjustment);
+            } else {
+                // Adjustment values resulted in no change
+                brightnessAdjustmentFlags = 0;
             }
         } else {
             mAppliedAutoBrightness = false;
+            brightnessAdjustmentFlags = 0;
         }
 
         // Use default brightness when dozing unless overridden.
         if (brightness < 0 && Display.isDozeState(state)) {
             brightness = mScreenBrightnessDozeConfig;
+            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_DEFAULT);
         }
 
         // Apply manual brightness.
         if (brightness < 0) {
             brightness = clampScreenBrightness(mCurrentScreenBrightnessSetting);
+            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_MANUAL);
         }
 
         // Apply dimming by at least some minimum amount when user activity
@@ -964,6 +988,7 @@
             if (brightness > mScreenBrightnessRangeMinimum) {
                 brightness = Math.max(Math.min(brightness - SCREEN_DIM_MINIMUM_REDUCTION,
                         mScreenBrightnessDimConfig), mScreenBrightnessRangeMinimum);
+                mBrightnessReasonTemp.addModifier(BrightnessReason.MODIFIER_DIMMED);
             }
             if (!mAppliedDimming) {
                 slowChange = false;
@@ -982,6 +1007,7 @@
                         Math.min(mPowerRequest.screenLowPowerBrightnessFactor, 1);
                 final int lowPowerBrightness = (int) (brightness * brightnessFactor);
                 brightness = Math.max(lowPowerBrightness, mScreenBrightnessRangeMinimum);
+                mBrightnessReasonTemp.addModifier(BrightnessReason.MODIFIER_LOW_POWER);
             }
             if (!mAppliedLowPower) {
                 slowChange = false;
@@ -1047,6 +1073,14 @@
 
         }
 
+        // Log any changes to what is currently driving the brightness setting.
+        if (!mBrightnessReasonTemp.equals(mBrightnessReason) || brightnessAdjustmentFlags != 0) {
+            Slog.v(TAG, "Brightness [" + brightness + "] reason changing to: '"
+                    + mBrightnessReasonTemp.toString(brightnessAdjustmentFlags)
+                    + "', previous reason: '" + mBrightnessReason + "'.");
+            mBrightnessReason.set(mBrightnessReasonTemp);
+        }
+
         // Update display white-balance.
         if (mDisplayWhiteBalanceController != null) {
             if (state == Display.STATE_ON && mDisplayWhiteBalanceSettings.isEnabled()) {
@@ -1737,6 +1771,7 @@
         pw.println("  mPendingScreenBrightnessSetting=" + mPendingScreenBrightnessSetting);
         pw.println("  mTemporaryScreenBrightness=" + mTemporaryScreenBrightness);
         pw.println("  mAutoBrightnessAdjustment=" + mAutoBrightnessAdjustment);
+        pw.println("  mBrightnessReason=" + mBrightnessReason);
         pw.println("  mTemporaryAutoBrightnessAdjustment=" + mTemporaryAutoBrightnessAdjustment);
         pw.println("  mPendingAutoBrightnessAdjustment=" + mPendingAutoBrightnessAdjustment);
         pw.println("  mScreenBrightnessForVr=" + mScreenBrightnessForVr);
@@ -1956,4 +1991,121 @@
             sendUpdatePowerState();
         }
     }
+
+    /**
+     * Stores data about why the brightness was changed.  Made up of one main
+     * {@code BrightnessReason.REASON_*} reason and various {@code BrightnessReason.MODIFIER_*}
+     * modifiers.
+     */
+    private final class BrightnessReason {
+        static final int REASON_UNKNOWN = 0;
+        static final int REASON_MANUAL = 1;
+        static final int REASON_DOZE = 2;
+        static final int REASON_DOZE_DEFAULT = 3;
+        static final int REASON_AUTOMATIC = 4;
+        static final int REASON_SCREEN_OFF = 5;
+        static final int REASON_VR = 6;
+        static final int REASON_OVERRIDE = 7;
+        static final int REASON_TEMPORARY = 8;
+        static final int REASON_BOOST = 9;
+        static final int REASON_MAX = REASON_BOOST;
+
+        static final int MODIFIER_DIMMED = 0x1;
+        static final int MODIFIER_LOW_POWER = 0x2;
+        static final int MODIFIER_MASK = 0x3;
+
+        // ADJUSTMENT_*
+        // These things can happen at any point, even if the main brightness reason doesn't
+        // fundamentally change, so they're not stored.
+
+        // Auto-brightness adjustment factor changed
+        static final int ADJUSTMENT_AUTO_TEMP = 0x1;
+        // Temporary adjustment to the auto-brightness adjustment factor.
+        static final int ADJUSTMENT_AUTO = 0x2;
+
+        // One of REASON_*
+        public int reason;
+        // Any number of MODIFIER_*
+        public int modifier;
+
+        public void set(BrightnessReason other) {
+            setReason(other == null ? REASON_UNKNOWN : other.reason);
+            setModifier(other == null ? 0 : other.modifier);
+        }
+
+        public void setReason(int reason) {
+            if (reason < REASON_UNKNOWN || reason > REASON_MAX) {
+                Slog.w(TAG, "brightness reason out of bounds: " + reason);
+            } else {
+                this.reason = reason;
+            }
+        }
+
+        public void setModifier(int modifier) {
+            if ((modifier & ~MODIFIER_MASK) != 0) {
+                Slog.w(TAG, "brightness modifier out of bounds: 0x"
+                        + Integer.toHexString(modifier));
+            } else {
+                this.modifier = modifier;
+            }
+        }
+
+        public void addModifier(int modifier) {
+            setModifier(modifier | this.modifier);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == null || !(obj instanceof BrightnessReason)) {
+                return false;
+            }
+            BrightnessReason other = (BrightnessReason) obj;
+            return other.reason == reason && other.modifier == modifier;
+        }
+
+        @Override
+        public String toString() {
+            return toString(0);
+        }
+
+        public String toString(int adjustments) {
+            final StringBuilder sb = new StringBuilder();
+            sb.append(reasonToString(reason));
+            sb.append(" [");
+            if ((adjustments & ADJUSTMENT_AUTO_TEMP) != 0) {
+                sb.append(" temp_adj");
+            }
+            if ((adjustments & ADJUSTMENT_AUTO) != 0) {
+                sb.append(" auto_adj");
+            }
+            if ((modifier & MODIFIER_LOW_POWER) != 0) {
+                sb.append(" low_pwr");
+            }
+            if ((modifier & MODIFIER_DIMMED) != 0) {
+                sb.append(" dim");
+            }
+            int strlen = sb.length();
+            if (sb.charAt(strlen - 1) == '[') {
+                sb.setLength(strlen - 2);
+            } else {
+                sb.append(" ]");
+            }
+            return sb.toString();
+        }
+
+        private String reasonToString(int reason) {
+            switch (reason) {
+                case REASON_MANUAL: return "manual";
+                case REASON_DOZE: return "doze";
+                case REASON_DOZE_DEFAULT: return "doze_default";
+                case REASON_AUTOMATIC: return "automatic";
+                case REASON_SCREEN_OFF: return "screen_off";
+                case REASON_VR: return "vr";
+                case REASON_OVERRIDE: return "override";
+                case REASON_TEMPORARY: return "temporary";
+                case REASON_BOOST: return "boost";
+                default: return Integer.toString(reason);
+            }
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java
index b2420b5..64a9e00 100644
--- a/services/core/java/com/android/server/display/color/ColorDisplayService.java
+++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java
@@ -394,7 +394,9 @@
     private void tearDown() {
         Slog.d(TAG, "tearDown: currentUser=" + mCurrentUser);
 
-        getContext().getContentResolver().unregisterContentObserver(mContentObserver);
+        if (mContentObserver != null) {
+            getContext().getContentResolver().unregisterContentObserver(mContentObserver);
+        }
 
         if (mNightDisplayTintController.isAvailable(getContext())) {
             if (mNightDisplayAutoMode != null) {
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index ea1c49d..ab4ae9d 100644
--- a/services/core/java/com/android/server/job/JobSchedulerService.java
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -1635,6 +1635,9 @@
      */
     private static final long PERIODIC_JOB_WINDOW_BUFFER = 30 * MINUTE_IN_MILLIS;
 
+    /** The maximum period a periodic job can have. Anything higher will be clamped down to this. */
+    public static final long MAX_ALLOWED_PERIOD_MS = 365 * 24 * 60 * 60 * 1000L;
+
     /**
      * Called after a periodic has executed so we can reschedule it. We take the last execution
      * time of the job to be the time of completion (i.e. the time at which this function is
@@ -1652,11 +1655,21 @@
     JobStatus getRescheduleJobForPeriodic(JobStatus periodicToReschedule) {
         final long elapsedNow = sElapsedRealtimeClock.millis();
         final long newLatestRuntimeElapsed;
-        final long period = periodicToReschedule.getJob().getIntervalMillis();
-        final long latestRunTimeElapsed = periodicToReschedule.getOriginalLatestRunTimeElapsed();
-        final long flex = periodicToReschedule.getJob().getFlexMillis();
+        // Make sure period is in the interval [min_possible_period, max_possible_period].
+        final long period = Math.max(JobInfo.getMinPeriodMillis(),
+                Math.min(MAX_ALLOWED_PERIOD_MS, periodicToReschedule.getJob().getIntervalMillis()));
+        // Make sure flex is in the interval [min_possible_flex, period].
+        final long flex = Math.max(JobInfo.getMinFlexMillis(),
+                Math.min(period, periodicToReschedule.getJob().getFlexMillis()));
         long rescheduleBuffer = 0;
 
+        long olrte = periodicToReschedule.getOriginalLatestRunTimeElapsed();
+        if (olrte < 0 || olrte == JobStatus.NO_LATEST_RUNTIME) {
+            Slog.wtf(TAG, "Invalid periodic job original latest run time: " + olrte);
+            olrte = elapsedNow;
+        }
+        final long latestRunTimeElapsed = olrte;
+
         final long diffMs = Math.abs(elapsedNow - latestRunTimeElapsed);
         if (elapsedNow > latestRunTimeElapsed) {
             // The job ran past its expected run window. Have it count towards the current window
@@ -1664,7 +1677,7 @@
             if (DEBUG) {
                 Slog.i(TAG, "Periodic job ran after its intended window.");
             }
-            int numSkippedWindows = (int) (diffMs / period) + 1; // +1 to include original window
+            long numSkippedWindows = (diffMs / period) + 1; // +1 to include original window
             if (period != flex && diffMs > Math.min(PERIODIC_JOB_WINDOW_BUFFER,
                     (period - flex) / 2)) {
                 if (DEBUG) {
@@ -1684,6 +1697,16 @@
             }
         }
 
+        if (newLatestRuntimeElapsed < elapsedNow) {
+            Slog.wtf(TAG, "Rescheduling calculated latest runtime in the past: "
+                    + newLatestRuntimeElapsed);
+            return new JobStatus(periodicToReschedule, getCurrentHeartbeat(),
+                    elapsedNow + period - flex, elapsedNow + period,
+                    0 /* backoffAttempt */,
+                    sSystemClock.millis() /* lastSuccessfulRunTime */,
+                    periodicToReschedule.getLastFailedRunTime());
+        }
+
         final long newEarliestRunTimeElapsed = newLatestRuntimeElapsed
                 - Math.min(flex, period - rescheduleBuffer);
 
@@ -2280,8 +2303,6 @@
                     if (bucket >= mConstants.STANDBY_BEATS.length
                             || (mHeartbeat > appLastRan
                             && mHeartbeat < appLastRan + mConstants.STANDBY_BEATS[bucket])) {
-                        // TODO: log/trace that we're deferring the job due to bucketing if we
-                        // hit this
                         if (job.getWhenStandbyDeferred() == 0) {
                             if (DEBUG_STANDBY) {
                                 Slog.v(TAG, "Bucket deferral: " + mHeartbeat + " < "
diff --git a/services/core/java/com/android/server/job/controllers/JobStatus.java b/services/core/java/com/android/server/job/controllers/JobStatus.java
index fd20e11..eb5d472 100644
--- a/services/core/java/com/android/server/job/controllers/JobStatus.java
+++ b/services/core/java/com/android/server/job/controllers/JobStatus.java
@@ -511,8 +511,13 @@
         final long elapsedNow = sElapsedRealtimeClock.millis();
         final long earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis;
         if (job.isPeriodic()) {
-            latestRunTimeElapsedMillis = elapsedNow + job.getIntervalMillis();
-            earliestRunTimeElapsedMillis = latestRunTimeElapsedMillis - job.getFlexMillis();
+            // Make sure period is in the interval [min_possible_period, max_possible_period].
+            final long period = Math.max(JobInfo.getMinPeriodMillis(),
+                    Math.min(JobSchedulerService.MAX_ALLOWED_PERIOD_MS, job.getIntervalMillis()));
+            latestRunTimeElapsedMillis = elapsedNow + period;
+            earliestRunTimeElapsedMillis = latestRunTimeElapsedMillis
+                    // Make sure flex is in the interval [min_possible_flex, period].
+                    - Math.max(JobInfo.getMinFlexMillis(), Math.min(period, job.getFlexMillis()));
         } else {
             earliestRunTimeElapsedMillis = job.hasEarlyConstraint() ?
                     elapsedNow + job.getMinLatencyMillis() : NO_EARLIEST_RUNTIME;
@@ -1631,6 +1636,9 @@
         formatRunTime(pw, earliestRunTimeElapsedMillis, NO_EARLIEST_RUNTIME, elapsedRealtimeMillis);
         pw.print(", latest=");
         formatRunTime(pw, latestRunTimeElapsedMillis, NO_LATEST_RUNTIME, elapsedRealtimeMillis);
+        pw.print(", original latest=");
+        formatRunTime(pw, mOriginalLatestRunTimeElapsedMillis,
+                NO_LATEST_RUNTIME, elapsedRealtimeMillis);
         pw.println();
         if (numFailures != 0) {
             pw.print(prefix); pw.print("Num failures: "); pw.println(numFailures);
diff --git a/services/core/java/com/android/server/job/controllers/QuotaController.java b/services/core/java/com/android/server/job/controllers/QuotaController.java
index f560d69..18d193a 100644
--- a/services/core/java/com/android/server/job/controllers/QuotaController.java
+++ b/services/core/java/com/android/server/job/controllers/QuotaController.java
@@ -16,6 +16,10 @@
 
 package com.android.server.job.controllers;
 
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+
 import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
 import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX;
 import static com.android.server.job.JobSchedulerService.NEVER_INDEX;
@@ -75,17 +79,28 @@
 /**
  * Controller that tracks whether an app has exceeded its standby bucket quota.
  *
- * Each job in each bucket is given 10 minutes to run within its respective time window. Active
- * jobs can run indefinitely, working set jobs can run for 10 minutes within a 2 hour window,
- * frequent jobs get to run 10 minutes in an 8 hour window, and rare jobs get to run 10 minutes in
- * a 24 hour window. The windows are rolling, so as soon as a job would have some quota based on its
- * bucket, it will be eligible to run. When a job's bucket changes, its new quota is immediately
- * applied to it.
+ * With initial defaults, each app in each bucket is given 10 minutes to run within its respective
+ * time window. Active jobs can run indefinitely, working set jobs can run for 10 minutes within a
+ * 2 hour window, frequent jobs get to run 10 minutes in an 8 hour window, and rare jobs get to run
+ * 10 minutes in a 24 hour window. The windows are rolling, so as soon as a job would have some
+ * quota based on its bucket, it will be eligible to run. When a job's bucket changes, its new
+ * quota is immediately applied to it.
+ *
+ * Job and session count limits are included to prevent abuse/spam. Each bucket has its own limit on
+ * the number of jobs or sessions that can run within the window. Regardless of bucket, apps will
+ * not be allowed to run more than 20 jobs within the past 10 minutes.
  *
  * Jobs are throttled while an app is not in a foreground state. All jobs are allowed to run
  * freely when an app enters the foreground state and are restricted when the app leaves the
- * foreground state. However, jobs that are started while the app is in the TOP state are not
- * restricted regardless of the app's state change.
+ * foreground state. However, jobs that are started while the app is in the TOP state do not count
+ * towards any quota and are not restricted regardless of the app's state change.
+ *
+ * Jobs will not be throttled when the device is charging. The device is considered to be charging
+ * once the {@link BatteryManager#ACTION_CHARGING} intent has been broadcast.
+ *
+ * Note: all limits are enforced per bucket window unless explicitly stated otherwise.
+ * All stated values are configurable and subject to change. See {@link QcConstants} for current
+ * defaults.
  *
  * Test: atest com.android.server.job.controllers.QuotaControllerTest
  */
@@ -94,8 +109,6 @@
     private static final boolean DEBUG = JobSchedulerService.DEBUG
             || Log.isLoggable(TAG, Log.DEBUG);
 
-    private static final long MINUTE_IN_MILLIS = 60 * 1000L;
-
     private static final String ALARM_TAG_CLEANUP = "*job.cleanup*";
     private static final String ALARM_TAG_QUOTA_CHECK = "*job.quota_check*";
 
@@ -244,6 +257,8 @@
         public long expirationTimeElapsed;
 
         public long windowSizeMs;
+        public int jobCountLimit;
+        public int sessionCountLimit;
 
         /** The total amount of time the app ran in its respective bucket window size. */
         public long executionTimeInWindowMs;
@@ -260,54 +275,58 @@
         public int sessionCountInWindow;
 
         /**
-         * The time after which the sum of all the app's sessions plus {@link #mQuotaBufferMs}
-         * equals the quota. This is only valid if
-         * executionTimeInWindowMs >= {@link #mAllowedTimePerPeriodMs} or
-         * executionTimeInMaxPeriodMs >= {@link #mMaxExecutionTimeMs}.
+         * The time after which the app will be under the bucket quota and can start running jobs
+         * again. This is only valid if
+         * {@link #executionTimeInWindowMs} >= {@link #mAllowedTimePerPeriodMs},
+         * {@link #executionTimeInMaxPeriodMs} >= {@link #mMaxExecutionTimeMs},
+         * {@link #bgJobCountInWindow} >= {@link #jobCountLimit}, or
+         * {@link #sessionCountInWindow} >= {@link #sessionCountLimit}.
          */
-        public long quotaCutoffTimeElapsed;
+        public long inQuotaTimeElapsed;
 
         /**
-         * The time after which {@link #jobCountInAllowedTime} should be considered invalid, in the
-         * elapsed realtime timebase.
+         * The time after which {@link #jobCountInRateLimitingWindow} should be considered invalid,
+         * in the elapsed realtime timebase.
          */
-        public long jobCountExpirationTimeElapsed;
+        public long jobRateLimitExpirationTimeElapsed;
 
         /**
-         * The number of jobs that ran in at least the last {@link #mAllowedTimePerPeriodMs}.
+         * The number of jobs that ran in at least the last {@link #mRateLimitingWindowMs}.
          * It may contain a few stale entries since cleanup won't happen exactly every
-         * {@link #mAllowedTimePerPeriodMs}.
+         * {@link #mRateLimitingWindowMs}.
          */
-        public int jobCountInAllowedTime;
+        public int jobCountInRateLimitingWindow;
 
         /**
-         * The time after which {@link #sessionCountInAllowedTime} should be considered
+         * The time after which {@link #sessionCountInRateLimitingWindow} should be considered
          * invalid, in the elapsed realtime timebase.
          */
-        public long sessionCountExpirationTimeElapsed;
+        public long sessionRateLimitExpirationTimeElapsed;
 
         /**
          * The number of {@link TimingSession}s that ran in at least the last
-         * {@link #mAllowedTimePerPeriodMs}. It may contain a few stale entries since cleanup won't
-         * happen exactly every {@link #mAllowedTimePerPeriodMs}. This should only be considered
-         * valid before elapsed realtime has reached {@link #sessionCountExpirationTimeElapsed}.
+         * {@link #mRateLimitingWindowMs}. It may contain a few stale entries since cleanup won't
+         * happen exactly every {@link #mRateLimitingWindowMs}. This should only be considered
+         * valid before elapsed realtime has reached {@link #sessionRateLimitExpirationTimeElapsed}.
          */
-        public int sessionCountInAllowedTime;
+        public int sessionCountInRateLimitingWindow;
 
         @Override
         public String toString() {
             return "expirationTime=" + expirationTimeElapsed + ", "
-                    + "windowSize=" + windowSizeMs + ", "
+                    + "windowSizeMs=" + windowSizeMs + ", "
+                    + "jobCountLimit=" + jobCountLimit + ", "
+                    + "sessionCountLimit=" + sessionCountLimit + ", "
                     + "executionTimeInWindow=" + executionTimeInWindowMs + ", "
                     + "bgJobCountInWindow=" + bgJobCountInWindow + ", "
                     + "executionTimeInMaxPeriod=" + executionTimeInMaxPeriodMs + ", "
                     + "bgJobCountInMaxPeriod=" + bgJobCountInMaxPeriod + ", "
                     + "sessionCountInWindow=" + sessionCountInWindow + ", "
-                    + "quotaCutoffTime=" + quotaCutoffTimeElapsed + ", "
-                    + "jobCountExpirationTime=" + jobCountExpirationTimeElapsed + ", "
-                    + "jobCountInAllowedTime=" + jobCountInAllowedTime + ", "
-                    + "sessionCountExpirationTime=" + sessionCountExpirationTimeElapsed + ", "
-                    + "sessionCountInAllowedTime=" + sessionCountInAllowedTime;
+                    + "inQuotaTime=" + inQuotaTimeElapsed + ", "
+                    + "jobCountExpirationTime=" + jobRateLimitExpirationTimeElapsed + ", "
+                    + "jobCountInRateLimitingWindow=" + jobCountInRateLimitingWindow + ", "
+                    + "sessionCountExpirationTime=" + sessionRateLimitExpirationTimeElapsed + ", "
+                    + "sessionCountInRateLimitingWindow=" + sessionCountInRateLimitingWindow;
         }
 
         @Override
@@ -316,18 +335,21 @@
                 ExecutionStats other = (ExecutionStats) obj;
                 return this.expirationTimeElapsed == other.expirationTimeElapsed
                         && this.windowSizeMs == other.windowSizeMs
+                        && this.jobCountLimit == other.jobCountLimit
+                        && this.sessionCountLimit == other.sessionCountLimit
                         && this.executionTimeInWindowMs == other.executionTimeInWindowMs
                         && this.bgJobCountInWindow == other.bgJobCountInWindow
                         && this.executionTimeInMaxPeriodMs == other.executionTimeInMaxPeriodMs
                         && this.sessionCountInWindow == other.sessionCountInWindow
                         && this.bgJobCountInMaxPeriod == other.bgJobCountInMaxPeriod
-                        && this.quotaCutoffTimeElapsed == other.quotaCutoffTimeElapsed
-                        && this.jobCountExpirationTimeElapsed == other.jobCountExpirationTimeElapsed
-                        && this.jobCountInAllowedTime == other.jobCountInAllowedTime
-                        && this.sessionCountExpirationTimeElapsed
-                        == other.sessionCountExpirationTimeElapsed
-                        && this.sessionCountInAllowedTime
-                        == other.sessionCountInAllowedTime;
+                        && this.inQuotaTimeElapsed == other.inQuotaTimeElapsed
+                        && this.jobRateLimitExpirationTimeElapsed
+                                == other.jobRateLimitExpirationTimeElapsed
+                        && this.jobCountInRateLimitingWindow == other.jobCountInRateLimitingWindow
+                        && this.sessionRateLimitExpirationTimeElapsed
+                                == other.sessionRateLimitExpirationTimeElapsed
+                        && this.sessionCountInRateLimitingWindow
+                                == other.sessionCountInRateLimitingWindow;
             } else {
                 return false;
             }
@@ -338,16 +360,18 @@
             int result = 0;
             result = 31 * result + hashLong(expirationTimeElapsed);
             result = 31 * result + hashLong(windowSizeMs);
+            result = 31 * result + hashLong(jobCountLimit);
+            result = 31 * result + hashLong(sessionCountLimit);
             result = 31 * result + hashLong(executionTimeInWindowMs);
             result = 31 * result + bgJobCountInWindow;
             result = 31 * result + hashLong(executionTimeInMaxPeriodMs);
             result = 31 * result + bgJobCountInMaxPeriod;
             result = 31 * result + sessionCountInWindow;
-            result = 31 * result + hashLong(quotaCutoffTimeElapsed);
-            result = 31 * result + hashLong(jobCountExpirationTimeElapsed);
-            result = 31 * result + jobCountInAllowedTime;
-            result = 31 * result + hashLong(sessionCountExpirationTimeElapsed);
-            result = 31 * result + sessionCountInAllowedTime;
+            result = 31 * result + hashLong(inQuotaTimeElapsed);
+            result = 31 * result + hashLong(jobRateLimitExpirationTimeElapsed);
+            result = 31 * result + jobCountInRateLimitingWindow;
+            result = 31 * result + hashLong(sessionRateLimitExpirationTimeElapsed);
+            result = 31 * result + sessionCountInRateLimitingWindow;
             return result;
         }
     }
@@ -399,19 +423,19 @@
     private boolean mShouldThrottle;
 
     /** How much time each app will have to run jobs within their standby bucket window. */
-    private long mAllowedTimePerPeriodMs = 10 * MINUTE_IN_MILLIS;
+    private long mAllowedTimePerPeriodMs = QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_MS;
 
     /**
      * The maximum amount of time an app can have its jobs running within a {@link #MAX_PERIOD_MS}
      * window.
      */
-    private long mMaxExecutionTimeMs = 4 * 60 * MINUTE_IN_MILLIS;
+    private long mMaxExecutionTimeMs = QcConstants.DEFAULT_MAX_EXECUTION_TIME_MS;
 
     /**
      * How much time the app should have before transitioning from out-of-quota to in-quota.
      * This should not affect processing if the app is already in-quota.
      */
-    private long mQuotaBufferMs = 30 * 1000L; // 30 seconds
+    private long mQuotaBufferMs = QcConstants.DEFAULT_IN_QUOTA_BUFFER_MS;
 
     /**
      * {@link #mAllowedTimePerPeriodMs} - {@link #mQuotaBufferMs}. This can be used to determine
@@ -425,14 +449,19 @@
      */
     private long mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
 
-    /** The maximum number of jobs that can run within the past {@link #mAllowedTimePerPeriodMs}. */
-    private int mMaxJobCountPerAllowedTime = 20;
+    /** The period of time used to rate limit recently run jobs. */
+    private long mRateLimitingWindowMs = QcConstants.DEFAULT_RATE_LIMITING_WINDOW_MS;
+
+    /** The maximum number of jobs that can run within the past {@link #mRateLimitingWindowMs}. */
+    private int mMaxJobCountPerRateLimitingWindow =
+            QcConstants.DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
 
     /**
      * The maximum number of {@link TimingSession}s that can run within the past {@link
-     * #mAllowedTimePerPeriodMs}.
+     * #mRateLimitingWindowMs}.
      */
-    private int mMaxSessionCountPerAllowedTime = 20;
+    private int mMaxSessionCountPerRateLimitingWindow =
+            QcConstants.DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW;
 
     private long mNextCleanupTimeElapsed = 0;
     private final AlarmManager.OnAlarmListener mSessionCleanupAlarmListener =
@@ -486,11 +515,11 @@
      * The rolling window size for each standby bucket. Within each window, an app will have 10
      * minutes to run its jobs.
      */
-    private final long[] mBucketPeriodsMs = new long[] {
-            10 * MINUTE_IN_MILLIS, // 10 minutes for ACTIVE -- ACTIVE apps can run jobs at any time
-            2 * 60 * MINUTE_IN_MILLIS, // 2 hours for WORKING
-            8 * 60 * MINUTE_IN_MILLIS, // 8 hours for FREQUENT
-            24 * 60 * MINUTE_IN_MILLIS // 24 hours for RARE
+    private final long[] mBucketPeriodsMs = new long[]{
+            QcConstants.DEFAULT_WINDOW_SIZE_ACTIVE_MS,
+            QcConstants.DEFAULT_WINDOW_SIZE_WORKING_MS,
+            QcConstants.DEFAULT_WINDOW_SIZE_FREQUENT_MS,
+            QcConstants.DEFAULT_WINDOW_SIZE_RARE_MS
     };
 
     /** The maximum period any bucket can have. */
@@ -503,16 +532,13 @@
      *
      * @see #mBucketPeriodsMs
      */
-    private final int[] mMaxBucketJobCounts = new int[] {
-            200,  // ACTIVE   -- 1200/hr
-            1200, // WORKING  -- 600/hr
-            1800, // FREQUENT -- 225/hr
-            2400  // RARE     -- 100/hr
+    private final int[] mMaxBucketJobCounts = new int[]{
+            QcConstants.DEFAULT_MAX_JOB_COUNT_ACTIVE,
+            QcConstants.DEFAULT_MAX_JOB_COUNT_WORKING,
+            QcConstants.DEFAULT_MAX_JOB_COUNT_FREQUENT,
+            QcConstants.DEFAULT_MAX_JOB_COUNT_RARE
     };
 
-    /** The minimum number of jobs that any bucket will be allowed to run. */
-    private static final int MIN_BUCKET_JOB_COUNT = 100;
-
     /**
      * The maximum number of {@link TimingSession}s based on its standby bucket. For each max value
      * count in the array, the app will not be allowed to have more than that many number of
@@ -527,14 +553,12 @@
             QcConstants.DEFAULT_MAX_SESSION_COUNT_RARE
     };
 
-    /** The minimum number of {@link TimingSession}s that any bucket will be allowed to run. */
-    private static final int MIN_BUCKET_SESSION_COUNT = 3;
-
     /**
      * Treat two distinct {@link TimingSession}s as the same if they start and end within this
      * amount of time of each other.
      */
-    private long mTimingSessionCoalescingDurationMs = 0;
+    private long mTimingSessionCoalescingDurationMs =
+            QcConstants.DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS;
 
     /** An app has reached its quota. The message should contain a {@link Package} object. */
     private static final int MSG_REACHED_QUOTA = 0;
@@ -593,7 +617,7 @@
         jobStatus.setTrackingController(JobStatus.TRACKING_QUOTA);
         if (mShouldThrottle) {
             final boolean isWithinQuota = isWithinQuotaLocked(jobStatus);
-            jobStatus.setQuotaConstraintSatisfied(isWithinQuota);
+            setConstraintSatisfied(jobStatus, isWithinQuota);
             if (!isWithinQuota) {
                 maybeScheduleStartAlarmLocked(userId, pkgName,
                         getEffectiveStandbyBucket(jobStatus));
@@ -761,8 +785,8 @@
             final int standbyBucket) {
         final long now = sElapsedRealtimeClock.millis();
         final boolean isUnderAllowedTimeQuota =
-                (stats.jobCountExpirationTimeElapsed <= now
-                        || stats.jobCountInAllowedTime < mMaxJobCountPerAllowedTime);
+                (stats.jobRateLimitExpirationTimeElapsed <= now
+                        || stats.jobCountInRateLimitingWindow < mMaxJobCountPerRateLimitingWindow);
         return isUnderAllowedTimeQuota
                 && (stats.bgJobCountInWindow < mMaxBucketJobCounts[standbyBucket]);
     }
@@ -770,10 +794,8 @@
     private boolean isUnderSessionCountQuotaLocked(@NonNull ExecutionStats stats,
             final int standbyBucket) {
         final long now = sElapsedRealtimeClock.millis();
-        final boolean isUnderAllowedTimeQuota =
-                (stats.sessionCountExpirationTimeElapsed <= now
-                        || stats.sessionCountInAllowedTime
-                        < mMaxSessionCountPerAllowedTime);
+        final boolean isUnderAllowedTimeQuota = (stats.sessionRateLimitExpirationTimeElapsed <= now
+                || stats.sessionCountInRateLimitingWindow < mMaxSessionCountPerRateLimitingWindow);
         return isUnderAllowedTimeQuota
                 && stats.sessionCountInWindow < mMaxBucketSessionCounts[standbyBucket];
     }
@@ -924,12 +946,18 @@
         }
         if (refreshStatsIfOld) {
             final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket];
+            final int jobCountLimit = mMaxBucketJobCounts[standbyBucket];
+            final int sessionCountLimit = mMaxBucketSessionCounts[standbyBucket];
             Timer timer = mPkgTimers.get(userId, packageName);
             if ((timer != null && timer.isActive())
                     || stats.expirationTimeElapsed <= sElapsedRealtimeClock.millis()
-                    || stats.windowSizeMs != bucketWindowSizeMs) {
+                    || stats.windowSizeMs != bucketWindowSizeMs
+                    || stats.jobCountLimit != jobCountLimit
+                    || stats.sessionCountLimit != sessionCountLimit) {
                 // The stats are no longer valid.
                 stats.windowSizeMs = bucketWindowSizeMs;
+                stats.jobCountLimit = jobCountLimit;
+                stats.sessionCountLimit = sessionCountLimit;
                 updateExecutionStatsLocked(userId, packageName, stats);
             }
         }
@@ -945,7 +973,7 @@
         stats.executionTimeInMaxPeriodMs = 0;
         stats.bgJobCountInMaxPeriod = 0;
         stats.sessionCountInWindow = 0;
-        stats.quotaCutoffTimeElapsed = 0;
+        stats.inQuotaTimeElapsed = 0;
 
         Timer timer = mPkgTimers.get(userId, packageName);
         final long nowElapsed = sElapsedRealtimeClock.millis();
@@ -958,12 +986,12 @@
             // invalidate now.
             stats.expirationTimeElapsed = nowElapsed;
             if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) {
-                stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
-                        nowElapsed - mAllowedTimeIntoQuotaMs);
+                stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
+                        nowElapsed - mAllowedTimeIntoQuotaMs + stats.windowSizeMs);
             }
             if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
-                stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
-                        nowElapsed - mMaxExecutionTimeIntoQuotaMs);
+                stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
+                        nowElapsed - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS);
             }
         }
 
@@ -985,36 +1013,40 @@
             TimingSession session = sessions.get(i);
 
             // Window management.
-            if (startWindowElapsed < session.startTimeElapsed) {
-                stats.executionTimeInWindowMs += session.endTimeElapsed - session.startTimeElapsed;
+            if (startWindowElapsed < session.endTimeElapsed) {
+                final long start;
+                if (startWindowElapsed < session.startTimeElapsed) {
+                    start = session.startTimeElapsed;
+                    emptyTimeMs =
+                            Math.min(emptyTimeMs, session.startTimeElapsed - startWindowElapsed);
+                } else {
+                    // The session started before the window but ended within the window. Only
+                    // include the portion that was within the window.
+                    start = startWindowElapsed;
+                    emptyTimeMs = 0;
+                }
+
+                stats.executionTimeInWindowMs += session.endTimeElapsed - start;
                 stats.bgJobCountInWindow += session.bgJobCount;
-                emptyTimeMs = Math.min(emptyTimeMs, session.startTimeElapsed - startWindowElapsed);
                 if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) {
-                    stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
-                            session.startTimeElapsed + stats.executionTimeInWindowMs
-                                    - mAllowedTimeIntoQuotaMs);
+                    stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
+                            start + stats.executionTimeInWindowMs - mAllowedTimeIntoQuotaMs
+                                    + stats.windowSizeMs);
+                }
+                if (stats.bgJobCountInWindow >= stats.jobCountLimit) {
+                    stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
+                            session.endTimeElapsed + stats.windowSizeMs);
                 }
                 if (i == loopStart
                         || (sessions.get(i + 1).startTimeElapsed - session.endTimeElapsed)
                                 > mTimingSessionCoalescingDurationMs) {
                     // Coalesce sessions if they are very close to each other in time
                     sessionCountInWindow++;
-                }
-            } else if (startWindowElapsed < session.endTimeElapsed) {
-                // The session started before the window but ended within the window. Only include
-                // the portion that was within the window.
-                stats.executionTimeInWindowMs += session.endTimeElapsed - startWindowElapsed;
-                stats.bgJobCountInWindow += session.bgJobCount;
-                emptyTimeMs = 0;
-                if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) {
-                    stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
-                            startWindowElapsed + stats.executionTimeInWindowMs
-                                    - mAllowedTimeIntoQuotaMs);
-                }
-                if (i == loopStart
-                        || (sessions.get(i + 1).startTimeElapsed - session.endTimeElapsed)
-                                > mTimingSessionCoalescingDurationMs) {
-                    sessionCountInWindow++;
+
+                    if (sessionCountInWindow >= stats.sessionCountLimit) {
+                        stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
+                                session.endTimeElapsed + stats.windowSizeMs);
+                    }
                 }
             }
 
@@ -1025,9 +1057,9 @@
                 stats.bgJobCountInMaxPeriod += session.bgJobCount;
                 emptyTimeMs = Math.min(emptyTimeMs, session.startTimeElapsed - startMaxElapsed);
                 if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
-                    stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
+                    stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
                             session.startTimeElapsed + stats.executionTimeInMaxPeriodMs
-                                    - mMaxExecutionTimeIntoQuotaMs);
+                                    - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS);
                 }
             } else if (startMaxElapsed < session.endTimeElapsed) {
                 // The session started before the window but ended within the window. Only include
@@ -1036,9 +1068,9 @@
                 stats.bgJobCountInMaxPeriod += session.bgJobCount;
                 emptyTimeMs = 0;
                 if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
-                    stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
+                    stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
                             startMaxElapsed + stats.executionTimeInMaxPeriodMs
-                                    - mMaxExecutionTimeIntoQuotaMs);
+                                    - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS);
                 }
             } else {
                 // This session ended before the window. No point in going any further.
@@ -1094,11 +1126,11 @@
                 stats = new ExecutionStats();
                 appStats[i] = stats;
             }
-            if (stats.jobCountExpirationTimeElapsed <= now) {
-                stats.jobCountExpirationTimeElapsed = now + mAllowedTimePerPeriodMs;
-                stats.jobCountInAllowedTime = 0;
+            if (stats.jobRateLimitExpirationTimeElapsed <= now) {
+                stats.jobRateLimitExpirationTimeElapsed = now + mRateLimitingWindowMs;
+                stats.jobCountInRateLimitingWindow = 0;
             }
-            stats.jobCountInAllowedTime += count;
+            stats.jobCountInRateLimitingWindow += count;
         }
     }
 
@@ -1115,11 +1147,11 @@
                 stats = new ExecutionStats();
                 appStats[i] = stats;
             }
-            if (stats.sessionCountExpirationTimeElapsed <= now) {
-                stats.sessionCountExpirationTimeElapsed = now + mAllowedTimePerPeriodMs;
-                stats.sessionCountInAllowedTime = 0;
+            if (stats.sessionRateLimitExpirationTimeElapsed <= now) {
+                stats.sessionRateLimitExpirationTimeElapsed = now + mRateLimitingWindowMs;
+                stats.sessionCountInRateLimitingWindow = 0;
             }
-            stats.sessionCountInAllowedTime++;
+            stats.sessionCountInRateLimitingWindow++;
         }
     }
 
@@ -1250,10 +1282,10 @@
                 // An app in the ACTIVE bucket may be out of quota while the job could be in quota
                 // for some reason. Therefore, avoid setting the real value here and check each job
                 // individually.
-                changed |= js.setQuotaConstraintSatisfied(realInQuota);
+                changed |= setConstraintSatisfied(js, realInQuota);
             } else {
                 // This job is somehow exempted. Need to determine its own quota status.
-                changed |= js.setQuotaConstraintSatisfied(isWithinQuotaLocked(js));
+                changed |= setConstraintSatisfied(js, isWithinQuotaLocked(js));
             }
         }
         if (!realInQuota) {
@@ -1278,7 +1310,7 @@
 
         @Override
         public void accept(JobStatus jobStatus) {
-            wasJobChanged |= jobStatus.setQuotaConstraintSatisfied(isWithinQuotaLocked(jobStatus));
+            wasJobChanged |= setConstraintSatisfied(jobStatus, isWithinQuotaLocked(jobStatus));
             final int userId = jobStatus.getSourceUserId();
             final String packageName = jobStatus.getSourcePackageName();
             final int realStandbyBucket = jobStatus.getStandbyBucket();
@@ -1367,18 +1399,17 @@
         }
 
         // The time this app will have quota again.
-        long inQuotaTimeElapsed = stats.quotaCutoffTimeElapsed + stats.windowSizeMs;
-        if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeMs) {
+        long inQuotaTimeElapsed = stats.inQuotaTimeElapsed;
+        if (!isUnderJobCountQuota && stats.bgJobCountInWindow < stats.jobCountLimit) {
+            // App hit the rate limit.
             inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed,
-                    stats.quotaCutoffTimeElapsed + MAX_PERIOD_MS);
+                    stats.jobRateLimitExpirationTimeElapsed);
         }
-        if (!isUnderJobCountQuota) {
+        if (!isUnderTimingSessionCountQuota
+                && stats.sessionCountInWindow < stats.sessionCountLimit) {
+            // App hit the rate limit.
             inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed,
-                    stats.jobCountExpirationTimeElapsed + mAllowedTimePerPeriodMs);
-        }
-        if (!isUnderTimingSessionCountQuota) {
-            inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed,
-                    stats.sessionCountExpirationTimeElapsed + mAllowedTimePerPeriodMs);
+                    stats.sessionRateLimitExpirationTimeElapsed);
         }
         // Only schedule the alarm if:
         // 1. There isn't one currently scheduled
@@ -1399,10 +1430,18 @@
                     ALARM_TAG_QUOTA_CHECK, alarmListener, mHandler);
             alarmListener.setTriggerTime(inQuotaTimeElapsed);
         } else if (DEBUG) {
-            Slog.d(TAG, "No need to scheduling start alarm for " + pkgString);
+            Slog.d(TAG, "No need to schedule start alarm for " + pkgString);
         }
     }
 
+    private boolean setConstraintSatisfied(@NonNull JobStatus jobStatus, boolean isWithinQuota) {
+        if (!isWithinQuota && jobStatus.getWhenStandbyDeferred() == 0) {
+            // Mark that the job is being deferred due to buckets.
+            jobStatus.setWhenStandbyDeferred(sElapsedRealtimeClock.millis());
+        }
+        return jobStatus.setQuotaConstraintSatisfied(isWithinQuota);
+    }
+
     private final class ChargingTracker extends BroadcastReceiver {
         /**
          * Track whether we're charging. This has a slightly different definition than that of
@@ -1968,14 +2007,15 @@
         private static final String KEY_MAX_JOB_COUNT_WORKING = "max_job_count_working";
         private static final String KEY_MAX_JOB_COUNT_FREQUENT = "max_job_count_frequent";
         private static final String KEY_MAX_JOB_COUNT_RARE = "max_job_count_rare";
-        private static final String KEY_MAX_JOB_COUNT_PER_ALLOWED_TIME =
-                "max_count_per_allowed_time";
+        private static final String KEY_RATE_LIMITING_WINDOW_MS = "rate_limiting_window_ms";
+        private static final String KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW =
+                "max_job_count_per_rate_limiting_window";
         private static final String KEY_MAX_SESSION_COUNT_ACTIVE = "max_session_count_active";
         private static final String KEY_MAX_SESSION_COUNT_WORKING = "max_session_count_working";
         private static final String KEY_MAX_SESSION_COUNT_FREQUENT = "max_session_count_frequent";
         private static final String KEY_MAX_SESSION_COUNT_RARE = "max_session_count_rare";
-        private static final String KEY_MAX_SESSION_COUNT_PER_ALLOWED_TIME =
-                "max_session_count_per_allowed_time";
+        private static final String KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW =
+                "max_session_count_per_rate_limiting_window";
         private static final String KEY_TIMING_SESSION_COALESCING_DURATION_MS =
                 "timing_session_coalescing_duration_ms";
 
@@ -1984,7 +2024,7 @@
         private static final long DEFAULT_IN_QUOTA_BUFFER_MS =
                 30 * 1000L; // 30 seconds
         private static final long DEFAULT_WINDOW_SIZE_ACTIVE_MS =
-                10 * 60 * 1000L; // 10 minutes for ACTIVE -- ACTIVE apps can run jobs at any time
+                DEFAULT_ALLOWED_TIME_PER_PERIOD_MS; // ACTIVE apps can run jobs at any time
         private static final long DEFAULT_WINDOW_SIZE_WORKING_MS =
                 2 * 60 * 60 * 1000L; // 2 hours
         private static final long DEFAULT_WINDOW_SIZE_FREQUENT_MS =
@@ -1992,16 +2032,18 @@
         private static final long DEFAULT_WINDOW_SIZE_RARE_MS =
                 24 * 60 * 60 * 1000L; // 24 hours
         private static final long DEFAULT_MAX_EXECUTION_TIME_MS =
-                4 * 60 * 60 * 1000L; // 4 hours
-        private static final int DEFAULT_MAX_JOB_COUNT_ACTIVE =
-                200; // 1200/hr
-        private static final int DEFAULT_MAX_JOB_COUNT_WORKING =
-                1200; // 600/hr
-        private static final int DEFAULT_MAX_JOB_COUNT_FREQUENT =
-                1800; // 225/hr
-        private static final int DEFAULT_MAX_JOB_COUNT_RARE =
-                2400; // 100/hr
-        private static final int DEFAULT_MAX_JOB_COUNT_PER_ALLOWED_TIME = 20;
+                4 * HOUR_IN_MILLIS;
+        private static final long DEFAULT_RATE_LIMITING_WINDOW_MS =
+                10 * MINUTE_IN_MILLIS;
+        private static final int DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 20;
+        private static final int DEFAULT_MAX_JOB_COUNT_ACTIVE = // 20/window = 120/hr = 1/session
+                DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
+        private static final int DEFAULT_MAX_JOB_COUNT_WORKING = // 120/window = 60/hr = 12/session
+                (int) (60.0 * DEFAULT_WINDOW_SIZE_WORKING_MS / HOUR_IN_MILLIS);
+        private static final int DEFAULT_MAX_JOB_COUNT_FREQUENT = // 200/window = 25/hr = 25/session
+                (int) (25.0 * DEFAULT_WINDOW_SIZE_FREQUENT_MS / HOUR_IN_MILLIS);
+        private static final int DEFAULT_MAX_JOB_COUNT_RARE = // 48/window = 2/hr = 16/session
+                (int) (2.0 * DEFAULT_WINDOW_SIZE_RARE_MS / HOUR_IN_MILLIS);
         private static final int DEFAULT_MAX_SESSION_COUNT_ACTIVE =
                 20; // 120/hr
         private static final int DEFAULT_MAX_SESSION_COUNT_WORKING =
@@ -2010,8 +2052,8 @@
                 8; // 1/hr
         private static final int DEFAULT_MAX_SESSION_COUNT_RARE =
                 3; // .125/hr
-        private static final int DEFAULT_MAX_SESSION_COUNT_PER_ALLOWED_TIME = 20;
-        private static final long DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS = 0;
+        private static final int DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 20;
+        private static final long DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS = 5000; // 5 seconds
 
         /** How much time each app will have to run jobs within their standby bucket window. */
         public long ALLOWED_TIME_PER_PERIOD_MS = DEFAULT_ALLOWED_TIME_PER_PERIOD_MS;
@@ -2079,11 +2121,14 @@
          */
         public int MAX_JOB_COUNT_RARE = DEFAULT_MAX_JOB_COUNT_RARE;
 
+        /** The period of time used to rate limit recently run jobs. */
+        public long RATE_LIMITING_WINDOW_MS = DEFAULT_RATE_LIMITING_WINDOW_MS;
+
         /**
-         * The maximum number of jobs that can run within the past
-         * {@link #ALLOWED_TIME_PER_PERIOD_MS}.
+         * The maximum number of jobs that can run within the past {@link #RATE_LIMITING_WINDOW_MS}.
          */
-        public int MAX_JOB_COUNT_PER_ALLOWED_TIME = DEFAULT_MAX_JOB_COUNT_PER_ALLOWED_TIME;
+        public int MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW =
+                DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
 
         /**
          * The maximum number of {@link TimingSession}s an app can run within this particular
@@ -2113,7 +2158,8 @@
          * The maximum number of {@link TimingSession}s that can run within the past
          * {@link #ALLOWED_TIME_PER_PERIOD_MS}.
          */
-        public int MAX_SESSION_COUNT_PER_ALLOWED_TIME = DEFAULT_MAX_SESSION_COUNT_PER_ALLOWED_TIME;
+        public int MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW =
+                DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW;
 
         /**
          * Treat two distinct {@link TimingSession}s as the same if they start and end within this
@@ -2122,6 +2168,29 @@
         public long TIMING_SESSION_COALESCING_DURATION_MS =
                 DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS;
 
+        // Safeguards
+
+        /** The minimum number of jobs that any bucket will be allowed to run within its window. */
+        private static final int MIN_BUCKET_JOB_COUNT = 10;
+
+        /**
+         * The minimum number of {@link TimingSession}s that any bucket will be allowed to run
+         * within its window.
+         */
+        private static final int MIN_BUCKET_SESSION_COUNT = 1;
+
+        /** The minimum value that {@link #MAX_EXECUTION_TIME_MS} can have. */
+        private static final long MIN_MAX_EXECUTION_TIME_MS = 60 * MINUTE_IN_MILLIS;
+
+        /** The minimum value that {@link #MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW} can have. */
+        private static final int MIN_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 10;
+
+        /** The minimum value that {@link #MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW} can have. */
+        private static final int MIN_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 10;
+
+        /** The minimum value that {@link #RATE_LIMITING_WINDOW_MS} can have. */
+        private static final long MIN_RATE_LIMITING_WINDOW_MS = 30 * SECOND_IN_MILLIS;
+
         QcConstants(Handler handler) {
             super(handler);
         }
@@ -2167,8 +2236,11 @@
                     KEY_MAX_JOB_COUNT_FREQUENT, DEFAULT_MAX_JOB_COUNT_FREQUENT);
             MAX_JOB_COUNT_RARE = mParser.getInt(
                     KEY_MAX_JOB_COUNT_RARE, DEFAULT_MAX_JOB_COUNT_RARE);
-            MAX_JOB_COUNT_PER_ALLOWED_TIME = mParser.getInt(
-                    KEY_MAX_JOB_COUNT_PER_ALLOWED_TIME, DEFAULT_MAX_JOB_COUNT_PER_ALLOWED_TIME);
+            RATE_LIMITING_WINDOW_MS = mParser.getLong(
+                    KEY_RATE_LIMITING_WINDOW_MS, DEFAULT_RATE_LIMITING_WINDOW_MS);
+            MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = mParser.getInt(
+                    KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
+                    DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW);
             MAX_SESSION_COUNT_ACTIVE = mParser.getInt(
                     KEY_MAX_SESSION_COUNT_ACTIVE, DEFAULT_MAX_SESSION_COUNT_ACTIVE);
             MAX_SESSION_COUNT_WORKING = mParser.getInt(
@@ -2177,9 +2249,9 @@
                     KEY_MAX_SESSION_COUNT_FREQUENT, DEFAULT_MAX_SESSION_COUNT_FREQUENT);
             MAX_SESSION_COUNT_RARE = mParser.getInt(
                     KEY_MAX_SESSION_COUNT_RARE, DEFAULT_MAX_SESSION_COUNT_RARE);
-            MAX_SESSION_COUNT_PER_ALLOWED_TIME = mParser.getInt(
-                    KEY_MAX_SESSION_COUNT_PER_ALLOWED_TIME,
-                    DEFAULT_MAX_SESSION_COUNT_PER_ALLOWED_TIME);
+            MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = mParser.getInt(
+                    KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
+                    DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW);
             TIMING_SESSION_COALESCING_DURATION_MS = mParser.getLong(
                     KEY_TIMING_SESSION_COALESCING_DURATION_MS,
                     DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS);
@@ -2192,7 +2264,14 @@
             synchronized (mLock) {
                 boolean changed = false;
 
-                long newAllowedTimeMs = Math.min(MAX_PERIOD_MS,
+                long newMaxExecutionTimeMs = Math.max(MIN_MAX_EXECUTION_TIME_MS,
+                        Math.min(MAX_PERIOD_MS, MAX_EXECUTION_TIME_MS));
+                if (mMaxExecutionTimeMs != newMaxExecutionTimeMs) {
+                    mMaxExecutionTimeMs = newMaxExecutionTimeMs;
+                    mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
+                    changed = true;
+                }
+                long newAllowedTimeMs = Math.min(mMaxExecutionTimeMs,
                         Math.max(MINUTE_IN_MILLIS, ALLOWED_TIME_PER_PERIOD_MS));
                 if (mAllowedTimePerPeriodMs != newAllowedTimeMs) {
                     mAllowedTimePerPeriodMs = newAllowedTimeMs;
@@ -2231,47 +2310,44 @@
                     mBucketPeriodsMs[RARE_INDEX] = newRarePeriodMs;
                     changed = true;
                 }
-                long newMaxExecutionTimeMs = Math.max(60 * MINUTE_IN_MILLIS,
-                        Math.min(MAX_PERIOD_MS, MAX_EXECUTION_TIME_MS));
-                if (mMaxExecutionTimeMs != newMaxExecutionTimeMs) {
-                    mMaxExecutionTimeMs = newMaxExecutionTimeMs;
-                    mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
+                long newRateLimitingWindowMs = Math.min(MAX_PERIOD_MS,
+                        Math.max(MIN_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS));
+                if (mRateLimitingWindowMs != newRateLimitingWindowMs) {
+                    mRateLimitingWindowMs = newRateLimitingWindowMs;
                     changed = true;
                 }
-                int newMaxCountPerAllowedPeriod = Math.max(10,
-                        MAX_JOB_COUNT_PER_ALLOWED_TIME);
-                if (mMaxJobCountPerAllowedTime != newMaxCountPerAllowedPeriod) {
-                    mMaxJobCountPerAllowedTime = newMaxCountPerAllowedPeriod;
+                int newMaxJobCountPerRateLimitingWindow = Math.max(
+                        MIN_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
+                        MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW);
+                if (mMaxJobCountPerRateLimitingWindow != newMaxJobCountPerRateLimitingWindow) {
+                    mMaxJobCountPerRateLimitingWindow = newMaxJobCountPerRateLimitingWindow;
                     changed = true;
                 }
-                int newActiveMaxJobCount = Math.max(mMaxJobCountPerAllowedTime,
-                        Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_ACTIVE));
+                int newActiveMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_ACTIVE);
                 if (mMaxBucketJobCounts[ACTIVE_INDEX] != newActiveMaxJobCount) {
                     mMaxBucketJobCounts[ACTIVE_INDEX] = newActiveMaxJobCount;
                     changed = true;
                 }
-                int newWorkingMaxJobCount = Math.max(mMaxJobCountPerAllowedTime,
-                        Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_WORKING));
+                int newWorkingMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_WORKING);
                 if (mMaxBucketJobCounts[WORKING_INDEX] != newWorkingMaxJobCount) {
                     mMaxBucketJobCounts[WORKING_INDEX] = newWorkingMaxJobCount;
                     changed = true;
                 }
-                int newFrequentMaxJobCount = Math.max(mMaxJobCountPerAllowedTime,
-                        Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_FREQUENT));
+                int newFrequentMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_FREQUENT);
                 if (mMaxBucketJobCounts[FREQUENT_INDEX] != newFrequentMaxJobCount) {
                     mMaxBucketJobCounts[FREQUENT_INDEX] = newFrequentMaxJobCount;
                     changed = true;
                 }
-                int newRareMaxJobCount = Math.max(mMaxJobCountPerAllowedTime,
-                        Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_RARE));
+                int newRareMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_RARE);
                 if (mMaxBucketJobCounts[RARE_INDEX] != newRareMaxJobCount) {
                     mMaxBucketJobCounts[RARE_INDEX] = newRareMaxJobCount;
                     changed = true;
                 }
-                int newMaxSessionCountPerAllowedPeriod = Math.max(10,
-                        MAX_SESSION_COUNT_PER_ALLOWED_TIME);
-                if (mMaxSessionCountPerAllowedTime != newMaxSessionCountPerAllowedPeriod) {
-                    mMaxSessionCountPerAllowedTime = newMaxSessionCountPerAllowedPeriod;
+                int newMaxSessionCountPerRateLimitPeriod = Math.max(
+                        MIN_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
+                        MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW);
+                if (mMaxSessionCountPerRateLimitingWindow != newMaxSessionCountPerRateLimitPeriod) {
+                    mMaxSessionCountPerRateLimitingWindow = newMaxSessionCountPerRateLimitPeriod;
                     changed = true;
                 }
                 int newActiveMaxSessionCount =
@@ -2332,14 +2408,15 @@
             pw.printPair(KEY_MAX_JOB_COUNT_WORKING, MAX_JOB_COUNT_WORKING).println();
             pw.printPair(KEY_MAX_JOB_COUNT_FREQUENT, MAX_JOB_COUNT_FREQUENT).println();
             pw.printPair(KEY_MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE).println();
-            pw.printPair(KEY_MAX_JOB_COUNT_PER_ALLOWED_TIME, MAX_JOB_COUNT_PER_ALLOWED_TIME)
-                    .println();
+            pw.printPair(KEY_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS).println();
+            pw.printPair(KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
+                    MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW).println();
             pw.printPair(KEY_MAX_SESSION_COUNT_ACTIVE, MAX_SESSION_COUNT_ACTIVE).println();
             pw.printPair(KEY_MAX_SESSION_COUNT_WORKING, MAX_SESSION_COUNT_WORKING).println();
             pw.printPair(KEY_MAX_SESSION_COUNT_FREQUENT, MAX_SESSION_COUNT_FREQUENT).println();
             pw.printPair(KEY_MAX_SESSION_COUNT_RARE, MAX_SESSION_COUNT_RARE).println();
-            pw.printPair(KEY_MAX_SESSION_COUNT_PER_ALLOWED_TIME, MAX_SESSION_COUNT_PER_ALLOWED_TIME)
-                    .println();
+            pw.printPair(KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
+                    MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW).println();
             pw.printPair(KEY_TIMING_SESSION_COALESCING_DURATION_MS,
                     TIMING_SESSION_COALESCING_DURATION_MS).println();
             pw.decreaseIndent();
@@ -2365,8 +2442,10 @@
             proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_FREQUENT,
                     MAX_JOB_COUNT_FREQUENT);
             proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE);
-            proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_PER_ALLOWED_TIME,
-                    MAX_JOB_COUNT_PER_ALLOWED_TIME);
+            proto.write(ConstantsProto.QuotaController.RATE_LIMITING_WINDOW_MS,
+                    RATE_LIMITING_WINDOW_MS);
+            proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
+                    MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW);
             proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_ACTIVE,
                     MAX_SESSION_COUNT_ACTIVE);
             proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_WORKING,
@@ -2375,8 +2454,8 @@
                     MAX_SESSION_COUNT_FREQUENT);
             proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_RARE,
                     MAX_SESSION_COUNT_RARE);
-            proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_PER_ALLOWED_TIME,
-                    MAX_SESSION_COUNT_PER_ALLOWED_TIME);
+            proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
+                    MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW);
             proto.write(ConstantsProto.QuotaController.TIMING_SESSION_COALESCING_DURATION_MS,
                     TIMING_SESSION_COALESCING_DURATION_MS);
             proto.end(qcToken);
@@ -2431,8 +2510,18 @@
     }
 
     @VisibleForTesting
-    int getMaxJobCountPerAllowedTime() {
-        return mMaxJobCountPerAllowedTime;
+    int getMaxJobCountPerRateLimitingWindow() {
+        return mMaxJobCountPerRateLimitingWindow;
+    }
+
+    @VisibleForTesting
+    int getMaxSessionCountPerRateLimitingWindow() {
+        return mMaxSessionCountPerRateLimitingWindow;
+    }
+
+    @VisibleForTesting
+    long getRateLimitingWindowMs() {
+        return mRateLimitingWindowMs;
     }
 
     @VisibleForTesting
@@ -2441,11 +2530,6 @@
     }
 
     @VisibleForTesting
-    int getMaxSessionCountPerAllowedTime() {
-        return mMaxSessionCountPerAllowedTime;
-    }
-
-    @VisibleForTesting
     @Nullable
     List<TimingSession> getTimingSessions(int userId, String packageName) {
         return mTimingSessions.get(userId, packageName);
@@ -2659,6 +2743,12 @@
                                 StateControllerProto.QuotaController.ExecutionStats.WINDOW_SIZE_MS,
                                 es.windowSizeMs);
                         proto.write(
+                                StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_LIMIT,
+                                es.jobCountLimit);
+                        proto.write(
+                                StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_LIMIT,
+                                es.sessionCountLimit);
+                        proto.write(
                                 StateControllerProto.QuotaController.ExecutionStats.EXECUTION_TIME_IN_WINDOW_MS,
                                 es.executionTimeInWindowMs);
                         proto.write(
@@ -2674,20 +2764,20 @@
                                 StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_WINDOW,
                                 es.sessionCountInWindow);
                         proto.write(
-                                StateControllerProto.QuotaController.ExecutionStats.QUOTA_CUTOFF_TIME_ELAPSED,
-                                es.quotaCutoffTimeElapsed);
+                                StateControllerProto.QuotaController.ExecutionStats.IN_QUOTA_TIME_ELAPSED,
+                                es.inQuotaTimeElapsed);
                         proto.write(
                                 StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_EXPIRATION_TIME_ELAPSED,
-                                es.jobCountExpirationTimeElapsed);
+                                es.jobRateLimitExpirationTimeElapsed);
                         proto.write(
-                                StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_IN_ALLOWED_TIME,
-                                es.jobCountInAllowedTime);
+                                StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_IN_RATE_LIMITING_WINDOW,
+                                es.jobCountInRateLimitingWindow);
                         proto.write(
                                 StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_EXPIRATION_TIME_ELAPSED,
-                                es.sessionCountExpirationTimeElapsed);
+                                es.sessionRateLimitExpirationTimeElapsed);
                         proto.write(
-                                StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_ALLOWED_TIME,
-                                es.sessionCountInAllowedTime);
+                                StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_RATE_LIMITING_WINDOW,
+                                es.sessionCountInRateLimitingWindow);
                         proto.end(esToken);
                     }
                 }
diff --git a/services/core/java/com/android/server/location/GnssVisibilityControl.java b/services/core/java/com/android/server/location/GnssVisibilityControl.java
index 8d4ad7f..65bd5c6 100644
--- a/services/core/java/com/android/server/location/GnssVisibilityControl.java
+++ b/services/core/java/com/android/server/location/GnssVisibilityControl.java
@@ -456,9 +456,8 @@
         final String proxyAppPkgName = nfwNotification.mProxyAppPackageName;
         final ProxyAppState proxyAppState = mProxyAppsState.get(proxyAppPkgName);
         final boolean isLocationRequestAccepted = nfwNotification.isRequestAccepted();
-        final boolean isPermissionMismatched =
-                (proxyAppState == null) ? isLocationRequestAccepted
-                        : (proxyAppState.mHasLocationPermission != isLocationRequestAccepted);
+        final boolean isPermissionMismatched = isPermissionMismatched(proxyAppState,
+                nfwNotification);
         logEvent(nfwNotification, isPermissionMismatched);
 
         if (!nfwNotification.isRequestAttributedToProxyApp()) {
@@ -506,14 +505,24 @@
 
         // Log proxy app permission mismatch between framework and GNSS HAL.
         if (isPermissionMismatched) {
-            Log.w(TAG, "Permission mismatch. Framework proxy app " + proxyAppPkgName
+            Log.w(TAG, "Permission mismatch. Proxy app " + proxyAppPkgName
                     + " location permission is set to " + proxyAppState.mHasLocationPermission
+                    + " and GNSS HAL enabled is set to " + mIsGpsEnabled
                     + " but GNSS non-framework location access response type is "
                     + nfwNotification.getResponseTypeAsString() + " for notification: "
                     + nfwNotification);
         }
     }
 
+    private boolean isPermissionMismatched(ProxyAppState proxyAppState,
+            NfwNotification nfwNotification) {
+        // Non-framework non-emergency location requests must be accepted only when IGnss.hal
+        // is enabled and the proxy app has location permission.
+        final boolean isLocationRequestAccepted = nfwNotification.isRequestAccepted();
+        return (proxyAppState == null || !mIsGpsEnabled) ? isLocationRequestAccepted
+                        : (proxyAppState.mHasLocationPermission != isLocationRequestAccepted);
+    }
+
     private void showLocationIcon(ProxyAppState proxyAppState, NfwNotification nfwNotification,
             int uid, String proxyAppPkgName) {
         // If we receive a new NfwNotification before the location icon is turned off for the
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index 19ff2c1..6c34e13 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -217,6 +217,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.internal.notification.SystemNotificationChannels;
+import com.android.internal.os.RoSystemProperties;
 import com.android.internal.telephony.PhoneConstants;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.ConcurrentUtils;
@@ -1353,9 +1354,15 @@
                         mContext, 0, snoozeIntent, PendingIntent.FLAG_UPDATE_CURRENT));
 
                 final Intent viewIntent = buildViewDataUsageIntent(res, policy.template);
-                builder.setContentIntent(PendingIntent.getActivity(
-                        mContext, 0, viewIntent, PendingIntent.FLAG_UPDATE_CURRENT));
-
+                // TODO: Resolve to single code path.
+                if (isHeadlessSystemUserBuild()) {
+                    builder.setContentIntent(PendingIntent.getActivityAsUser(
+                            mContext, 0, viewIntent, PendingIntent.FLAG_UPDATE_CURRENT,
+                            /* options= */ null, UserHandle.CURRENT));
+                } else {
+                    builder.setContentIntent(PendingIntent.getActivity(
+                            mContext, 0, viewIntent, PendingIntent.FLAG_UPDATE_CURRENT));
+                }
                 break;
             }
             case TYPE_LIMIT: {
@@ -1375,8 +1382,15 @@
                 builder.setSmallIcon(R.drawable.stat_notify_disabled_data);
 
                 final Intent intent = buildNetworkOverLimitIntent(res, policy.template);
-                builder.setContentIntent(PendingIntent.getActivity(
-                        mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
+                // TODO: Resolve to single code path.
+                if (isHeadlessSystemUserBuild()) {
+                    builder.setContentIntent(PendingIntent.getActivityAsUser(
+                            mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT,
+                            /* options= */ null, UserHandle.CURRENT));
+                } else {
+                    builder.setContentIntent(PendingIntent.getActivity(
+                            mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
+                }
                 break;
             }
             case TYPE_LIMIT_SNOOZED: {
@@ -1399,8 +1413,15 @@
                 builder.setChannelId(SystemNotificationChannels.NETWORK_STATUS);
 
                 final Intent intent = buildViewDataUsageIntent(res, policy.template);
-                builder.setContentIntent(PendingIntent.getActivity(
-                        mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
+                // TODO: Resolve to single code path.
+                if (isHeadlessSystemUserBuild()) {
+                    builder.setContentIntent(PendingIntent.getActivityAsUser(
+                            mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT,
+                            /* options= */ null, UserHandle.CURRENT));
+                } else {
+                    builder.setContentIntent(PendingIntent.getActivity(
+                            mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
+                }
                 break;
             }
             case TYPE_RAPID: {
@@ -1419,8 +1440,15 @@
                         mContext, 0, snoozeIntent, PendingIntent.FLAG_UPDATE_CURRENT));
 
                 final Intent viewIntent = buildViewDataUsageIntent(res, policy.template);
-                builder.setContentIntent(PendingIntent.getActivity(
-                        mContext, 0, viewIntent, PendingIntent.FLAG_UPDATE_CURRENT));
+                // TODO: Resolve to single code path.
+                if (isHeadlessSystemUserBuild()) {
+                    builder.setContentIntent(PendingIntent.getActivityAsUser(
+                            mContext, 0, viewIntent, PendingIntent.FLAG_UPDATE_CURRENT,
+                            /* options= */ null, UserHandle.CURRENT));
+                } else {
+                    builder.setContentIntent(PendingIntent.getActivity(
+                            mContext, 0, viewIntent, PendingIntent.FLAG_UPDATE_CURRENT));
+                }
                 break;
             }
             default: {
@@ -5264,6 +5292,10 @@
         return (bundle != null) ? bundle.getBoolean(key, defaultValue) : defaultValue;
     }
 
+    private static boolean isHeadlessSystemUserBuild() {
+        return RoSystemProperties.MULTIUSER_HEADLESS_SYSTEM_USER;
+    }
+
     private class NotificationId {
         private final String mTag;
         private final int mId;
diff --git a/services/core/java/com/android/server/net/NetworkStatsAccess.java b/services/core/java/com/android/server/net/NetworkStatsAccess.java
index cebc472..7c1c1c7 100644
--- a/services/core/java/com/android/server/net/NetworkStatsAccess.java
+++ b/services/core/java/com/android/server/net/NetworkStatsAccess.java
@@ -109,7 +109,7 @@
         final TelephonyManager tm = (TelephonyManager)
                 context.getSystemService(Context.TELEPHONY_SERVICE);
         boolean hasCarrierPrivileges = tm != null &&
-                tm.checkCarrierPrivilegesForPackage(callingPackage) ==
+                tm.checkCarrierPrivilegesForPackageAnyPhone(callingPackage) ==
                         TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
         boolean isDeviceOwner = dpmi != null && dpmi.isActiveAdminWithPolicy(callingUid,
                 DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
diff --git a/services/core/java/com/android/server/net/NetworkStatsFactory.java b/services/core/java/com/android/server/net/NetworkStatsFactory.java
index 69efd02..473cc97 100644
--- a/services/core/java/com/android/server/net/NetworkStatsFactory.java
+++ b/services/core/java/com/android/server/net/NetworkStatsFactory.java
@@ -263,6 +263,10 @@
         return stats;
     }
 
+    /**
+     * @deprecated Use NetworkStatsService#getDetailedUidStats which also accounts for
+     * VPN traffic
+     */
     public NetworkStats readNetworkStatsDetail() throws IOException {
         return readNetworkStatsDetail(UID_ALL, null, TAG_ALL, null);
     }
diff --git a/services/core/java/com/android/server/net/NetworkStatsRecorder.java b/services/core/java/com/android/server/net/NetworkStatsRecorder.java
index a2e7e0c..bdff500 100644
--- a/services/core/java/com/android/server/net/NetworkStatsRecorder.java
+++ b/services/core/java/com/android/server/net/NetworkStatsRecorder.java
@@ -41,10 +41,10 @@
 import com.android.internal.util.FileRotator;
 import com.android.internal.util.IndentingPrintWriter;
 
-import libcore.io.IoUtils;
-
 import com.google.android.collect.Sets;
 
+import libcore.io.IoUtils;
+
 import java.io.ByteArrayOutputStream;
 import java.io.DataOutputStream;
 import java.io.File;
@@ -234,7 +234,7 @@
 
         if (vpnArray != null) {
             for (VpnInfo info : vpnArray) {
-                delta.migrateTun(info.ownerUid, info.vpnIface, info.primaryUnderlyingIface);
+                delta.migrateTun(info.ownerUid, info.vpnIface, info.underlyingIfaces);
             }
         }
 
diff --git a/services/core/java/com/android/server/net/NetworkStatsService.java b/services/core/java/com/android/server/net/NetworkStatsService.java
index f34ace5..a13368f 100644
--- a/services/core/java/com/android/server/net/NetworkStatsService.java
+++ b/services/core/java/com/android/server/net/NetworkStatsService.java
@@ -293,6 +293,22 @@
     /** Data layer operation counters for splicing into other structures. */
     private NetworkStats mUidOperations = new NetworkStats(0L, 10);
 
+    /**
+     * Snapshot containing most recent network stats for all UIDs across all interfaces and tags
+     * since boot.
+     *
+     * <p>Maintains migrated VPN stats which are result of performing TUN migration on {@link
+     * #mLastUidDetailSnapshot}.
+     */
+    @GuardedBy("mStatsLock")
+    private NetworkStats mTunAdjustedStats;
+    /**
+     * Used by {@link #mTunAdjustedStats} to migrate VPN traffic over delta between this snapshot
+     * and latest snapshot.
+     */
+    @GuardedBy("mStatsLock")
+    private NetworkStats mLastUidDetailSnapshot;
+
     /** Must be set in factory by calling #setHandler. */
     private Handler mHandler;
     private Handler.Callback mHandlerCallback;
@@ -812,15 +828,39 @@
     @Override
     public NetworkStats getDetailedUidStats(String[] requiredIfaces) {
         try {
+            // Get the latest snapshot from NetworkStatsFactory.
+            // TODO: Querying for INTERFACES_ALL may incur performance penalty. Consider restricting
+            // this to limited set of ifaces.
+            NetworkStats uidDetailStats = getNetworkStatsUidDetail(INTERFACES_ALL);
+
+            // Migrate traffic from VPN UID over delta and update mTunAdjustedStats.
+            NetworkStats result;
+            synchronized (mStatsLock) {
+                migrateTunTraffic(uidDetailStats, mVpnInfos);
+                result = mTunAdjustedStats.clone();
+            }
+
+            // Apply filter based on ifacesToQuery.
             final String[] ifacesToQuery =
                     NetworkStatsFactory.augmentWithStackedInterfaces(requiredIfaces);
-            return getNetworkStatsUidDetail(ifacesToQuery);
+            result.filter(UID_ALL, ifacesToQuery, TAG_ALL);
+            return result;
         } catch (RemoteException e) {
             Log.wtf(TAG, "Error compiling UID stats", e);
             return new NetworkStats(0L, 0);
         }
     }
 
+    @VisibleForTesting
+    NetworkStats getTunAdjustedStats() {
+        synchronized (mStatsLock) {
+            if (mTunAdjustedStats == null) {
+                return null;
+            }
+            return mTunAdjustedStats.clone();
+        }
+    }
+
     @Override
     public String[] getMobileIfaces() {
         return mMobileIfaces;
@@ -1295,6 +1335,34 @@
         // a race condition between the service handler thread and the observer's
         mStatsObservers.updateStats(xtSnapshot, uidSnapshot, new ArrayMap<>(mActiveIfaces),
                 new ArrayMap<>(mActiveUidIfaces), vpnArray, currentTime);
+
+        migrateTunTraffic(uidSnapshot, vpnArray);
+    }
+
+    /**
+     * Updates {@link #mTunAdjustedStats} with the delta containing traffic migrated off of VPNs.
+     */
+    @GuardedBy("mStatsLock")
+    private void migrateTunTraffic(NetworkStats uidDetailStats, VpnInfo[] vpnInfoArray) {
+        if (mTunAdjustedStats == null) {
+            // Either device booted or system server restarted, hence traffic cannot be migrated
+            // correctly without knowing the past state of VPN's underlying networks.
+            mTunAdjustedStats = uidDetailStats;
+            mLastUidDetailSnapshot = uidDetailStats;
+            return;
+        }
+        // Migrate delta traffic from VPN to other apps.
+        NetworkStats delta = uidDetailStats.subtract(mLastUidDetailSnapshot);
+        for (VpnInfo info : vpnInfoArray) {
+            delta.migrateTun(info.ownerUid, info.vpnIface, info.underlyingIfaces);
+        }
+        // Filter out debug entries as that may lead to over counting.
+        delta.filterDebugEntries();
+        // Update #mTunAdjustedStats with migrated delta.
+        mTunAdjustedStats.combineAllValues(delta);
+        mTunAdjustedStats.setElapsedRealtime(uidDetailStats.getElapsedRealtime());
+        // Update last snapshot.
+        mLastUidDetailSnapshot = uidDetailStats;
     }
 
     /**
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 1160e33..8485f46 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -283,7 +283,7 @@
     static final boolean DEBUG_INTERRUPTIVENESS = SystemProperties.getBoolean(
             "debug.notification.interruptiveness", false);
 
-    static final int MAX_PACKAGE_NOTIFICATIONS = 50;
+    static final int MAX_PACKAGE_NOTIFICATIONS = 25;
     static final float DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE = 5f;
 
     // message codes
@@ -1036,19 +1036,12 @@
                     final StatusBarNotification n = r.sbn;
                     final int callingUid = n.getUid();
                     final String pkg = n.getPackageName();
-                    final boolean wasBubble = r.getNotification().isBubbleNotification();
                     if (isBubble && isNotificationAppropriateToBubble(r, pkg, callingUid,
                             null /* oldEntry */)) {
                         r.getNotification().flags |= FLAG_BUBBLE;
                     } else {
                         r.getNotification().flags &= ~FLAG_BUBBLE;
                     }
-                    if (wasBubble != r.getNotification().isBubbleNotification()) {
-                        // Add the "alert only once" flag so that the notification won't HUN
-                        // unnecessarily just because the bubble flag was changed.
-                        r.getNotification().flags |= FLAG_ONLY_ALERT_ONCE;
-                        mListeners.notifyPostedLocked(r, r);
-                    }
                 }
             }
         }
@@ -2643,18 +2636,25 @@
                 ParceledListSlice channelsList) {
             List<NotificationChannel> channels = channelsList.getList();
             final int channelsSize = channels.size();
+            boolean needsPolicyFileChange = false;
             for (int i = 0; i < channelsSize; i++) {
                 final NotificationChannel channel = channels.get(i);
                 Preconditions.checkNotNull(channel, "channel in list is null");
-                mPreferencesHelper.createNotificationChannel(pkg, uid, channel,
-                        true /* fromTargetApp */, mConditionProviders.isPackageOrComponentAllowed(
+                needsPolicyFileChange = mPreferencesHelper.createNotificationChannel(pkg, uid,
+                        channel, true /* fromTargetApp */,
+                        mConditionProviders.isPackageOrComponentAllowed(
                                 pkg, UserHandle.getUserId(uid)));
-                mListeners.notifyNotificationChannelChanged(pkg,
-                        UserHandle.getUserHandleForUid(uid),
-                        mPreferencesHelper.getNotificationChannel(pkg, uid, channel.getId(), false),
-                        NOTIFICATION_CHANNEL_OR_GROUP_ADDED);
+                if (needsPolicyFileChange) {
+                    mListeners.notifyNotificationChannelChanged(pkg,
+                            UserHandle.getUserHandleForUid(uid),
+                            mPreferencesHelper.getNotificationChannel(pkg, uid, channel.getId(),
+                                    false),
+                            NOTIFICATION_CHANNEL_OR_GROUP_ADDED);
+                }
             }
-            handleSavePolicyFile();
+            if (needsPolicyFileChange) {
+                handleSavePolicyFile();
+            }
         }
 
         @Override
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index 4ed24ec..c2e559a 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -174,6 +174,7 @@
     private ArrayList<CharSequence> mSmartReplies;
 
     private final List<Adjustment> mAdjustments;
+    private String mAdjustmentIssuer;
     private final NotificationStats mStats;
     private int mUserSentiment;
     private boolean mIsInterruptive;
@@ -684,6 +685,9 @@
                     importance = Math.min(IMPORTANCE_HIGH, importance);
                     setAssistantImportance(importance);
                 }
+                if (!signals.isEmpty() && adjustment.getIssuer() != null) {
+                    mAdjustmentIssuer = adjustment.getIssuer();
+                }
             }
             // We have now gotten all the information out of the adjustments and can forget them.
             mAdjustments.clear();
@@ -1297,6 +1301,13 @@
             lm.addTaggedData(MetricsEvent.FIELD_NOTIFICATION_IMPORTANCE_ASST,
                         mAssistantImportance);
         }
+        // Log the issuer of any adjustments that may have affected this notification. We only log
+        // the hash here as NotificationItem events are frequent, and the number of NAS
+        // implementations (and hence the chance of collisions) is low.
+        if (mAdjustmentIssuer != null) {
+            lm.addTaggedData(MetricsEvent.FIELD_NOTIFICATION_ASSISTANT_SERVICE_HASH,
+                    mAdjustmentIssuer.hashCode());
+        }
         return lm;
     }
 
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 9886d0a..1c0ac16 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -481,11 +481,16 @@
      * @param allowed whether bubbles are allowed.
      */
     public void setBubblesAllowed(String pkg, int uid, boolean allowed) {
+        boolean changed = false;
         synchronized (mPackagePreferences) {
             PackagePreferences p = getOrCreatePackagePreferencesLocked(pkg, uid);
+            changed = p.allowBubble != allowed;
             p.allowBubble = allowed;
             p.lockedAppFields = p.lockedAppFields | LockableAppFields.USER_LOCKED_BUBBLE;
         }
+        if (changed) {
+            updateConfig();
+        }
     }
 
     /**
@@ -610,12 +615,13 @@
     }
 
     @Override
-    public void createNotificationChannel(String pkg, int uid, NotificationChannel channel,
+    public boolean createNotificationChannel(String pkg, int uid, NotificationChannel channel,
             boolean fromTargetApp, boolean hasDndAccess) {
         Preconditions.checkNotNull(pkg);
         Preconditions.checkNotNull(channel);
         Preconditions.checkNotNull(channel.getId());
         Preconditions.checkArgument(!TextUtils.isEmpty(channel.getName()));
+        boolean needsPolicyFileChange = false;
         synchronized (mPackagePreferences) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
             if (r == null) {
@@ -632,17 +638,28 @@
             if (existing != null && fromTargetApp) {
                 if (existing.isDeleted()) {
                     existing.setDeleted(false);
+                    needsPolicyFileChange = true;
 
                     // log a resurrected channel as if it's new again
                     MetricsLogger.action(getChannelLog(channel, pkg).setType(
                             com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_OPEN));
                 }
 
-                existing.setName(channel.getName().toString());
-                existing.setDescription(channel.getDescription());
-                existing.setBlockableSystem(channel.isBlockableSystem());
-                if (existing.getGroup() == null) {
+                if (!Objects.equals(channel.getName().toString(), existing.getName().toString())) {
+                    existing.setName(channel.getName().toString());
+                    needsPolicyFileChange = true;
+                }
+                if (!Objects.equals(channel.getDescription(), existing.getDescription())) {
+                    existing.setDescription(channel.getDescription());
+                    needsPolicyFileChange = true;
+                }
+                if (channel.isBlockableSystem() != existing.isBlockableSystem()) {
+                    existing.setBlockableSystem(channel.isBlockableSystem());
+                    needsPolicyFileChange = true;
+                }
+                if (channel.getGroup() != null && existing.getGroup() == null) {
                     existing.setGroup(channel.getGroup());
+                    needsPolicyFileChange = true;
                 }
 
                 // Apps are allowed to downgrade channel importance if the user has not changed any
@@ -651,23 +668,30 @@
                 if (existing.getUserLockedFields() == 0 &&
                         channel.getImportance() < existing.getImportance()) {
                     existing.setImportance(channel.getImportance());
+                    needsPolicyFileChange = true;
                 }
 
                 // system apps and dnd access apps can bypass dnd if the user hasn't changed any
                 // fields on the channel yet
                 if (existing.getUserLockedFields() == 0 && hasDndAccess) {
                     boolean bypassDnd = channel.canBypassDnd();
-                    existing.setBypassDnd(bypassDnd);
+                    if (bypassDnd != existing.canBypassDnd()) {
+                        existing.setBypassDnd(bypassDnd);
+                        needsPolicyFileChange = true;
 
-                    if (bypassDnd != mAreChannelsBypassingDnd
-                            || previousExistingImportance != existing.getImportance()) {
-                        updateChannelsBypassingDnd(mContext.getUserId());
+                        if (bypassDnd != mAreChannelsBypassingDnd
+                                || previousExistingImportance != existing.getImportance()) {
+                            updateChannelsBypassingDnd(mContext.getUserId());
+                        }
                     }
                 }
 
                 updateConfig();
-                return;
+                return needsPolicyFileChange;
             }
+
+            needsPolicyFileChange = true;
+
             if (channel.getImportance() < IMPORTANCE_NONE
                     || channel.getImportance() > NotificationManager.IMPORTANCE_MAX) {
                 throw new IllegalArgumentException("Invalid importance level");
@@ -703,6 +727,8 @@
             MetricsLogger.action(getChannelLog(channel, pkg).setType(
                     com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_OPEN));
         }
+
+        return needsPolicyFileChange;
     }
 
     void clearLockedFieldsLocked(NotificationChannel channel) {
diff --git a/services/core/java/com/android/server/notification/RankingConfig.java b/services/core/java/com/android/server/notification/RankingConfig.java
index 72502acd..5de00e4 100644
--- a/services/core/java/com/android/server/notification/RankingConfig.java
+++ b/services/core/java/com/android/server/notification/RankingConfig.java
@@ -39,7 +39,7 @@
             boolean fromTargetApp);
     ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg,
             int uid, boolean includeDeleted, boolean includeNonGrouped, boolean includeEmpty);
-    void createNotificationChannel(String pkg, int uid, NotificationChannel channel,
+    boolean createNotificationChannel(String pkg, int uid, NotificationChannel channel,
             boolean fromTargetApp, boolean hasDndAccess);
     void updateNotificationChannel(String pkg, int uid, NotificationChannel channel, boolean fromUser);
     NotificationChannel getNotificationChannel(String pkg, int uid, String channelId, boolean includeDeleted);
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 2a61fee..a116237 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -14757,7 +14757,7 @@
 
             if (ps != null) {
                 try {
-                    rm.restoreUserData(packageName, installedUsers, appId, ceDataInode,
+                    rm.snapshotAndRestoreUserData(packageName, installedUsers, appId, ceDataInode,
                             seInfo, token);
                 } catch (RemoteException re) {
                     // Cannot happen, the RollbackManager is hosted in the same process.
@@ -15032,24 +15032,26 @@
 
         void tryProcessInstallRequest(InstallArgs args, int currentStatus) {
             mCurrentState.put(args, currentStatus);
-            boolean success = true;
             if (mCurrentState.size() != mChildParams.size()) {
                 return;
             }
+            int completeStatus = PackageManager.INSTALL_SUCCEEDED;
             for (Integer status : mCurrentState.values()) {
                 if (status == PackageManager.INSTALL_UNKNOWN) {
                     return;
                 } else if (status != PackageManager.INSTALL_SUCCEEDED) {
-                    success = false;
+                    completeStatus = status;
                     break;
                 }
             }
             final List<InstallRequest> installRequests = new ArrayList<>(mCurrentState.size());
             for (Map.Entry<InstallArgs, Integer> entry : mCurrentState.entrySet()) {
                 installRequests.add(new InstallRequest(entry.getKey(),
-                        createPackageInstalledInfo(entry.getValue())));
+                        createPackageInstalledInfo(completeStatus)));
             }
-            processInstallRequestsAsync(success, installRequests);
+            processInstallRequestsAsync(
+                    completeStatus == PackageManager.INSTALL_SUCCEEDED,
+                    installRequests);
         }
     }
 
@@ -17080,6 +17082,13 @@
                         cleanUpAppIdCreation(result);
                     }
                 }
+                // TODO(patb): create a more descriptive reason than unknown in future release
+                // mark all non-failure installs as UNKNOWN so we do not treat them as success
+                for (InstallRequest request : requests) {
+                    if (request.installResult.returnCode == PackageManager.INSTALL_SUCCEEDED) {
+                        request.installResult.returnCode = PackageManager.INSTALL_UNKNOWN;
+                    }
+                }
             }
             for (PrepareResult result : prepareResults.values()) {
                 if (result.freezer != null) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index f5c8049..81de8e26 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -2496,9 +2496,9 @@
                 default:
                     throw new IllegalArgumentException("Unknown option " + opt);
             }
-            if (replaceExisting) {
-                sessionParams.installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
-            }
+        }
+        if (replaceExisting) {
+            sessionParams.installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
         }
         return params;
     }
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index eec4b70..d6e87aa 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -674,9 +674,10 @@
             return new ArrayList<>();
         }
 
-        // Get the list of all dynamic shortcuts in this package
+        // Get the list of all dynamic shortcuts in this package.
         final ArrayList<ShortcutInfo> shortcuts = new ArrayList<>();
-        findAll(shortcuts, ShortcutInfo::isDynamicVisible, ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER);
+        findAll(shortcuts, ShortcutInfo::isDynamicVisible,
+                ShortcutInfo.CLONE_REMOVE_FOR_APP_PREDICTION);
 
         final List<ShortcutManager.ShareShortcutInfo> result = new ArrayList<>();
         for (int i = 0; i < shortcuts.size(); i++) {
diff --git a/services/core/java/com/android/server/pm/StagingManager.java b/services/core/java/com/android/server/pm/StagingManager.java
index 24bf18d..950450c 100644
--- a/services/core/java/com/android/server/pm/StagingManager.java
+++ b/services/core/java/com/android/server/pm/StagingManager.java
@@ -36,7 +36,6 @@
 import android.content.pm.ParceledListSlice;
 import android.content.pm.Signature;
 import android.content.rollback.IRollbackManager;
-import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
@@ -116,12 +115,9 @@
         final PackageInfo packageInfo = mApexManager.getPackageInfoForApexName(packageName);
 
         if (packageInfo == null) {
-            // Only allow installing new apexes if on a debuggable build.
-            if (!Build.IS_DEBUGGABLE) {
-                Slog.w(TAG, "Attempted to install new apex " + packageName + " on user build");
-                return false;
-            }
-            return true;
+            // Don't allow installation of new APEX.
+            Slog.e(TAG, "Attempted to install a new apex " + packageName + ". Rejecting");
+            return false;
         }
 
         final SigningDetails existingSigningDetails;
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 267fbf0..beb7268 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -1173,6 +1173,14 @@
                                     }
                                 }
 
+                                if (hardRestricted && !restrictionExempt
+                                        && (flags & FLAG_PERMISSION_SYSTEM_FIXED) != 0) {
+                                    // Applying a hard restriction implies revoking it. This might
+                                    // lead to a system-fixed, revoked permission.
+                                    flags &= ~FLAG_PERMISSION_SYSTEM_FIXED;
+                                    wasChanged = true;
+                                }
+
                                 if (wasChanged) {
                                     updatedUserIds = ArrayUtils.appendInt(updatedUserIds, userId);
                                 }
diff --git a/services/core/java/com/android/server/power/AttentionDetector.java b/services/core/java/com/android/server/power/AttentionDetector.java
index 14f1196..ed11fd4 100644
--- a/services/core/java/com/android/server/power/AttentionDetector.java
+++ b/services/core/java/com/android/server/power/AttentionDetector.java
@@ -19,6 +19,8 @@
 import static android.provider.Settings.System.ADAPTIVE_SLEEP;
 
 import android.Manifest;
+import android.app.ActivityManager;
+import android.app.SynchronousUserSwitchObserver;
 import android.attention.AttentionManagerInternal;
 import android.attention.AttentionManagerInternal.AttentionCallbackInternal;
 import android.content.ContentResolver;
@@ -28,6 +30,7 @@
 import android.os.Handler;
 import android.os.PowerManager;
 import android.os.PowerManagerInternal;
+import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -54,6 +57,8 @@
     private static final String TAG = "AttentionDetector";
     private static final boolean DEBUG = false;
 
+    private Context mContext;
+
     private boolean mIsSettingEnabled;
 
     /**
@@ -132,6 +137,7 @@
     }
 
     public void systemReady(Context context) {
+        mContext = context;
         updateEnabledFromSettings(context);
         mPackageManager = context.getPackageManager();
         mContentResolver = context.getContentResolver();
@@ -141,6 +147,13 @@
         mMaxAttentionApiTimeoutMillis = context.getResources().getInteger(
                 com.android.internal.R.integer.config_attentionApiTimeout);
 
+        try {
+            final UserSwitchObserver observer = new UserSwitchObserver();
+            ActivityManager.getService().registerUserSwitchObserver(observer, TAG);
+        } catch (RemoteException e) {
+             // Shouldn't happen since in-process.
+        }
+
         context.getContentResolver().registerContentObserver(Settings.System.getUriFor(
                 Settings.System.ADAPTIVE_SLEEP),
                 false, new ContentObserver(new Handler()) {
@@ -326,4 +339,11 @@
             mRequested.set(false);
         }
     }
+
+    private final class UserSwitchObserver extends SynchronousUserSwitchObserver {
+        @Override
+        public void onUserSwitching(int newUserId) throws RemoteException {
+            updateEnabledFromSettings(mContext);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index e2bbb2d..cfd3ae6 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -2085,7 +2085,8 @@
                     nextTimeout = -1;
                 }
 
-                if ((mUserActivitySummary & USER_ACTIVITY_SCREEN_BRIGHT) != 0) {
+                if ((mUserActivitySummary & USER_ACTIVITY_SCREEN_BRIGHT) != 0
+                        && (mWakeLockSummary & WAKE_LOCK_STAY_AWAKE) == 0) {
                     nextTimeout = mAttentionDetector.updateUserActivity(nextTimeout);
                 }
 
diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
index 9a2778d..301f650 100644
--- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
+++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
@@ -923,8 +923,8 @@
         }
 
         if (rd != null) {
-            // This is the apk session for a staged session. We have already
-            // backed up the apks, we just need to do user data backup.
+            // This is the apk session for a staged session. We do not need to create a new rollback
+            // for this session.
             PackageParser.PackageLite newPackage = null;
             try {
                 newPackage = PackageParser.parsePackageLite(
@@ -937,8 +937,6 @@
             for (PackageRollbackInfo info : rd.info.getPackages()) {
                 if (info.getPackageName().equals(packageName)) {
                     info.getInstalledUsers().addAll(IntArray.wrap(installedUsers));
-                    mAppDataRollbackHelper.snapshotAppData(rd.info.getRollbackId(), info);
-                    saveRollbackData(rd);
                     return true;
                 }
             }
@@ -959,8 +957,7 @@
         }
         newRollback.addToken(token);
 
-        return enableRollbackForPackageSession(newRollback.data, packageSession,
-                installedUsers, /* snapshotUserData*/ true);
+        return enableRollbackForPackageSession(newRollback.data, packageSession, installedUsers);
     }
 
     /**
@@ -971,8 +968,7 @@
      * @return true on success, false on failure.
      */
     private boolean enableRollbackForPackageSession(RollbackData data,
-            PackageInstaller.SessionInfo session, @NonNull int[] installedUsers,
-            boolean snapshotUserData) {
+            PackageInstaller.SessionInfo session, @NonNull int[] installedUsers) {
         // TODO: Don't attempt to enable rollback for split installs.
         final int installFlags = session.installFlags;
         if ((installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) == 0) {
@@ -1033,10 +1029,6 @@
                 isApex, IntArray.wrap(installedUsers),
                 new SparseLongArray() /* ceSnapshotInodes */);
 
-        if (snapshotUserData && !isApex) {
-            mAppDataRollbackHelper.snapshotAppData(data.info.getRollbackId(), packageRollbackInfo);
-        }
-
         try {
             ApplicationInfo appInfo = pkgInfo.applicationInfo;
             RollbackStore.backupPackageCodePath(data, packageName, appInfo.sourceDir);
@@ -1057,13 +1049,15 @@
     }
 
     @Override
-    public void restoreUserData(String packageName, int[] userIds, int appId, long ceDataInode,
-            String seInfo, int token) {
+    public void snapshotAndRestoreUserData(String packageName, int[] userIds, int appId,
+            long ceDataInode, String seInfo, int token) {
         if (Binder.getCallingUid() != Process.SYSTEM_UID) {
-            throw new SecurityException("restoreUserData may only be called by the system.");
+            throw new SecurityException(
+                    "snapshotAndRestoreUserData may only be called by the system.");
         }
 
         getHandler().post(() -> {
+            snapshotUserDataInternal(packageName);
             restoreUserDataInternal(packageName, userIds, appId, ceDataInode, seInfo, token);
             final PackageManagerInternal pmi = LocalServices.getService(
                     PackageManagerInternal.class);
@@ -1071,6 +1065,38 @@
         });
     }
 
+    private void snapshotUserDataInternal(String packageName) {
+        synchronized (mLock) {
+            // staged installs
+            ensureRollbackDataLoadedLocked();
+            for (int i = 0; i < mRollbacks.size(); i++) {
+                RollbackData data = mRollbacks.get(i);
+                if (data.state != RollbackData.ROLLBACK_STATE_ENABLING) {
+                    continue;
+                }
+
+                for (PackageRollbackInfo info : data.info.getPackages()) {
+                    if (info.getPackageName().equals(packageName)) {
+                        mAppDataRollbackHelper.snapshotAppData(data.info.getRollbackId(), info);
+                        saveRollbackData(data);
+                        return;
+                    }
+                }
+            }
+            // non-staged installs
+            PackageRollbackInfo info;
+            for (NewRollback rollback : mNewRollbacks) {
+                info = getPackageRollbackInfo(rollback.data, packageName);
+                if (info != null) {
+                    mAppDataRollbackHelper.snapshotAppData(rollback.data.info.getRollbackId(),
+                            info);
+                    saveRollbackData(rollback.data);
+                    return;
+                }
+            }
+        }
+    }
+
     private void restoreUserDataInternal(String packageName, int[] userIds, int appId,
             long ceDataInode, String seInfo, int token) {
         PackageRollbackInfo info = null;
@@ -1130,7 +1156,7 @@
 
             if (!session.isMultiPackage()) {
                 if (!enableRollbackForPackageSession(newRollback.data, session,
-                            new int[0], /* snapshotUserData */ false)) {
+                            new int[0])) {
                     Log.e(TAG, "Unable to enable rollback for session: " + sessionId);
                     result.offer(false);
                     return;
@@ -1145,7 +1171,7 @@
                         return;
                     }
                     if (!enableRollbackForPackageSession(newRollback.data, childSession,
-                                new int[0], /* snapshotUserData */ false)) {
+                                new int[0])) {
                         Log.e(TAG, "Unable to enable rollback for session: " + sessionId);
                         result.offer(false);
                         return;
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 112104d..828f790 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -719,21 +719,6 @@
         // Ensure state for the current user is applied, even if passed a non-current user.
         final int net1 = gatherDisableActionsLocked(mCurrentUserId, 1);
         final int net2 = gatherDisableActionsLocked(mCurrentUserId, 2);
-
-        // TODO(b/113914868): investigation log for disappearing home button
-        if (whichFlag == 1 && pkg != null && pkg.contains("systemui")) {
-            String disabledData = "{ ";
-            for (int i = 0; i < mDisableRecords.size(); i++) {
-                DisableRecord tok = mDisableRecords.get(i);
-                disabledData += "    ([" + i + "] " + tok + "), ";
-            }
-            disabledData += " }";
-            final UiState state = getUiState(displayId);
-
-            Log.d(TAG, "disabledlocked (b/113914868): displayId=" + displayId + ", net1=" + net1
-                    + ", mDisabled1=" + state.mDisabled1 + ", token=" + token
-                    + ", mDisableRecords=" + mDisableRecords.size() + " => " + disabledData);
-        }
         final UiState state = getUiState(displayId);
         if (!state.disableEquals(net1, net2)) {
             state.setDisabled(net1, net2);
diff --git a/services/core/java/com/android/server/telecom/TelecomLoaderService.java b/services/core/java/com/android/server/telecom/TelecomLoaderService.java
index e65eae0..54369ca 100644
--- a/services/core/java/com/android/server/telecom/TelecomLoaderService.java
+++ b/services/core/java/com/android/server/telecom/TelecomLoaderService.java
@@ -16,21 +16,17 @@
 
 package com.android.server.telecom;
 
+import android.app.role.RoleManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Handler;
 import android.os.IBinder;
-import android.os.Looper;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.UserHandle;
-import android.provider.Settings;
 import android.telecom.DefaultDialerManager;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
@@ -221,19 +217,10 @@
     private void registerDefaultAppNotifier() {
         final DefaultPermissionGrantPolicy permissionPolicy = getDefaultPermissionGrantPolicy();
         // Notify the package manager on default app changes
-        final Uri defaultDialerAppUri = Settings.Secure.getUriFor(
-                Settings.Secure.DIALER_DEFAULT_APPLICATION);
-        ContentObserver contentObserver = new ContentObserver(
-                new Handler(Looper.getMainLooper())) {
-            @Override
-            public void onChange(boolean selfChange, Uri uri, int userId) {
-                if (defaultDialerAppUri.equals(uri)) {
-                    updateSimCallManagerPermissions(permissionPolicy, userId);
-                }
-            }
-        };
-        mContext.getContentResolver().registerContentObserver(defaultDialerAppUri,
-                false, contentObserver, UserHandle.USER_ALL);
+        final RoleManager roleManager = mContext.getSystemService(RoleManager.class);
+        roleManager.addOnRoleHoldersChangedListenerAsUser(mContext.getMainExecutor(),
+                (roleName, user) -> updateSimCallManagerPermissions(permissionPolicy,
+                        user.getIdentifier()), UserHandle.ALL);
     }
 
 
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index 3a39053..a2eb40b 100644
--- a/services/core/java/com/android/server/trust/TrustManagerService.java
+++ b/services/core/java/com/android/server/trust/TrustManagerService.java
@@ -518,8 +518,11 @@
                     agentInfo = mActiveAgents.valueAt(index);
                 }
 
-                boolean directUnlock = resolveInfo.serviceInfo.directBootAware
-                    && agentInfo.settings.canUnlockProfile;
+                boolean directUnlock = false;
+                if (agentInfo.settings != null) {
+                    directUnlock = resolveInfo.serviceInfo.directBootAware
+                        && agentInfo.settings.canUnlockProfile;
+                }
 
                 if (directUnlock) {
                     if (DEBUG) Slog.d(TAG, "refreshAgentList: trustagent " + name
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java
index 476a273..165055a 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java
@@ -19,6 +19,7 @@
 import android.content.pm.PackageInfo;
 import android.os.AsyncTask;
 import android.os.UserHandle;
+import android.util.Slog;
 import android.webkit.WebViewProviderInfo;
 import android.webkit.WebViewProviderResponse;
 
@@ -154,7 +155,10 @@
         WebViewProviderInfo[] webviewProviders = mSystemInterface.getWebViewPackages();
         WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders);
         if (fallbackProvider != null) {
+            Slog.i(TAG, "One-time migration: enabling " + fallbackProvider.packageName);
             mSystemInterface.enablePackageForAllUsers(mContext, fallbackProvider.packageName, true);
+        } else {
+            Slog.i(TAG, "Skipping one-time migration: no fallback provider");
         }
         mSystemInterface.enableFallbackLogic(false);
     }
diff --git a/services/core/java/com/android/server/wm/ActivityServiceConnectionsHolder.java b/services/core/java/com/android/server/wm/ActivityServiceConnectionsHolder.java
index ad46248..c56a9e2 100644
--- a/services/core/java/com/android/server/wm/ActivityServiceConnectionsHolder.java
+++ b/services/core/java/com/android/server/wm/ActivityServiceConnectionsHolder.java
@@ -101,7 +101,13 @@
         if (mConnections == null || mConnections.isEmpty()) {
             return;
         }
-        mService.mH.post(() -> mService.mAmInternal.disconnectActivityFromServices(this));
+        // Capture and null out mConnections, to guarantee that we process
+        // disconnect of these specific connections exactly once even if
+        // we're racing with rapid activity lifecycle churn and this
+        // method is invoked more than once on this object.
+        final Object disc = mConnections;
+        mConnections = null;
+        mService.mH.post(() -> mService.mAmInternal.disconnectActivityFromServices(this, disc));
     }
 
     public void dump(PrintWriter pw, String prefix) {
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 765c9d0..b97ecec 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -5867,8 +5867,10 @@
      */
     Intent getSecondaryHomeIntent(String preferredPackage) {
         final Intent intent = new Intent(mTopAction, mTopData != null ? Uri.parse(mTopData) : null);
-        if (preferredPackage == null) {
-            // Using the component stored in config if no package name.
+        final boolean useSystemProvidedLauncher = mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_useSystemProvidedLauncherForSecondary);
+        if (preferredPackage == null || useSystemProvidedLauncher) {
+            // Using the component stored in config if no package name or forced.
             final String secondaryHomeComponent = mContext.getResources().getString(
                     com.android.internal.R.string.config_secondaryHomeComponent);
             intent.setComponent(ComponentName.unflattenFromString(secondaryHomeComponent));
diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java
index 3d7e50d..cae7612 100644
--- a/services/core/java/com/android/server/wm/AppWindowToken.java
+++ b/services/core/java/com/android/server/wm/AppWindowToken.java
@@ -1733,17 +1733,13 @@
             return;
         }
 
-        if (mThumbnail == null && getTask() != null) {
-            final TaskSnapshotController snapshotCtrl = mWmService.mTaskSnapshotController;
-            final ArraySet<Task> tasks = new ArraySet<>();
-            tasks.add(getTask());
-            snapshotCtrl.snapshotTasks(tasks);
-            snapshotCtrl.addSkipClosingAppSnapshotTasks(tasks);
-            final ActivityManager.TaskSnapshot snapshot = snapshotCtrl.getSnapshot(
-                    getTask().mTaskId, getTask().mUserId, false /* restoreFromDisk */,
-                    false /* reducedResolution */);
+        Task task = getTask();
+        if (mThumbnail == null && task != null && !hasCommittedReparentToAnimationLeash()) {
+            SurfaceControl.ScreenshotGraphicBuffer snapshot =
+                    mWmService.mTaskSnapshotController.createTaskSnapshot(
+                            task, 1 /* scaleFraction */);
             if (snapshot != null) {
-                mThumbnail = new AppWindowThumbnail(t, this, snapshot.getSnapshot(),
+                mThumbnail = new AppWindowThumbnail(t, this, snapshot.getGraphicBuffer(),
                         true /* relative */);
             }
         }
@@ -2858,7 +2854,7 @@
             }
         }
         t.hide(mTransitChangeLeash);
-        t.reparent(mTransitChangeLeash, null);
+        t.remove(mTransitChangeLeash);
         mTransitChangeLeash = null;
         if (cancel) {
             onAnimationLeashLost(t);
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index e1dd352..21f01ff 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -2720,9 +2720,9 @@
                 res.getBoolean(R.bool.config_navBarAlwaysShowOnSideEdgeGesture);
 
         // This should calculate how much above the frame we accept gestures.
-        mBottomGestureAdditionalInset = Math.max(0,
+        mBottomGestureAdditionalInset =
                 res.getDimensionPixelSize(R.dimen.navigation_bar_gesture_height)
-                        - getNavigationBarFrameHeight(portraitRotation, uiMode));
+                        - getNavigationBarFrameHeight(portraitRotation, uiMode);
 
         updateConfigurationAndScreenSizeDependentBehaviors();
         mWindowOutsetBottom = ScreenShapeHelper.getWindowOutsetBottomPx(mContext.getResources());
diff --git a/services/core/java/com/android/server/wm/RecentsAnimation.java b/services/core/java/com/android/server/wm/RecentsAnimation.java
index c5b2566..434239f 100644
--- a/services/core/java/com/android/server/wm/RecentsAnimation.java
+++ b/services/core/java/com/android/server/wm/RecentsAnimation.java
@@ -231,6 +231,11 @@
                 mService.mRootActivityContainer.sendPowerHintForLaunchEndIfNeeded();
             }
 
+            // Once the target is shown, prevent spurious background app switches
+            if (reorderMode == REORDER_MOVE_TO_TOP) {
+                mService.stopAppSwitches();
+            }
+
             mService.mH.post(
                     () -> mService.mAmInternal.setRunningRemoteAnimation(mCallingPid, false));
 
diff --git a/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java b/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java
index 854537b..fb781b0 100644
--- a/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java
+++ b/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java
@@ -113,7 +113,12 @@
         // statistics because it passes every touch event though a GestureDetector. By creating an
         // anonymous subclass of GestureDetector, these statistics will be recorded with a unique
         // source name that can be filtered.
-        mGestureDetector = new GestureDetector(mContext, new FlingGestureDetector(), mHandler) {};
+
+        // GestureDetector would get a ViewConfiguration instance by context, that may also
+        // create a new WindowManagerImpl for the new display, and lock WindowManagerGlobal
+        // temporarily in the constructor that would make a deadlock.
+        mHandler.post(() -> mGestureDetector =
+                new GestureDetector(mContext, new FlingGestureDetector(), mHandler) {});
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index 432ca33..1815218 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -21,6 +21,7 @@
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityManager.TaskSnapshot;
@@ -241,6 +242,28 @@
         return null;
     }
 
+    @Nullable
+    SurfaceControl.ScreenshotGraphicBuffer createTaskSnapshot(@NonNull Task task,
+            float scaleFraction) {
+        if (task.getSurfaceControl() == null) {
+            if (DEBUG_SCREENSHOT) {
+                Slog.w(TAG_WM, "Failed to take screenshot. No surface control for " + task);
+            }
+            return null;
+        }
+        task.getBounds(mTmpRect);
+        mTmpRect.offsetTo(0, 0);
+        final SurfaceControl.ScreenshotGraphicBuffer screenshotBuffer =
+                SurfaceControl.captureLayers(
+                        task.getSurfaceControl().getHandle(), mTmpRect, scaleFraction);
+        final GraphicBuffer buffer = screenshotBuffer != null ? screenshotBuffer.getGraphicBuffer()
+                : null;
+        if (buffer == null || buffer.getWidth() <= 1 || buffer.getHeight() <= 1) {
+            return null;
+        }
+        return screenshotBuffer;
+    }
+
     @Nullable private TaskSnapshot snapshotTask(Task task) {
         if (!mService.mPolicy.isScreenOn()) {
             if (DEBUG_SCREENSHOT) {
@@ -248,12 +271,6 @@
             }
             return null;
         }
-        if (task.getSurfaceControl() == null) {
-            if (DEBUG_SCREENSHOT) {
-                Slog.w(TAG_WM, "Failed to take screenshot. No surface control for " + task);
-            }
-            return null;
-        }
 
         final AppWindowToken appWindowToken = findAppTokenForSnapshot(task);
         if (appWindowToken == null) {
@@ -271,8 +288,6 @@
 
         final boolean isLowRamDevice = ActivityManager.isLowRamDeviceStatic();
         final float scaleFraction = isLowRamDevice ? mPersister.getReducedScale() : 1f;
-        task.getBounds(mTmpRect);
-        mTmpRect.offsetTo(0, 0);
 
         final WindowState mainWindow = appWindowToken.findMainWindow();
         if (mainWindow == null) {
@@ -280,18 +295,17 @@
             return null;
         }
         final SurfaceControl.ScreenshotGraphicBuffer screenshotBuffer =
-                SurfaceControl.captureLayers(
-                        task.getSurfaceControl().getHandle(), mTmpRect, scaleFraction);
-        final GraphicBuffer buffer = screenshotBuffer != null ? screenshotBuffer.getGraphicBuffer()
-                : null;
-        if (buffer == null || buffer.getWidth() <= 1 || buffer.getHeight() <= 1) {
+                createTaskSnapshot(task, scaleFraction);
+
+        if (screenshotBuffer == null) {
             if (DEBUG_SCREENSHOT) {
                 Slog.w(TAG_WM, "Failed to take screenshot for " + task);
             }
             return null;
         }
         final boolean isWindowTranslucent = mainWindow.getAttrs().format != PixelFormat.OPAQUE;
-        return new TaskSnapshot(appWindowToken.mActivityComponent, buffer,
+        return new TaskSnapshot(
+                appWindowToken.mActivityComponent, screenshotBuffer.getGraphicBuffer(),
                 screenshotBuffer.getColorSpace(), appWindowToken.getConfiguration().orientation,
                 getInsets(mainWindow), isLowRamDevice /* reduced */, scaleFraction /* scale */,
                 true /* isRealSnapshot */, task.getWindowingMode(), getSystemUiVisibility(task),
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 4e1cac9..203704b 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -749,10 +749,12 @@
             if (!disableOtaDexopt) {
                 traceBeginAndSlog("StartOtaDexOptService");
                 try {
+                    Watchdog.getInstance().pauseWatchingCurrentThread("moveab");
                     OtaDexoptService.main(mSystemContext, mPackageManagerService);
                 } catch (Throwable e) {
                     reportWtf("starting OtaDexOptService", e);
                 } finally {
+                    Watchdog.getInstance().resumeWatchingCurrentThread("moveab");
                     traceEnd();
                 }
             }
diff --git a/services/net/java/android/net/NetworkMonitorManager.java b/services/net/java/android/net/NetworkMonitorManager.java
new file mode 100644
index 0000000..0f41302
--- /dev/null
+++ b/services/net/java/android/net/NetworkMonitorManager.java
@@ -0,0 +1,201 @@
+/*
+ * 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.net;
+
+import android.annotation.NonNull;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * A convenience wrapper for INetworkMonitor.
+ *
+ * Wraps INetworkMonitor calls, making them a bit more friendly to use. Currently handles:
+ * - Clearing calling identity
+ * - Ignoring RemoteExceptions
+ * - Converting to stable parcelables
+ *
+ * By design, all methods on INetworkMonitor are asynchronous oneway IPCs and are thus void. All the
+ * wrapper methods in this class return a boolean that callers can use to determine whether
+ * RemoteException was thrown.
+ */
+public class NetworkMonitorManager {
+
+    @NonNull private final INetworkMonitor mNetworkMonitor;
+    @NonNull private final String mTag;
+
+    public NetworkMonitorManager(@NonNull INetworkMonitor networkMonitorManager,
+            @NonNull String tag) {
+        mNetworkMonitor = networkMonitorManager;
+        mTag = tag;
+    }
+
+    public NetworkMonitorManager(@NonNull INetworkMonitor networkMonitorManager) {
+        this(networkMonitorManager, NetworkMonitorManager.class.getSimpleName());
+    }
+
+    private void log(String s, Throwable e) {
+        Log.e(mTag, s, e);
+    }
+
+    // CHECKSTYLE:OFF Generated code
+
+    public boolean start() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.start();
+            return true;
+        } catch (RemoteException e) {
+            log("Error in start", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean launchCaptivePortalApp() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.launchCaptivePortalApp();
+            return true;
+        } catch (RemoteException e) {
+            log("Error in launchCaptivePortalApp", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyCaptivePortalAppFinished(int response) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyCaptivePortalAppFinished(response);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyCaptivePortalAppFinished", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean setAcceptPartialConnectivity() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.setAcceptPartialConnectivity();
+            return true;
+        } catch (RemoteException e) {
+            log("Error in setAcceptPartialConnectivity", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean forceReevaluation(int uid) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.forceReevaluation(uid);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in forceReevaluation", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyPrivateDnsChanged(PrivateDnsConfigParcel config) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyPrivateDnsChanged(config);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyPrivateDnsChanged", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyDnsResponse(int returnCode) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyDnsResponse(returnCode);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyDnsResponse", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyNetworkConnected(LinkProperties lp, NetworkCapabilities nc) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyNetworkConnected(lp, nc);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyNetworkConnected", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyNetworkDisconnected() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyNetworkDisconnected();
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyNetworkDisconnected", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyLinkPropertiesChanged(LinkProperties lp) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyLinkPropertiesChanged(lp);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyLinkPropertiesChanged", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyNetworkCapabilitiesChanged(NetworkCapabilities nc) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyNetworkCapabilitiesChanged(nc);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyNetworkCapabilitiesChanged", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    // CHECKSTYLE:ON Generated code
+}
diff --git a/services/net/java/android/net/NetworkStackClient.java b/services/net/java/android/net/NetworkStackClient.java
index 6b5842f..99da637 100644
--- a/services/net/java/android/net/NetworkStackClient.java
+++ b/services/net/java/android/net/NetworkStackClient.java
@@ -26,6 +26,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.net.dhcp.DhcpServingParamsParcel;
 import android.net.dhcp.IDhcpServerCallbacks;
@@ -33,15 +34,21 @@
 import android.net.util.SharedLog;
 import android.os.Binder;
 import android.os.Build;
+import android.os.Environment;
 import android.os.IBinder;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.SystemClock;
 import android.os.UserHandle;
+import android.provider.DeviceConfig;
+import android.text.format.DateUtils;
+import android.util.ArraySet;
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
 
+import java.io.File;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 
@@ -54,6 +61,23 @@
 
     private static final int NETWORKSTACK_TIMEOUT_MS = 10_000;
     private static final String IN_PROCESS_SUFFIX = ".InProcess";
+    private static final String PREFS_FILE = "NetworkStackClientPrefs.xml";
+    private static final String PREF_KEY_LAST_CRASH_TIME = "lastcrash_time";
+    private static final String CONFIG_MIN_CRASH_INTERVAL_MS = "min_crash_interval";
+    private static final String CONFIG_MIN_UPTIME_BEFORE_CRASH_MS = "min_uptime_before_crash";
+    private static final String CONFIG_ALWAYS_RATELIMIT_NETWORKSTACK_CRASH =
+            "always_ratelimit_networkstack_crash";
+
+    // Even if the network stack is lost, do not crash the system more often than this.
+    // Connectivity would be broken, but if the user needs the device for something urgent
+    // (like calling emergency services) we should not bootloop the device.
+    // This is the default value: the actual value can be adjusted via device config.
+    private static final long DEFAULT_MIN_CRASH_INTERVAL_MS = 6 * DateUtils.HOUR_IN_MILLIS;
+
+    // Even if the network stack is lost, do not crash the system server if it was less than
+    // this much after boot. This avoids bootlooping the device, and crashes should address very
+    // infrequent failures, not failures on boot.
+    private static final long DEFAULT_MIN_UPTIME_BEFORE_CRASH_MS = 30 * DateUtils.MINUTE_IN_MILLIS;
 
     private static NetworkStackClient sInstance;
 
@@ -67,12 +91,28 @@
     @GuardedBy("mLog")
     private final SharedLog mLog = new SharedLog(TAG);
 
-    private volatile boolean mNetworkStackStartRequested = false;
+    private volatile boolean mWasSystemServerInitialized = false;
+
+    @GuardedBy("mHealthListeners")
+    private final ArraySet<NetworkStackHealthListener> mHealthListeners = new ArraySet<>();
 
     private interface NetworkStackCallback {
         void onNetworkStackConnected(INetworkStackConnector connector);
     }
 
+    /**
+     * Callback interface for severe failures of the NetworkStack.
+     *
+     * <p>Useful for health monitors such as PackageWatchdog.
+     */
+    public interface NetworkStackHealthListener {
+        /**
+         * Called when there is a severe failure of the network stack.
+         * @param packageName Package name of the network stack.
+         */
+        void onNetworkStackFailure(@NonNull String packageName);
+    }
+
     private NetworkStackClient() { }
 
     /**
@@ -86,6 +126,15 @@
     }
 
     /**
+     * Add a {@link NetworkStackHealthListener} to listen to network stack health events.
+     */
+    public void registerHealthListener(@NonNull NetworkStackHealthListener listener) {
+        synchronized (mHealthListeners) {
+            mHealthListeners.add(listener);
+        }
+    }
+
+    /**
      * Create a DHCP server according to the specified parameters.
      *
      * <p>The server will be returned asynchronously through the provided callbacks.
@@ -147,6 +196,16 @@
     }
 
     private class NetworkStackConnection implements ServiceConnection {
+        @NonNull
+        private final Context mContext;
+        @NonNull
+        private final String mPackageName;
+
+        private NetworkStackConnection(@NonNull Context context, @NonNull String packageName) {
+            mContext = context;
+            mPackageName = packageName;
+        }
+
         @Override
         public void onServiceConnected(ComponentName name, IBinder service) {
             logi("Network stack service connected");
@@ -155,14 +214,14 @@
 
         @Override
         public void onServiceDisconnected(ComponentName name) {
-            // The system has lost its network stack (probably due to a crash in the
-            // network stack process): better crash rather than stay in a bad state where all
-            // networking is broken.
             // onServiceDisconnected is not being called on device shutdown, so this method being
             // called always indicates a bad state for the system server.
-            maybeCrashWithTerribleFailure("Lost network stack");
+            // This code path is only run by the system server: only the system server binds
+            // to the NetworkStack as a service. Other processes get the NetworkStack from
+            // the ServiceManager.
+            maybeCrashWithTerribleFailure("Lost network stack", mContext, mPackageName);
         }
-    };
+    }
 
     private void registerNetworkStackService(@NonNull IBinder service) {
         final INetworkStackConnector connector = INetworkStackConnector.Stub.asInterface(service);
@@ -189,7 +248,7 @@
      */
     public void init() {
         log("Network stack init");
-        mNetworkStackStartRequested = true;
+        mWasSystemServerInitialized = true;
     }
 
     /**
@@ -202,6 +261,7 @@
      */
     public void start(Context context) {
         log("Starting network stack");
+
         final PackageManager pm = context.getPackageManager();
 
         // Try to bind in-process if the device was shipped with an in-process version
@@ -216,16 +276,19 @@
         }
 
         if (intent == null) {
-            maybeCrashWithTerribleFailure("Could not resolve the network stack");
+            maybeCrashWithTerribleFailure("Could not resolve the network stack", context, null);
             return;
         }
 
+        final String packageName = intent.getComponent().getPackageName();
+
         // Start the network stack. The service will be added to the service manager in
         // NetworkStackConnection.onServiceConnected().
-        if (!context.bindServiceAsUser(intent, new NetworkStackConnection(),
+        if (!context.bindServiceAsUser(intent, new NetworkStackConnection(context, packageName),
                 Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT, UserHandle.SYSTEM)) {
             maybeCrashWithTerribleFailure(
-                    "Could not bind to network stack in-process, or in app with " + intent);
+                    "Could not bind to network stack in-process, or in app with " + intent,
+                    context, packageName);
             return;
         }
 
@@ -274,11 +337,91 @@
         }
     }
 
-    private void maybeCrashWithTerribleFailure(@NonNull String message) {
+    private void maybeCrashWithTerribleFailure(@NonNull String message,
+            @NonNull Context context, @Nullable String packageName) {
         logWtf(message, null);
-        if (Build.IS_DEBUGGABLE) {
+        // uptime is monotonic even after a framework restart
+        final long uptime = SystemClock.elapsedRealtime();
+        final long now = System.currentTimeMillis();
+        final long minCrashIntervalMs = DeviceConfig.getLong(DeviceConfig.NAMESPACE_CONNECTIVITY,
+                CONFIG_MIN_CRASH_INTERVAL_MS, DEFAULT_MIN_CRASH_INTERVAL_MS);
+        final long minUptimeBeforeCrash = DeviceConfig.getLong(DeviceConfig.NAMESPACE_CONNECTIVITY,
+                CONFIG_MIN_UPTIME_BEFORE_CRASH_MS, DEFAULT_MIN_UPTIME_BEFORE_CRASH_MS);
+        final boolean alwaysRatelimit = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_CONNECTIVITY,
+                CONFIG_ALWAYS_RATELIMIT_NETWORKSTACK_CRASH, false);
+
+        final SharedPreferences prefs = getSharedPreferences(context);
+        final long lastCrashTime = tryGetLastCrashTime(prefs);
+
+        // Only crash if there was enough time since boot, and (if known) enough time passed since
+        // the last crash.
+        // time and lastCrashTime may be unreliable if devices have incorrect clock time, but they
+        // are only used to limit the number of crashes compared to only using the time since boot,
+        // which would also be OK behavior by itself.
+        // - If lastCrashTime is incorrectly more than the current time, only look at uptime
+        // - If it is much less than current time, only look at uptime
+        // - If current time is during the next few hours after last crash time, don't crash.
+        //   Considering that this only matters if last boot was some time ago, it's likely that
+        //   time will be set correctly. Otherwise, not crashing is not a big problem anyway. Being
+        //   in this last state would also not last for long since the window is only a few hours.
+        final boolean alwaysCrash = Build.IS_DEBUGGABLE && !alwaysRatelimit;
+        final boolean justBooted = uptime < minUptimeBeforeCrash;
+        final boolean haveLastCrashTime = (lastCrashTime != 0) && (lastCrashTime < now);
+        final boolean haveKnownRecentCrash =
+                haveLastCrashTime && (now < lastCrashTime + minCrashIntervalMs);
+        if (alwaysCrash || (!justBooted && !haveKnownRecentCrash)) {
+            // The system is not bound to its network stack (for example due to a crash in the
+            // network stack process): better crash rather than stay in a bad state where all
+            // networking is broken.
+            // Using device-encrypted SharedPreferences as DeviceConfig does not have a synchronous
+            // API to persist settings before a crash.
+            tryWriteLastCrashTime(prefs, now);
             throw new IllegalStateException(message);
         }
+
+        // Here the system crashed recently already. Inform listeners that something is
+        // definitely wrong.
+        if (packageName != null) {
+            final ArraySet<NetworkStackHealthListener> listeners;
+            synchronized (mHealthListeners) {
+                listeners = new ArraySet<>(mHealthListeners);
+            }
+            for (NetworkStackHealthListener listener : listeners) {
+                listener.onNetworkStackFailure(packageName);
+            }
+        }
+    }
+
+    @Nullable
+    private SharedPreferences getSharedPreferences(@NonNull Context context) {
+        try {
+            final File prefsFile = new File(
+                    Environment.getDataSystemDeDirectory(UserHandle.USER_SYSTEM), PREFS_FILE);
+            return context.createDeviceProtectedStorageContext()
+                    .getSharedPreferences(prefsFile, Context.MODE_PRIVATE);
+        } catch (Throwable e) {
+            logWtf("Error loading shared preferences", e);
+            return null;
+        }
+    }
+
+    private long tryGetLastCrashTime(@Nullable SharedPreferences prefs) {
+        if (prefs == null) return 0L;
+        try {
+            return prefs.getLong(PREF_KEY_LAST_CRASH_TIME, 0L);
+        } catch (Throwable e) {
+            logWtf("Error getting last crash time", e);
+            return 0L;
+        }
+    }
+
+    private void tryWriteLastCrashTime(@Nullable SharedPreferences prefs, long value) {
+        if (prefs == null) return;
+        try {
+            prefs.edit().putLong(PREF_KEY_LAST_CRASH_TIME, value).commit();
+        } catch (Throwable e) {
+            logWtf("Error writing last crash time", e);
+        }
     }
 
     /**
@@ -350,7 +493,7 @@
                     "Only the system server should try to bind to the network stack.");
         }
 
-        if (!mNetworkStackStartRequested) {
+        if (!mWasSystemServerInitialized) {
             // The network stack is not being started in this process, e.g. this process is not
             // the system server. Get a remote connector registered by the system server.
             final INetworkStackConnector connector = getRemoteConnector();
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
index 18c524a..748c23a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.job;
 
+import static android.text.format.DateUtils.DAY_IN_MILLIS;
 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 
@@ -162,6 +163,56 @@
 
     /**
      * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
+     * with the correct delay and deadline constraints if the periodic job is scheduled with the
+     * minimum possible period.
+     */
+    @Test
+    public void testGetRescheduleJobForPeriodic_minPeriod() {
+        final long now = sElapsedRealtimeClock.millis();
+        JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow",
+                createJobInfo().setPeriodic(15 * MINUTE_IN_MILLIS));
+        final long nextWindowStartTime = now + 15 * MINUTE_IN_MILLIS;
+        final long nextWindowEndTime = now + 30 * MINUTE_IN_MILLIS;
+
+        for (int i = 0; i < 25; i++) {
+            JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
+            assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
+            assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+            advanceElapsedClock(30_000); // 30 seconds
+        }
+
+        for (int i = 0; i < 5; i++) {
+            // Window buffering in last 1/6 of window.
+            JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
+            assertEquals(nextWindowStartTime + i * 30_000, rescheduledJob.getEarliestRunTime());
+            assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+            advanceElapsedClock(30_000); // 30 seconds
+        }
+    }
+
+    /**
+     * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
+     * with the correct delay and deadline constraints if the periodic job is scheduled with a
+     * period that's too large.
+     */
+    @Test
+    public void testGetRescheduleJobForPeriodic_largePeriod() {
+        final long now = sElapsedRealtimeClock.millis();
+        JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow",
+                createJobInfo().setPeriodic(2 * 365 * DAY_IN_MILLIS));
+        assertEquals(now, job.getEarliestRunTime());
+        // Periods are capped at 365 days (1 year).
+        assertEquals(now + 365 * DAY_IN_MILLIS, job.getLatestRunTimeElapsed());
+        final long nextWindowStartTime = now + 365 * DAY_IN_MILLIS;
+        final long nextWindowEndTime = nextWindowStartTime + 365 * DAY_IN_MILLIS;
+
+        JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
+        assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
+        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
+    }
+
+    /**
+     * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
      * with the correct delay and deadline constraints if the periodic job is completed and
      * rescheduled while run in its expected running window.
      */
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index 4e89357..f07a5b5 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -386,6 +386,8 @@
         ExecutionStats expectedStats = new ExecutionStats();
         expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS;
         expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
+        expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_RARE;
+        expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_RARE;
 
         final int uid = 10001;
         mQuotaController.onAppRemovedLocked("com.android.test.remove", uid);
@@ -424,6 +426,8 @@
         ExecutionStats expectedStats = new ExecutionStats();
         expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS;
         expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
+        expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_RARE;
+        expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_RARE;
 
         mQuotaController.onUserRemovedLocked(0);
         assertNull(mQuotaController.getTimingSessions(0, "com.android.test"));
@@ -456,6 +460,8 @@
         ExecutionStats inputStats = new ExecutionStats();
 
         inputStats.windowSizeMs = expectedStats.windowSizeMs = 12 * HOUR_IN_MILLIS;
+        inputStats.jobCountLimit = expectedStats.jobCountLimit = 100;
+        inputStats.sessionCountLimit = expectedStats.sessionCountLimit = 100;
         // Invalid time is now +24 hours since there are no sessions at all for the app.
         expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS;
         mQuotaController.updateExecutionStatsLocked(0, "com.android.test.not.run", inputStats);
@@ -520,6 +526,7 @@
         assertEquals(expectedStats, inputStats);
 
         inputStats.windowSizeMs = expectedStats.windowSizeMs = HOUR_IN_MILLIS;
+        inputStats.sessionCountLimit = expectedStats.sessionCountLimit = 2;
         // Invalid time is now since the start of the session is at the very edge of the window
         // cutoff time.
         expectedStats.expirationTimeElapsed = now;
@@ -528,10 +535,13 @@
         expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
         expectedStats.bgJobCountInMaxPeriod = 15;
         expectedStats.sessionCountInWindow = 3;
+        expectedStats.inQuotaTimeElapsed = now + 11 * MINUTE_IN_MILLIS;
         mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
         assertEquals(expectedStats, inputStats);
 
         inputStats.windowSizeMs = expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS;
+        inputStats.jobCountLimit = expectedStats.jobCountLimit = 6;
+        inputStats.sessionCountLimit = expectedStats.sessionCountLimit = 100;
         // Invalid time is now since the session straddles the window cutoff time.
         expectedStats.expirationTimeElapsed = now;
         expectedStats.executionTimeInWindowMs = 11 * MINUTE_IN_MILLIS;
@@ -539,8 +549,7 @@
         expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
         expectedStats.bgJobCountInMaxPeriod = 15;
         expectedStats.sessionCountInWindow = 4;
-        expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
-                + mQcConstants.IN_QUOTA_BUFFER_MS;
+        expectedStats.inQuotaTimeElapsed = now + 5 * MINUTE_IN_MILLIS;
         mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
         assertEquals(expectedStats, inputStats);
 
@@ -553,8 +562,9 @@
         expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
         expectedStats.bgJobCountInMaxPeriod = 15;
         expectedStats.sessionCountInWindow = 4;
-        expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
-                + mQcConstants.IN_QUOTA_BUFFER_MS;
+        // App goes under job execution time limit in ~61 minutes, but will be under job count limit
+        // in 65 minutes.
+        expectedStats.inQuotaTimeElapsed = now + 65 * MINUTE_IN_MILLIS;
         mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
         assertEquals(expectedStats, inputStats);
 
@@ -567,8 +577,7 @@
         expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
         expectedStats.bgJobCountInMaxPeriod = 15;
         expectedStats.sessionCountInWindow = 5;
-        expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
-                + mQcConstants.IN_QUOTA_BUFFER_MS;
+        expectedStats.inQuotaTimeElapsed = now + 4 * HOUR_IN_MILLIS + 5 * MINUTE_IN_MILLIS;
         mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
         assertEquals(expectedStats, inputStats);
 
@@ -577,6 +586,7 @@
                 .add(0,
                         createTimingSession(now - (23 * HOUR_IN_MILLIS), MINUTE_IN_MILLIS, 3));
         inputStats.windowSizeMs = expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
+        inputStats.jobCountLimit = expectedStats.jobCountLimit = 100;
         // Invalid time is now +1 hour since the earliest session in the max period is 1 hour
         // before the end of the max period cutoff time.
         expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS;
@@ -585,7 +595,7 @@
         expectedStats.executionTimeInMaxPeriodMs = 23 * MINUTE_IN_MILLIS;
         expectedStats.bgJobCountInMaxPeriod = 18;
         expectedStats.sessionCountInWindow = 5;
-        expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
+        expectedStats.inQuotaTimeElapsed = now + 6 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS
                 + mQcConstants.IN_QUOTA_BUFFER_MS;
         mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
         assertEquals(expectedStats, inputStats);
@@ -602,7 +612,7 @@
         expectedStats.executionTimeInMaxPeriodMs = 24 * MINUTE_IN_MILLIS;
         expectedStats.bgJobCountInMaxPeriod = 20;
         expectedStats.sessionCountInWindow = 5;
-        expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
+        expectedStats.inQuotaTimeElapsed = now + 6 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS
                 + mQcConstants.IN_QUOTA_BUFFER_MS;
         mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
         assertEquals(expectedStats, inputStats);
@@ -627,6 +637,8 @@
 
         // Active
         expectedStats.windowSizeMs = 10 * MINUTE_IN_MILLIS;
+        expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_ACTIVE;
+        expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_ACTIVE;
         expectedStats.expirationTimeElapsed = now + 4 * MINUTE_IN_MILLIS;
         expectedStats.executionTimeInWindowMs = 3 * MINUTE_IN_MILLIS;
         expectedStats.bgJobCountInWindow = 5;
@@ -638,45 +650,103 @@
 
         // Working
         expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS;
+        expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_WORKING;
+        expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_WORKING;
         expectedStats.expirationTimeElapsed = now;
         expectedStats.executionTimeInWindowMs = 13 * MINUTE_IN_MILLIS;
         expectedStats.bgJobCountInWindow = 10;
         expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
         expectedStats.bgJobCountInMaxPeriod = 20;
         expectedStats.sessionCountInWindow = 2;
-        expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS)
+        expectedStats.inQuotaTimeElapsed = now + 3 * MINUTE_IN_MILLIS
                 + mQcConstants.IN_QUOTA_BUFFER_MS;
         assertEquals(expectedStats,
                 mQuotaController.getExecutionStatsLocked(0, "com.android.test", WORKING_INDEX));
 
         // Frequent
         expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
+        expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_FREQUENT;
+        expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_FREQUENT;
         expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS;
         expectedStats.executionTimeInWindowMs = 23 * MINUTE_IN_MILLIS;
         expectedStats.bgJobCountInWindow = 15;
         expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
         expectedStats.bgJobCountInMaxPeriod = 20;
         expectedStats.sessionCountInWindow = 3;
-        expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS)
+        expectedStats.inQuotaTimeElapsed = now + 6 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS
                 + mQcConstants.IN_QUOTA_BUFFER_MS;
         assertEquals(expectedStats,
                 mQuotaController.getExecutionStatsLocked(0, "com.android.test", FREQUENT_INDEX));
 
         // Rare
         expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
+        expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_RARE;
+        expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_RARE;
         expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS;
         expectedStats.executionTimeInWindowMs = 33 * MINUTE_IN_MILLIS;
         expectedStats.bgJobCountInWindow = 20;
         expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
         expectedStats.bgJobCountInMaxPeriod = 20;
         expectedStats.sessionCountInWindow = 4;
-        expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS)
+        expectedStats.inQuotaTimeElapsed = now + 22 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS
                 + mQcConstants.IN_QUOTA_BUFFER_MS;
         assertEquals(expectedStats,
                 mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX));
     }
 
     /**
+     * Tests that getExecutionStatsLocked returns the correct stats soon after device startup.
+     */
+    @Test
+    public void testGetExecutionStatsLocked_Values_BeginningOfTime() {
+        // Set time to 3 minutes after boot.
+        advanceElapsedClock(-JobSchedulerService.sElapsedRealtimeClock.millis());
+        advanceElapsedClock(3 * MINUTE_IN_MILLIS);
+
+        mQuotaController.saveTimingSession(0, "com.android.test",
+                createTimingSession(MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 2));
+
+        ExecutionStats expectedStats = new ExecutionStats();
+
+        // Active
+        expectedStats.windowSizeMs = 10 * MINUTE_IN_MILLIS;
+        expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_ACTIVE;
+        expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_ACTIVE;
+        expectedStats.expirationTimeElapsed = 11 * MINUTE_IN_MILLIS;
+        expectedStats.executionTimeInWindowMs = MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInWindow = 2;
+        expectedStats.executionTimeInMaxPeriodMs = MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInMaxPeriod = 2;
+        expectedStats.sessionCountInWindow = 1;
+        assertEquals(expectedStats,
+                mQuotaController.getExecutionStatsLocked(0, "com.android.test", ACTIVE_INDEX));
+
+        // Working
+        expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS;
+        expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_WORKING;
+        expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_WORKING;
+        expectedStats.expirationTimeElapsed = 2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS;
+        assertEquals(expectedStats,
+                mQuotaController.getExecutionStatsLocked(0, "com.android.test", WORKING_INDEX));
+
+        // Frequent
+        expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
+        expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_FREQUENT;
+        expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_FREQUENT;
+        expectedStats.expirationTimeElapsed = 8 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS;
+        assertEquals(expectedStats,
+                mQuotaController.getExecutionStatsLocked(0, "com.android.test", FREQUENT_INDEX));
+
+        // Rare
+        expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
+        expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_RARE;
+        expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_RARE;
+        expectedStats.expirationTimeElapsed = 24 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS;
+        assertEquals(expectedStats,
+                mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX));
+    }
+
+    /**
      * Tests that getExecutionStatsLocked returns the correct timing session stats when coalescing.
      */
     @Test
@@ -850,13 +920,15 @@
 
         ExecutionStats expectedStats = new ExecutionStats();
         expectedStats.windowSizeMs = originalStatsActive.windowSizeMs;
+        expectedStats.jobCountLimit = originalStatsActive.jobCountLimit;
+        expectedStats.sessionCountLimit = originalStatsActive.sessionCountLimit;
         expectedStats.expirationTimeElapsed = originalStatsActive.expirationTimeElapsed;
         expectedStats.executionTimeInWindowMs = originalStatsActive.executionTimeInWindowMs;
         expectedStats.bgJobCountInWindow = originalStatsActive.bgJobCountInWindow;
         expectedStats.executionTimeInMaxPeriodMs = originalStatsActive.executionTimeInMaxPeriodMs;
         expectedStats.bgJobCountInMaxPeriod = originalStatsActive.bgJobCountInMaxPeriod;
         expectedStats.sessionCountInWindow = originalStatsActive.sessionCountInWindow;
-        expectedStats.quotaCutoffTimeElapsed = originalStatsActive.quotaCutoffTimeElapsed;
+        expectedStats.inQuotaTimeElapsed = originalStatsActive.inQuotaTimeElapsed;
         final ExecutionStats newStatsActive = mQuotaController.getExecutionStatsLocked(0,
                 "com.android.test", ACTIVE_INDEX);
         // Stats for the same bucket should use the same object.
@@ -864,33 +936,39 @@
         assertEquals(expectedStats, newStatsActive);
 
         expectedStats.windowSizeMs = originalStatsWorking.windowSizeMs;
+        expectedStats.jobCountLimit = originalStatsWorking.jobCountLimit;
+        expectedStats.sessionCountLimit = originalStatsWorking.sessionCountLimit;
         expectedStats.expirationTimeElapsed = originalStatsWorking.expirationTimeElapsed;
         expectedStats.executionTimeInWindowMs = originalStatsWorking.executionTimeInWindowMs;
         expectedStats.bgJobCountInWindow = originalStatsWorking.bgJobCountInWindow;
         expectedStats.sessionCountInWindow = originalStatsWorking.sessionCountInWindow;
-        expectedStats.quotaCutoffTimeElapsed = originalStatsWorking.quotaCutoffTimeElapsed;
+        expectedStats.inQuotaTimeElapsed = originalStatsWorking.inQuotaTimeElapsed;
         final ExecutionStats newStatsWorking = mQuotaController.getExecutionStatsLocked(0,
                 "com.android.test", WORKING_INDEX);
         assertTrue(originalStatsWorking == newStatsWorking);
         assertNotEquals(expectedStats, newStatsWorking);
 
         expectedStats.windowSizeMs = originalStatsFrequent.windowSizeMs;
+        expectedStats.jobCountLimit = originalStatsFrequent.jobCountLimit;
+        expectedStats.sessionCountLimit = originalStatsFrequent.sessionCountLimit;
         expectedStats.expirationTimeElapsed = originalStatsFrequent.expirationTimeElapsed;
         expectedStats.executionTimeInWindowMs = originalStatsFrequent.executionTimeInWindowMs;
         expectedStats.bgJobCountInWindow = originalStatsFrequent.bgJobCountInWindow;
         expectedStats.sessionCountInWindow = originalStatsFrequent.sessionCountInWindow;
-        expectedStats.quotaCutoffTimeElapsed = originalStatsFrequent.quotaCutoffTimeElapsed;
+        expectedStats.inQuotaTimeElapsed = originalStatsFrequent.inQuotaTimeElapsed;
         final ExecutionStats newStatsFrequent = mQuotaController.getExecutionStatsLocked(0,
                 "com.android.test", FREQUENT_INDEX);
         assertTrue(originalStatsFrequent == newStatsFrequent);
         assertNotEquals(expectedStats, newStatsFrequent);
 
         expectedStats.windowSizeMs = originalStatsRare.windowSizeMs;
+        expectedStats.jobCountLimit = originalStatsRare.jobCountLimit;
+        expectedStats.sessionCountLimit = originalStatsRare.sessionCountLimit;
         expectedStats.expirationTimeElapsed = originalStatsRare.expirationTimeElapsed;
         expectedStats.executionTimeInWindowMs = originalStatsRare.executionTimeInWindowMs;
         expectedStats.bgJobCountInWindow = originalStatsRare.bgJobCountInWindow;
         expectedStats.sessionCountInWindow = originalStatsRare.sessionCountInWindow;
-        expectedStats.quotaCutoffTimeElapsed = originalStatsRare.quotaCutoffTimeElapsed;
+        expectedStats.inQuotaTimeElapsed = originalStatsRare.inQuotaTimeElapsed;
         final ExecutionStats newStatsRare = mQuotaController.getExecutionStatsLocked(0,
                 "com.android.test", RARE_INDEX);
         assertTrue(originalStatsRare == newStatsRare);
@@ -1065,7 +1143,7 @@
     public void testIsWithinQuotaLocked_UnderDuration_OverJobCount() {
         setDischarging();
         final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
-        final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME;
+        final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
         mQuotaController.saveTimingSession(0, "com.android.test.spam",
                 createTimingSession(now - (HOUR_IN_MILLIS), 15 * MINUTE_IN_MILLIS, 25));
         mQuotaController.saveTimingSession(0, "com.android.test.spam",
@@ -1100,7 +1178,7 @@
     public void testIsWithinQuotaLocked_OverDuration_OverJobCount() {
         setDischarging();
         final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
-        final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME;
+        final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
         mQuotaController.saveTimingSession(0, "com.android.test",
                 createTimingSession(now - (HOUR_IN_MILLIS), 15 * MINUTE_IN_MILLIS, 25));
         mQuotaController.saveTimingSession(0, "com.android.test",
@@ -1141,7 +1219,7 @@
         advanceElapsedClock(10 * MINUTE_IN_MILLIS + 30 * SECOND_IN_MILLIS);
 
         assertEquals(2, mQuotaController.getExecutionStatsLocked(
-                SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX).jobCountInAllowedTime);
+                SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX).jobCountInRateLimitingWindow);
         assertTrue(mQuotaController.isWithinQuotaLocked(jobStatus));
     }
 
@@ -1212,9 +1290,9 @@
                 mQuotaController.getTimingSessions(SOURCE_USER_ID, fgChangerPkgName).size());
         for (int i = ACTIVE_INDEX; i < RARE_INDEX; ++i) {
             assertEquals(42, mQuotaController.getExecutionStatsLocked(
-                    SOURCE_USER_ID, fgChangerPkgName, i).jobCountInAllowedTime);
+                    SOURCE_USER_ID, fgChangerPkgName, i).jobCountInRateLimitingWindow);
             assertEquals(1, mQuotaController.getExecutionStatsLocked(
-                    SOURCE_USER_ID, unaffectedPkgName, i).jobCountInAllowedTime);
+                    SOURCE_USER_ID, unaffectedPkgName, i).jobCountInRateLimitingWindow);
         }
     }
 
@@ -1555,26 +1633,29 @@
     }
 
     @Test
-    public void testMaybeScheduleStartAlarmLocked_JobCount_AllowedTime() {
+    public void testMaybeScheduleStartAlarmLocked_JobCount_RateLimitingWindow() {
+        // Set rate limiting period different from allowed time to confirm code sets based on
+        // the former.
+        mQcConstants.ALLOWED_TIME_PER_PERIOD_MS = 10 * MINUTE_IN_MILLIS;
+        mQcConstants.RATE_LIMITING_WINDOW_MS = 5 * MINUTE_IN_MILLIS;
+        mQcConstants.updateConstants();
+
         final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
         final int standbyBucket = WORKING_INDEX;
         ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID,
                 SOURCE_PACKAGE, standbyBucket);
-        stats.jobCountInAllowedTime =
-                mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME + 2;
+        stats.jobCountInRateLimitingWindow =
+                mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW + 2;
 
         // Invalid time in the past, so the count shouldn't be used.
-        stats.jobCountExpirationTimeElapsed =
-                now - mQuotaController.getAllowedTimePerPeriodMs() / 2;
+        stats.jobRateLimitExpirationTimeElapsed = now - 5 * MINUTE_IN_MILLIS / 2;
         mQuotaController.maybeScheduleStartAlarmLocked(
                 SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
-        // Invalid time in the future, so the count should be used.
-        stats.jobCountExpirationTimeElapsed =
-                now + mQuotaController.getAllowedTimePerPeriodMs() / 2;
-        final long expectedWorkingAlarmTime =
-                stats.jobCountExpirationTimeElapsed + mQuotaController.getAllowedTimePerPeriodMs();
+        // Valid time in the future, so the count should be used.
+        stats.jobRateLimitExpirationTimeElapsed = now + 5 * MINUTE_IN_MILLIS / 2;
+        final long expectedWorkingAlarmTime = stats.jobRateLimitExpirationTimeElapsed;
         mQuotaController.maybeScheduleStartAlarmLocked(
                 SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         verify(mAlarmManager, times(1))
@@ -1732,12 +1813,13 @@
         mQcConstants.MAX_JOB_COUNT_WORKING = 4000;
         mQcConstants.MAX_JOB_COUNT_FREQUENT = 3000;
         mQcConstants.MAX_JOB_COUNT_RARE = 2000;
-        mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME = 500;
+        mQcConstants.RATE_LIMITING_WINDOW_MS = 15 * MINUTE_IN_MILLIS;
+        mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 500;
         mQcConstants.MAX_SESSION_COUNT_ACTIVE = 500;
         mQcConstants.MAX_SESSION_COUNT_WORKING = 400;
         mQcConstants.MAX_SESSION_COUNT_FREQUENT = 300;
         mQcConstants.MAX_SESSION_COUNT_RARE = 200;
-        mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME = 50;
+        mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 50;
         mQcConstants.TIMING_SESSION_COALESCING_DURATION_MS = 10 * SECOND_IN_MILLIS;
 
         mQcConstants.updateConstants();
@@ -1750,12 +1832,13 @@
                 mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]);
         assertEquals(60 * MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]);
         assertEquals(3 * HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs());
-        assertEquals(500, mQuotaController.getMaxJobCountPerAllowedTime());
+        assertEquals(15 * MINUTE_IN_MILLIS, mQuotaController.getRateLimitingWindowMs());
+        assertEquals(500, mQuotaController.getMaxJobCountPerRateLimitingWindow());
         assertEquals(5000, mQuotaController.getBucketMaxJobCounts()[ACTIVE_INDEX]);
         assertEquals(4000, mQuotaController.getBucketMaxJobCounts()[WORKING_INDEX]);
         assertEquals(3000, mQuotaController.getBucketMaxJobCounts()[FREQUENT_INDEX]);
         assertEquals(2000, mQuotaController.getBucketMaxJobCounts()[RARE_INDEX]);
-        assertEquals(50, mQuotaController.getMaxSessionCountPerAllowedTime());
+        assertEquals(50, mQuotaController.getMaxSessionCountPerRateLimitingWindow());
         assertEquals(500, mQuotaController.getBucketMaxSessionCounts()[ACTIVE_INDEX]);
         assertEquals(400, mQuotaController.getBucketMaxSessionCounts()[WORKING_INDEX]);
         assertEquals(300, mQuotaController.getBucketMaxSessionCounts()[FREQUENT_INDEX]);
@@ -1778,12 +1861,13 @@
         mQcConstants.MAX_JOB_COUNT_WORKING = 1;
         mQcConstants.MAX_JOB_COUNT_FREQUENT = 1;
         mQcConstants.MAX_JOB_COUNT_RARE = 1;
-        mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME = 0;
+        mQcConstants.RATE_LIMITING_WINDOW_MS = 15 * SECOND_IN_MILLIS;
+        mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 0;
         mQcConstants.MAX_SESSION_COUNT_ACTIVE = -1;
-        mQcConstants.MAX_SESSION_COUNT_WORKING = 1;
-        mQcConstants.MAX_SESSION_COUNT_FREQUENT = 2;
-        mQcConstants.MAX_SESSION_COUNT_RARE = 1;
-        mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME = 0;
+        mQcConstants.MAX_SESSION_COUNT_WORKING = 0;
+        mQcConstants.MAX_SESSION_COUNT_FREQUENT = -3;
+        mQcConstants.MAX_SESSION_COUNT_RARE = 0;
+        mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 0;
         mQcConstants.TIMING_SESSION_COALESCING_DURATION_MS = -1;
 
         mQcConstants.updateConstants();
@@ -1795,16 +1879,17 @@
         assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]);
         assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]);
         assertEquals(HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs());
-        assertEquals(10, mQuotaController.getMaxJobCountPerAllowedTime());
-        assertEquals(100, mQuotaController.getBucketMaxJobCounts()[ACTIVE_INDEX]);
-        assertEquals(100, mQuotaController.getBucketMaxJobCounts()[WORKING_INDEX]);
-        assertEquals(100, mQuotaController.getBucketMaxJobCounts()[FREQUENT_INDEX]);
-        assertEquals(100, mQuotaController.getBucketMaxJobCounts()[RARE_INDEX]);
-        assertEquals(10, mQuotaController.getMaxSessionCountPerAllowedTime());
-        assertEquals(3, mQuotaController.getBucketMaxSessionCounts()[ACTIVE_INDEX]);
-        assertEquals(3, mQuotaController.getBucketMaxSessionCounts()[WORKING_INDEX]);
-        assertEquals(3, mQuotaController.getBucketMaxSessionCounts()[FREQUENT_INDEX]);
-        assertEquals(3, mQuotaController.getBucketMaxSessionCounts()[RARE_INDEX]);
+        assertEquals(30 * SECOND_IN_MILLIS, mQuotaController.getRateLimitingWindowMs());
+        assertEquals(10, mQuotaController.getMaxJobCountPerRateLimitingWindow());
+        assertEquals(10, mQuotaController.getBucketMaxJobCounts()[ACTIVE_INDEX]);
+        assertEquals(10, mQuotaController.getBucketMaxJobCounts()[WORKING_INDEX]);
+        assertEquals(10, mQuotaController.getBucketMaxJobCounts()[FREQUENT_INDEX]);
+        assertEquals(10, mQuotaController.getBucketMaxJobCounts()[RARE_INDEX]);
+        assertEquals(10, mQuotaController.getMaxSessionCountPerRateLimitingWindow());
+        assertEquals(1, mQuotaController.getBucketMaxSessionCounts()[ACTIVE_INDEX]);
+        assertEquals(1, mQuotaController.getBucketMaxSessionCounts()[WORKING_INDEX]);
+        assertEquals(1, mQuotaController.getBucketMaxSessionCounts()[FREQUENT_INDEX]);
+        assertEquals(1, mQuotaController.getBucketMaxSessionCounts()[RARE_INDEX]);
         assertEquals(0, mQuotaController.getTimingSessionCoalescingDurationMs());
 
         // Test larger than a day. Controller should cap at one day.
@@ -1815,6 +1900,7 @@
         mQcConstants.WINDOW_SIZE_FREQUENT_MS = 25 * HOUR_IN_MILLIS;
         mQcConstants.WINDOW_SIZE_RARE_MS = 25 * HOUR_IN_MILLIS;
         mQcConstants.MAX_EXECUTION_TIME_MS = 25 * HOUR_IN_MILLIS;
+        mQcConstants.RATE_LIMITING_WINDOW_MS = 25 * HOUR_IN_MILLIS;
         mQcConstants.TIMING_SESSION_COALESCING_DURATION_MS = 25 * HOUR_IN_MILLIS;
 
         mQcConstants.updateConstants();
@@ -1826,6 +1912,7 @@
         assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]);
         assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]);
         assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs());
+        assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getRateLimitingWindowMs());
         assertEquals(15 * MINUTE_IN_MILLIS,
                 mQuotaController.getTimingSessionCoalescingDurationMs());
     }
@@ -2126,7 +2213,7 @@
         assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
         ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID,
                 SOURCE_PACKAGE, standbyBucket);
-        assertEquals(0, stats.jobCountInAllowedTime);
+        assertEquals(0, stats.jobCountInRateLimitingWindow);
 
         setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
         mQuotaController.prepareForExecutionLocked(jobFg1);
@@ -2138,7 +2225,7 @@
         mQuotaController.maybeStopTrackingJobLocked(jobFg2, null, false);
         assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
 
-        assertEquals(0, stats.jobCountInAllowedTime);
+        assertEquals(0, stats.jobCountInRateLimitingWindow);
     }
 
     /**
@@ -2154,7 +2241,7 @@
 
         ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID,
                 SOURCE_PACKAGE, standbyBucket);
-        assertEquals(0, stats.jobCountInAllowedTime);
+        assertEquals(0, stats.jobCountInRateLimitingWindow);
 
         setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING);
         mQuotaController.prepareForExecutionLocked(jobBg1);
@@ -2165,7 +2252,7 @@
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
 
-        assertEquals(2, stats.jobCountInAllowedTime);
+        assertEquals(2, stats.jobCountInRateLimitingWindow);
     }
 
     /**
@@ -2363,6 +2450,8 @@
                 timeout(remainingTimeMs + 2 * SECOND_IN_MILLIS).times(1))
                 .onControllerStateChanged();
         assertFalse(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+        assertEquals(JobSchedulerService.sElapsedRealtimeClock.millis(),
+                jobStatus.getWhenStandbyDeferred());
     }
 
     /**
@@ -2421,10 +2510,10 @@
 
     /**
      * Tests that the start alarm is properly scheduled when a job has been throttled due to the job
-     * count quota.
+     * count rate limiting.
      */
     @Test
-    public void testStartAlarmScheduled_JobCount_AllowedTime() {
+    public void testStartAlarmScheduled_JobCount_RateLimitingWindow() {
         // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
         // because it schedules an alarm too. Prevent it from doing so.
         spyOn(mQuotaController);
@@ -2432,7 +2521,7 @@
 
         // Essentially disable session throttling.
         mQcConstants.MAX_SESSION_COUNT_WORKING =
-                mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME = Integer.MAX_VALUE;
+                mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = Integer.MAX_VALUE;
         mQcConstants.updateConstants();
 
         final int standbyBucket = WORKING_INDEX;
@@ -2444,7 +2533,7 @@
         verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Ran jobs up to the job limit. All of them should be allowed to run.
-        for (int i = 0; i < mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME; ++i) {
+        for (int i = 0; i < mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; ++i) {
             JobStatus job = createJobStatus("testStartAlarmScheduled_JobCount_AllowedTime", i);
             setStandbyBucket(WORKING_INDEX, job);
             mQuotaController.maybeStartTrackingJobLocked(job, null);
@@ -2466,18 +2555,17 @@
 
         ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID,
                 SOURCE_PACKAGE, standbyBucket);
-        final long expectedWorkingAlarmTime =
-                stats.jobCountExpirationTimeElapsed + mQcConstants.ALLOWED_TIME_PER_PERIOD_MS;
+        final long expectedWorkingAlarmTime = stats.jobRateLimitExpirationTimeElapsed;
         verify(mAlarmManager, times(1))
                 .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
     }
 
     /**
-     * Tests that the start alarm is properly scheduled when a job has been throttled due to the job
-     * count quota.
+     * Tests that the start alarm is properly scheduled when a job has been throttled due to the
+     * session count rate limiting.
      */
     @Test
-    public void testStartAlarmScheduled_TimingSessionCount_AllowedTime() {
+    public void testStartAlarmScheduled_TimingSessionCount_RateLimitingWindow() {
         // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
         // because it schedules an alarm too. Prevent it from doing so.
         spyOn(mQuotaController);
@@ -2485,10 +2573,10 @@
 
         // Essentially disable job count throttling.
         mQcConstants.MAX_JOB_COUNT_FREQUENT =
-                mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME = Integer.MAX_VALUE;
-        // Make sure throttling is because of COUNT_PER_ALLOWED_TIME.
+                mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = Integer.MAX_VALUE;
+        // Make sure throttling is because of COUNT_PER_RATE_LIMITING_WINDOW.
         mQcConstants.MAX_SESSION_COUNT_FREQUENT =
-                mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME + 1;
+                mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW + 1;
         mQcConstants.updateConstants();
 
         final int standbyBucket = FREQUENT_INDEX;
@@ -2500,7 +2588,7 @@
         verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Ran jobs up to the job limit. All of them should be allowed to run.
-        for (int i = 0; i < mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME; ++i) {
+        for (int i = 0; i < mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW; ++i) {
             JobStatus job = createJobStatus(
                     "testStartAlarmScheduled_TimingSessionCount_AllowedTime", i);
             setStandbyBucket(FREQUENT_INDEX, job);
@@ -2515,16 +2603,17 @@
         // Start alarm shouldn't have been scheduled since the app was in quota up until this point.
         verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
-        // The app is now out of job count quota
+        // The app is now out of session count quota
         JobStatus throttledJob = createJobStatus(
                 "testStartAlarmScheduled_TimingSessionCount_AllowedTime", 42);
         mQuotaController.maybeStartTrackingJobLocked(throttledJob, null);
         assertFalse(throttledJob.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+        assertEquals(JobSchedulerService.sElapsedRealtimeClock.millis(),
+                throttledJob.getWhenStandbyDeferred());
 
         ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID,
                 SOURCE_PACKAGE, standbyBucket);
-        final long expectedWorkingAlarmTime =
-                stats.sessionCountExpirationTimeElapsed + mQcConstants.ALLOWED_TIME_PER_PERIOD_MS;
+        final long expectedWorkingAlarmTime = stats.sessionRateLimitExpirationTimeElapsed;
         verify(mAlarmManager, times(1))
                 .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
     }
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
index fa1bcac..fd3678d 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
@@ -423,9 +423,7 @@
         assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
         assertEquals(null, si.getIntent());
         assertEquals(123, si.getRank());
-        assertEquals("person", si.getPersons()[0].getName());
-        assertEquals("personKey", si.getPersons()[0].getKey());
-        assertEquals("personUri", si.getPersons()[0].getUri());
+        assertEquals(null, si.getPersons());
         assertEquals(1, si.getExtras().getInt("k"));
 
         assertEquals(ShortcutInfo.FLAG_PINNED | ShortcutInfo.FLAG_LONG_LIVED, si.getFlags());
@@ -455,6 +453,30 @@
 
         assertEquals(456, si.getIconResourceId());
         assertEquals(null, si.getIconResName());
+
+        si = sorig.clone(ShortcutInfo.CLONE_REMOVE_FOR_APP_PREDICTION);
+
+        assertEquals(mClientContext.getPackageName(), si.getPackage());
+        assertEquals("id", si.getId());
+        assertEquals(new ComponentName("a", "b"), si.getActivity());
+        assertEquals(null, si.getIcon());
+        assertEquals("title", si.getTitle());
+        assertEquals("text", si.getText());
+        assertEquals("dismes", si.getDisabledMessage());
+        assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
+        assertEquals("action", si.getIntent().getAction());
+        assertEquals("val", si.getIntent().getStringExtra("key"));
+        assertEquals(123, si.getRank());
+        assertEquals("person", si.getPersons()[0].getName());
+        assertEquals("personKey", si.getPersons()[0].getKey());
+        assertEquals("personUri", si.getPersons()[0].getUri());
+        assertEquals(1, si.getExtras().getInt("k"));
+
+        assertEquals(ShortcutInfo.FLAG_PINNED | ShortcutInfo.FLAG_LONG_LIVED, si.getFlags());
+        assertEquals(null, si.getBitmapPath());
+
+        assertEquals(456, si.getIconResourceId());
+        assertEquals(null, si.getIconResName());
     }
 
     public void testShortcutInfoClone_resId() {
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 87f221a..e75a30b 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -22,7 +22,6 @@
 import static android.app.Notification.CATEGORY_CALL;
 import static android.app.Notification.FLAG_BUBBLE;
 import static android.app.Notification.FLAG_FOREGROUND_SERVICE;
-import static android.app.Notification.FLAG_ONLY_ALERT_ONCE;
 import static android.app.NotificationManager.EXTRA_BLOCKED_STATE;
 import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
 import static android.app.NotificationManager.IMPORTANCE_HIGH;
@@ -1660,11 +1659,14 @@
         when(mPreferencesHelper.getNotificationChannel(eq(PKG), anyInt(),
                 eq(channel2.getId()), anyBoolean()))
                 .thenReturn(channel2);
+        when(mPreferencesHelper.createNotificationChannel(eq(PKG), anyInt(),
+                eq(channel2), anyBoolean(), anyBoolean()))
+                .thenReturn(true);
 
         reset(mListeners);
         mBinderService.createNotificationChannels(PKG,
                 new ParceledListSlice(Arrays.asList(mTestNotificationChannel, channel2)));
-        verify(mListeners, times(1)).notifyNotificationChannelChanged(eq(PKG),
+        verify(mListeners, never()).notifyNotificationChannelChanged(eq(PKG),
                 eq(Process.myUserHandle()), eq(mTestNotificationChannel),
                 eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_ADDED));
         verify(mListeners, times(1)).notifyNotificationChannelChanged(eq(PKG),
@@ -4963,13 +4965,10 @@
         mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), false);
         waitForIdle();
 
-        // Make sure we are not a bubble / reported as such to listeners
-        ArgumentCaptor<NotificationRecord> captor =
-                ArgumentCaptor.forClass(NotificationRecord.class);
-        verify(mListeners, times(1)).notifyPostedLocked(captor.capture(), any());
-
-        assertEquals((captor.getValue().getNotification().flags & FLAG_BUBBLE), 0);
-        assertTrue((captor.getValue().getNotification().flags & FLAG_ONLY_ALERT_ONCE) != 0);
+        // Make sure we are not a bubble
+        StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG);
+        assertEquals(1, notifsAfter.length);
+        assertEquals((notifsAfter[0].getNotification().flags & FLAG_BUBBLE), 0);
     }
 
     @Test
@@ -5000,13 +4999,10 @@
         mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), true);
         waitForIdle();
 
-        // Make sure we are a bubble / reported as such to listeners
-        ArgumentCaptor<NotificationRecord> captor =
-                ArgumentCaptor.forClass(NotificationRecord.class);
-        verify(mListeners, times(1)).notifyPostedLocked(captor.capture(), any());
-
-        assertTrue((captor.getValue().getNotification().flags & FLAG_BUBBLE) != 0);
-        assertTrue((captor.getValue().getNotification().flags & FLAG_ONLY_ALERT_ONCE) != 0);
+        // Make sure we are a bubble
+        StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG);
+        assertEquals(1, notifsAfter.length);
+        assertTrue((notifsAfter[0].getNotification().flags & FLAG_BUBBLE) != 0);
     }
 
     @Test
@@ -5037,7 +5033,6 @@
         StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG);
         assertEquals(1, notifsAfter.length);
         assertEquals((notifsAfter[0].getNotification().flags & FLAG_BUBBLE), 0);
-        verify(mListeners, times(0)).notifyPostedLocked(any(), any());
     }
 
     @Test
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 e22f827..8f8b746 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -262,13 +262,13 @@
         int uid0 = 1001;
         setUpPackageWithUid(package0, uid0);
         NotificationChannel channel0 = new NotificationChannel("id0", "name0", IMPORTANCE_HIGH);
-        mHelper.createNotificationChannel(package0, uid0, channel0, true, false);
+        assertTrue(mHelper.createNotificationChannel(package0, uid0, channel0, true, false));
 
         String package10 = "test.package.user10";
         int uid10 = 1001001;
         setUpPackageWithUid(package10, uid10);
         NotificationChannel channel10 = new NotificationChannel("id10", "name10", IMPORTANCE_HIGH);
-        mHelper.createNotificationChannel(package10, uid10, channel10, true, false);
+        assertTrue(mHelper.createNotificationChannel(package10, uid10, channel10, true, false));
 
         ByteArrayOutputStream baos = writeXmlAndPurge(package10, uid10, true, 10);
 
@@ -293,7 +293,7 @@
         int uid0 = 1001;
         setUpPackageWithUid(package0, uid0);
         NotificationChannel channel0 = new NotificationChannel("id0", "name0", IMPORTANCE_HIGH);
-        mHelper.createNotificationChannel(package0, uid0, channel0, true, false);
+        assertTrue(mHelper.createNotificationChannel(package0, uid0, channel0, true, false));
 
         ByteArrayOutputStream baos = writeXmlAndPurge(package0, uid0, true, 0);
 
@@ -334,8 +334,8 @@
 
         mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true);
         mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg2, true);
-        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false);
-        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, false, false);
+        assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false));
+        assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, false, false));
 
         mHelper.setShowBadge(PKG_N_MR1, UID_N_MR1, true);
         mHelper.setAppImportanceLocked(PKG_N_MR1, UID_N_MR1);
@@ -716,8 +716,8 @@
     public void testCreateChannel_blocked() throws Exception {
         mHelper.setImportance(PKG_N_MR1, UID_N_MR1, IMPORTANCE_NONE);
 
-        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1,
-                new NotificationChannel("bananas", "bananas", IMPORTANCE_LOW), true, false);
+        assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1,
+                new NotificationChannel("bananas", "bananas", IMPORTANCE_LOW), true, false));
     }
 
     @Test
@@ -746,10 +746,10 @@
         } catch (IllegalArgumentException e) {
             // yay
         }
-        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1,
-                new NotificationChannel("bananas", "bananas", IMPORTANCE_NONE), true, false);
-        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1,
-                new NotificationChannel("bananas", "bananas", IMPORTANCE_MAX), true, false);
+        assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1,
+                new NotificationChannel("bananas", "bananas", IMPORTANCE_NONE), true, false));
+        assertFalse(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1,
+                new NotificationChannel("bananas", "bananas", IMPORTANCE_MAX), true, false));
     }
 
 
@@ -763,7 +763,7 @@
         channel.setBypassDnd(true);
         channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
 
-        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, false, false);
+        assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, false, false));
 
         // same id, try to update all fields
         final NotificationChannel channel2 =
@@ -776,7 +776,8 @@
         mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, true);
 
         // all fields should be changed
-        assertEquals(channel2, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), false));
+        assertEquals(channel2,
+                mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), false));
 
         verify(mHandler, times(1)).requestSort();
     }
@@ -894,7 +895,7 @@
         }
         channel.lockFields(lockMask);
 
-        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false);
+        assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false));
 
         NotificationChannel savedChannel =
                 mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), false);
@@ -1469,13 +1470,18 @@
                 new NotificationChannel("id2", "name2", IMPORTANCE_LOW);
         channel.setVibrationPattern(vibration);
 
-        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false);
+        assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false));
 
         NotificationChannel newChannel = new NotificationChannel(
                 channel.getId(), channel.getName(), NotificationManager.IMPORTANCE_HIGH);
         newChannel.setVibrationPattern(new long[]{100});
+        newChannel.setAllowBubbles(!channel.canBubble());
+        newChannel.setLightColor(Color.BLUE);
+        newChannel.setSound(Uri.EMPTY, null);
+        newChannel.setShowBadge(!channel.canShowBadge());
 
-        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, false);
+        assertFalse(mHelper.createNotificationChannel(
+                PKG_N_MR1, UID_N_MR1, newChannel, true, false));
 
         // Old settings not overridden
         compareChannels(channel,
@@ -1588,16 +1594,17 @@
                 new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH);
         mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false);
 
-        mHelper.onPackagesChanged(true, UserHandle.USER_SYSTEM, new String[]{PKG_N_MR1}, new int[]{
-                UID_N_MR1});
+        assertTrue(mHelper.onPackagesChanged(true, UserHandle.USER_SYSTEM, new String[]{PKG_N_MR1},
+                new int[]{UID_N_MR1}));
 
-        assertEquals(0, mHelper.getNotificationChannels(PKG_N_MR1, UID_N_MR1, true).getList().size());
+        assertEquals(0, mHelper.getNotificationChannels(
+                PKG_N_MR1, UID_N_MR1, true).getList().size());
 
         // Not deleted
         mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false);
 
-        mHelper.onPackagesChanged(false, UserHandle.USER_SYSTEM, new String[]{PKG_N_MR1}, new int[]{
-                UID_N_MR1});
+        assertFalse(mHelper.onPackagesChanged(false, UserHandle.USER_SYSTEM,
+                new String[]{PKG_N_MR1}, new int[]{UID_N_MR1}));
         assertEquals(2, mHelper.getNotificationChannels(PKG_N_MR1, UID_N_MR1, false).getList().size());
     }
 
@@ -1825,13 +1832,13 @@
     @Test
     public void testCreateChannel_updateName() {
         NotificationChannel nc = new NotificationChannel("id", "hello", IMPORTANCE_DEFAULT);
-        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, nc, true, false);
+        assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, nc, true, false));
         NotificationChannel actual =
                 mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", false);
         assertEquals("hello", actual.getName());
 
         nc = new NotificationChannel("id", "goodbye", IMPORTANCE_HIGH);
-        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, nc, true, false);
+        assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, nc, true, false));
 
         actual = mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", false);
         assertEquals("goodbye", actual.getName());
@@ -1845,14 +1852,14 @@
         NotificationChannelGroup group = new NotificationChannelGroup("group", "");
         mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group, true);
         NotificationChannel nc = new NotificationChannel("id", "hello", IMPORTANCE_DEFAULT);
-        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, nc, true, false);
+        assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, nc, true, false));
         NotificationChannel actual =
                 mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", false);
         assertNull(actual.getGroup());
 
         nc = new NotificationChannel("id", "hello", IMPORTANCE_HIGH);
         nc.setGroup(group.getId());
-        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, nc, true, false);
+        assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, nc, true, false));
 
         actual = mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", false);
         assertNotNull(actual.getGroup());
@@ -2109,7 +2116,7 @@
 
         NotificationChannel update = new NotificationChannel("A", "a", IMPORTANCE_LOW);
         update.setBypassDnd(true);
-        mHelper.createNotificationChannel(SYSTEM_PKG, SYSTEM_UID, update, true, false);
+        assertFalse(mHelper.createNotificationChannel(SYSTEM_PKG, SYSTEM_UID, update, true, false));
 
         assertFalse(mHelper.getNotificationChannel(SYSTEM_PKG, SYSTEM_UID, "A", false)
                 .canBypassDnd());
@@ -2122,7 +2129,7 @@
 
         NotificationChannel update = new NotificationChannel("A", "a", IMPORTANCE_LOW);
         update.setBypassDnd(true);
-        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, update, true, true);
+        assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, update, true, true));
 
         assertTrue(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, "A", false).canBypassDnd());
     }
@@ -2676,4 +2683,11 @@
         assertTrue(mHelper.getNotificationChannel(PKG_O, UID_O, channel1.getId(), false)
                 .isImportanceLockedByCriticalDeviceFunction());
     }
+
+    @Test
+    public void testSetBubblesAllowed_false() {
+        mHelper.setBubblesAllowed(PKG_O, UID_O, false);
+        assertFalse(mHelper.areBubblesAllowed(PKG_O, UID_O));
+        verify(mHandler, times(1)).requestSort();
+    }
 }
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 0f7b35c..c77e25f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootActivityContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootActivityContainerTests.java
@@ -30,6 +30,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.reset;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.server.wm.ActivityDisplay.POSITION_TOP;
@@ -54,6 +55,7 @@
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
 import android.util.Pair;
@@ -601,6 +603,73 @@
     }
 
     /**
+     * Tests that the default secondary home activity is always picked when it is in forced by
+     * config_useSystemProvidedLauncherForSecondary.
+     */
+    @Test
+    public void testResolveSecondaryHomeActivityForced() throws Exception {
+        Resources resources = mContext.getResources();
+        spyOn(resources);
+        try {
+            // setUp: set secondary launcher and force it.
+            final String defaultSecondaryHome =
+                    "com.android.test/com.android.test.TestDefaultSecondaryHome";
+            final ComponentName secondaryComp = ComponentName.unflattenFromString(
+                    defaultSecondaryHome);
+            doReturn(defaultSecondaryHome).when(resources).getString(
+                    com.android.internal.R.string.config_secondaryHomeComponent);
+            doReturn(true).when(resources).getBoolean(
+                    com.android.internal.R.bool.config_useSystemProvidedLauncherForSecondary);
+            final Intent secondaryHomeIntent = mService.getSecondaryHomeIntent(null);
+            assertEquals(secondaryComp, secondaryHomeIntent.getComponent());
+            final ActivityInfo aInfoSecondary = new ActivityInfo();
+            aInfoSecondary.name = secondaryComp.getClassName();
+            aInfoSecondary.applicationInfo = new ApplicationInfo();
+            aInfoSecondary.applicationInfo.packageName = secondaryComp.getPackageName();
+            doReturn(aInfoSecondary).when(mRootActivityContainer).resolveHomeActivity(anyInt(),
+                    refEq(secondaryHomeIntent));
+            final Intent homeIntent = mService.getHomeIntent();
+            final ActivityInfo aInfoDefault = new ActivityInfo();
+            aInfoDefault.name = "fakeHomeActivity";
+            aInfoDefault.applicationInfo = new ApplicationInfo();
+            aInfoDefault.applicationInfo.packageName = "fakeHomePackage";
+            doReturn(aInfoDefault).when(mRootActivityContainer).resolveHomeActivity(anyInt(),
+                    refEq(homeIntent));
+            // Let resolveActivities call to validate both main launcher and second launcher so that
+            // resolveActivities call does not work as enabler for secondary.
+            final List<ResolveInfo> resolutions1 = new ArrayList<>();
+            final ResolveInfo resolveInfo1 = new ResolveInfo();
+            resolveInfo1.activityInfo = new ActivityInfo();
+            resolveInfo1.activityInfo.name = aInfoDefault.name;
+            resolveInfo1.activityInfo.applicationInfo = aInfoDefault.applicationInfo;
+            resolutions1.add(resolveInfo1);
+            doReturn(resolutions1).when(mRootActivityContainer).resolveActivities(anyInt(),
+                    refEq(homeIntent));
+            final List<ResolveInfo> resolutions2 = new ArrayList<>();
+            final ResolveInfo resolveInfo2 = new ResolveInfo();
+            resolveInfo2.activityInfo = new ActivityInfo();
+            resolveInfo2.activityInfo.name = aInfoSecondary.name;
+            resolveInfo2.activityInfo.applicationInfo = aInfoSecondary.applicationInfo;
+            resolutions2.add(resolveInfo2);
+            doReturn(resolutions2).when(mRootActivityContainer).resolveActivities(anyInt(),
+                    refEq(secondaryHomeIntent));
+            doReturn(true).when(mRootActivityContainer).canStartHomeOnDisplay(
+                    any(), anyInt(), anyBoolean());
+
+            // Run the test
+            final Pair<ActivityInfo, Intent> resolvedInfo = mRootActivityContainer
+                    .resolveSecondaryHomeActivity(0 /* userId */, 1 /* displayId */);
+            assertEquals(secondaryComp.getClassName(), resolvedInfo.first.name);
+            assertEquals(secondaryComp.getPackageName(),
+                    resolvedInfo.first.applicationInfo.packageName);
+            assertEquals(aInfoSecondary.name, resolvedInfo.first.name);
+        } finally {
+            // tearDown
+            reset(resources);
+        }
+    }
+
+    /**
      * Tests that secondary home should be selected if default home not support secondary displays
      * or there is no matched activity in the same package as selected default home.
      */
diff --git a/services/usb/java/com/android/server/usb/UsbSerialReader.java b/services/usb/java/com/android/server/usb/UsbSerialReader.java
index 32fc796..8ca77f0 100644
--- a/services/usb/java/com/android/server/usb/UsbSerialReader.java
+++ b/services/usb/java/com/android/server/usb/UsbSerialReader.java
@@ -85,22 +85,22 @@
                     throw new RemoteException("package " + packageName + " cannot be found");
                 }
                 packageTargetSdkVersion = pkg.applicationInfo.targetSdkVersion;
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
 
-            if (packageTargetSdkVersion >= Build.VERSION_CODES.Q) {
-                if (mContext.checkPermission(android.Manifest.permission.MANAGE_USB, pid, uid)
-                        == PackageManager.PERMISSION_DENIED) {
-                    UsbUserSettingsManager settings = mSettingsManager.getSettingsForUser(
-                            UserHandle.getUserId(uid));
+                if (packageTargetSdkVersion >= Build.VERSION_CODES.Q) {
+                    if (mContext.checkPermission(android.Manifest.permission.MANAGE_USB, pid, uid)
+                            == PackageManager.PERMISSION_DENIED) {
+                        UsbUserSettingsManager settings = mSettingsManager.getSettingsForUser(
+                                UserHandle.getUserId(uid));
 
-                    if (mDevice instanceof UsbDevice) {
-                        settings.checkPermission((UsbDevice) mDevice, packageName, uid);
-                    } else {
-                        settings.checkPermission((UsbAccessory) mDevice, uid);
+                        if (mDevice instanceof UsbDevice) {
+                            settings.checkPermission((UsbDevice) mDevice, packageName, uid);
+                        } else {
+                            settings.checkPermission((UsbAccessory) mDevice, uid);
+                        }
                     }
                 }
+            } finally {
+                Binder.restoreCallingIdentity(token);
             }
         }
 
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 5175c1d..a125e31 100755
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -1973,26 +1973,6 @@
     public static final String KEY_CARRIER_WIFI_STRING_ARRAY = "carrier_wifi_string_array";
 
     /**
-     * Base64 Encoding method the carrier will use for encoding encrypted IMSI and SSID.
-     * The value set as below:
-     * 2045 - RFC2045 (default value)
-     * 4648 - RFC4648
-     *
-     * @hide
-     */
-    public static final String KEY_IMSI_ENCODING_METHOD_INT = "imsi_encoding_method_int";
-
-    /**
-     * Defines the sequence of sending an encrypted IMSI identity for EAP-SIM/AKA authentication.
-     * The value set as below:
-     * 1 - encrypted IMSI as EAP-RESPONSE/IDENTITY (default one).
-     * 2 - anonymous as EAP-RESPONSE/IDENTITY -> encrypted IMSI as EAP-RESPONSE/AKA|SIM-IDENTITY.
-     *
-     * @hide
-     */
-    public static final String KEY_EAP_IDENTITY_SEQUENCE_INT = "imsi_eap_identity_sequence_int";
-
-    /**
      * Time delay (in ms) after which we show the notification to switch the preferred
      * network.
      * @hide
@@ -3265,8 +3245,6 @@
         sDefaults.putBoolean(KEY_STK_DISABLE_LAUNCH_BROWSER_BOOL, false);
         sDefaults.putBoolean(KEY_ALLOW_METERED_NETWORK_FOR_CERT_DOWNLOAD_BOOL, false);
         sDefaults.putStringArray(KEY_CARRIER_WIFI_STRING_ARRAY, null);
-        sDefaults.putInt(KEY_IMSI_ENCODING_METHOD_INT, 2045);
-        sDefaults.putInt(KEY_EAP_IDENTITY_SEQUENCE_INT, 1);
         sDefaults.putInt(KEY_PREF_NETWORK_NOTIFICATION_DELAY_INT, -1);
         sDefaults.putInt(KEY_EMERGENCY_NOTIFICATION_DELAY_INT, -1);
         sDefaults.putBoolean(KEY_ALLOW_USSD_REQUESTS_VIA_TELEPHONY_MANAGER_BOOL, true);
diff --git a/telephony/java/android/telephony/ServiceState.java b/telephony/java/android/telephony/ServiceState.java
index 984d8f7..5d39a2c 100644
--- a/telephony/java/android/telephony/ServiceState.java
+++ b/telephony/java/android/telephony/ServiceState.java
@@ -341,6 +341,7 @@
 
     private String mOperatorAlphaLongRaw;
     private String mOperatorAlphaShortRaw;
+    private boolean mIsIwlanPreferred;
 
     /**
      * get String description of roaming type
@@ -427,6 +428,7 @@
         mNrFrequencyRange = s.mNrFrequencyRange;
         mOperatorAlphaLongRaw = s.mOperatorAlphaLongRaw;
         mOperatorAlphaShortRaw = s.mOperatorAlphaShortRaw;
+        mIsIwlanPreferred = s.mIsIwlanPreferred;
     }
 
     /**
@@ -463,6 +465,7 @@
         mNrFrequencyRange = in.readInt();
         mOperatorAlphaLongRaw = in.readString();
         mOperatorAlphaShortRaw = in.readString();
+        mIsIwlanPreferred = in.readBoolean();
     }
 
     public void writeToParcel(Parcel out, int flags) {
@@ -492,6 +495,7 @@
         out.writeInt(mNrFrequencyRange);
         out.writeString(mOperatorAlphaLongRaw);
         out.writeString(mOperatorAlphaShortRaw);
+        out.writeBoolean(mIsIwlanPreferred);
     }
 
     public int describeContents() {
@@ -853,7 +857,8 @@
                     mNetworkRegistrationInfos,
                     mNrFrequencyRange,
                     mOperatorAlphaLongRaw,
-                    mOperatorAlphaShortRaw);
+                    mOperatorAlphaShortRaw,
+                    mIsIwlanPreferred);
         }
     }
 
@@ -885,7 +890,8 @@
                     && equalsHandlesNulls(mOperatorAlphaShortRaw, s.mOperatorAlphaShortRaw)
                     && mNetworkRegistrationInfos.size() == s.mNetworkRegistrationInfos.size()
                     && mNetworkRegistrationInfos.containsAll(s.mNetworkRegistrationInfos)
-                    && mNrFrequencyRange == s.mNrFrequencyRange;
+                    && mNrFrequencyRange == s.mNrFrequencyRange
+                    && mIsIwlanPreferred == s.mIsIwlanPreferred;
         }
     }
 
@@ -1043,6 +1049,7 @@
                     .append(", mNrFrequencyRange=").append(mNrFrequencyRange)
                     .append(", mOperatorAlphaLongRaw=").append(mOperatorAlphaLongRaw)
                     .append(", mOperatorAlphaShortRaw=").append(mOperatorAlphaShortRaw)
+                    .append(", mIsIwlanPreferred=").append(mIsIwlanPreferred)
                     .append("}").toString();
         }
     }
@@ -1085,6 +1092,7 @@
         }
         mOperatorAlphaLongRaw = null;
         mOperatorAlphaShortRaw = null;
+        mIsIwlanPreferred = false;
     }
 
     public void setStateOutOfService() {
@@ -1459,20 +1467,9 @@
     /** @hide */
     @UnsupportedAppUsage
     public int getRilDataRadioTechnology() {
-        NetworkRegistrationInfo wwanRegInfo = getNetworkRegistrationInfo(
-                NetworkRegistrationInfo.DOMAIN_PS, AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
-        NetworkRegistrationInfo wlanRegInfo = getNetworkRegistrationInfo(
-                NetworkRegistrationInfo.DOMAIN_PS, AccessNetworkConstants.TRANSPORT_TYPE_WLAN);
-        if (wlanRegInfo != null
-                && wlanRegInfo.getAccessNetworkTechnology() == TelephonyManager.NETWORK_TYPE_IWLAN
-                && wlanRegInfo.getRegistrationState()
-                == NetworkRegistrationInfo.REGISTRATION_STATE_HOME) {
-            return RIL_RADIO_TECHNOLOGY_IWLAN;
-        } else if (wwanRegInfo != null) {
-            return networkTypeToRilRadioTechnology(wwanRegInfo.getAccessNetworkTechnology());
-        }
-        return RIL_RADIO_TECHNOLOGY_UNKNOWN;
+        return networkTypeToRilRadioTechnology(getDataNetworkType());
     }
+
     /**
      * @hide
      * @Deprecated to be removed Q3 2013 use {@link #getRilDataRadioTechnology} or
@@ -1608,26 +1605,40 @@
         }
     }
 
-    /** @hide */
+    /**
+     * Get current data network type.
+     *
+     * Note that for IWLAN AP-assisted mode device, which is reporting both camped access networks
+     * (cellular RAT and IWLAN)at the same time, this API is simulating the old legacy mode device
+     * behavior,
+     *
+     * @return Current data network type
+     * @hide
+     */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
     public @TelephonyManager.NetworkType int getDataNetworkType() {
-        final NetworkRegistrationInfo iwlanRegState = getNetworkRegistrationInfo(
+        final NetworkRegistrationInfo iwlanRegInfo = getNetworkRegistrationInfo(
                 NetworkRegistrationInfo.DOMAIN_PS, AccessNetworkConstants.TRANSPORT_TYPE_WLAN);
-        if (iwlanRegState != null && iwlanRegState.getRegistrationState()
-                == NetworkRegistrationInfo.REGISTRATION_STATE_HOME) {
-            // If the device is on IWLAN, return IWLAN as the network type. This is to simulate the
-            // behavior of legacy mode device. In the future caller should use
-            // requestNetworkRegistrationInfo() to retrieve the actual data network type on cellular
-            // or on IWLAN.
-            return iwlanRegState.getAccessNetworkTechnology();
+        final NetworkRegistrationInfo wwanRegInfo = getNetworkRegistrationInfo(
+                NetworkRegistrationInfo.DOMAIN_PS, AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
+
+        // For legacy mode device, or AP-assisted mode device but IWLAN is out of service, use
+        // the RAT from cellular.
+        if (iwlanRegInfo == null || !iwlanRegInfo.isInService()) {
+            return (wwanRegInfo != null) ? wwanRegInfo.getAccessNetworkTechnology()
+                    : TelephonyManager.NETWORK_TYPE_UNKNOWN;
         }
 
-        final NetworkRegistrationInfo regState = getNetworkRegistrationInfo(
-                NetworkRegistrationInfo.DOMAIN_PS, AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
-        if (regState != null) {
-            return regState.getAccessNetworkTechnology();
+        // At this point, it must be an AP-assisted mode device and IWLAN is in service. We should
+        // use the RAT from IWLAN service is cellular is out of service, or when both are in service
+        // and any APN type of data is preferred on IWLAN.
+        if (!wwanRegInfo.isInService() || mIsIwlanPreferred) {
+            return iwlanRegInfo.getAccessNetworkTechnology();
         }
-        return TelephonyManager.NETWORK_TYPE_UNKNOWN;
+
+        // If both cellular and IWLAN are in service, but no APN is preferred on IWLAN, still use
+        // the RAT from cellular.
+        return wwanRegInfo.getAccessNetworkTechnology();
     }
 
     /** @hide */
@@ -1976,4 +1987,14 @@
     public String getOperatorAlphaShortRaw() {
         return mOperatorAlphaShortRaw;
     }
+
+    /**
+     * Set to {@code true} if any data network is preferred on IWLAN.
+     *
+     * @param isIwlanPreferred {@code true} if IWLAN is preferred.
+     * @hide
+     */
+    public void setIwlanPreferred(boolean isIwlanPreferred) {
+        mIsIwlanPreferred = isIwlanPreferred;
+    }
 }
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index d63c37a..2ba34c6 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -5356,18 +5356,30 @@
      * Returns the MMS user agent.
      */
     public String getMmsUserAgent() {
-        if (mContext == null) return null;
-        return SubscriptionManager.getResourcesForSubId(mContext, getSubId()).getString(
-                com.android.internal.R.string.config_mms_user_agent);
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                return telephony.getMmsUserAgent(getSubId());
+            }
+        } catch (RemoteException ex) {
+        } catch (NullPointerException ex) {
+        }
+        return null;
     }
 
     /**
      * Returns the MMS user agent profile URL.
      */
     public String getMmsUAProfUrl() {
-        if (mContext == null) return null;
-        return SubscriptionManager.getResourcesForSubId(mContext, getSubId()).getString(
-                com.android.internal.R.string.config_mms_user_agent_profile_url);
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                return telephony.getMmsUAProfUrl(getSubId());
+            }
+        } catch (RemoteException ex) {
+        } catch (NullPointerException ex) {
+        }
+        return null;
     }
 
     /**
diff --git a/telephony/java/android/telephony/emergency/EmergencyNumber.java b/telephony/java/android/telephony/emergency/EmergencyNumber.java
index 7b51457..511adf6 100644
--- a/telephony/java/android/telephony/emergency/EmergencyNumber.java
+++ b/telephony/java/android/telephony/emergency/EmergencyNumber.java
@@ -299,7 +299,7 @@
      * Get the dialing number of the emergency number.
      *
      * The character in the number string is only the dial pad
-     * character('0'-'9', '*', or '#'). For example: 911.
+     * character('0'-'9', '*', '+', or '#'). For example: 911.
      *
      * @return the dialing number.
      */
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 63aded1..cf1323a 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -1986,4 +1986,14 @@
      * outgoing SmsManager operation.
      */
     oneway void enqueueSmsPickResult(String callingPackage, IIntegerConsumer subIdResult);
+
+    /**
+     * Returns the MMS user agent.
+     */
+    String getMmsUserAgent(int subId);
+
+    /**
+     * Returns the MMS user agent profile URL.
+     */
+    String getMmsUAProfUrl(int subId);
 }
diff --git a/tests/PackageWatchdog/Android.bp b/tests/PackageWatchdog/Android.bp
index b079965..88d92c4 100644
--- a/tests/PackageWatchdog/Android.bp
+++ b/tests/PackageWatchdog/Android.bp
@@ -18,11 +18,18 @@
     srcs: ["src/**/*.java"],
     static_libs: [
         "junit",
+        "mockito-target-extended-minus-junit4",
         "frameworks-base-testutils",
         "androidx.test.rules",
         "services.core",
+        "services.net",
     ],
     libs: ["android.test.runner"],
+    jni_libs: [
+        // mockito-target-extended dependencies
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
     platform_apis: true,
     test_suites: ["device-tests"],
 }
diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
index eb19361..232b5cb 100644
--- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
+++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
@@ -23,10 +23,20 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.Manifest;
 import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
 import android.content.pm.VersionedPackage;
+import android.net.NetworkStackClient;
+import android.net.NetworkStackClient.NetworkStackHealthListener;
 import android.os.Handler;
 import android.os.test.TestLooper;
 import android.provider.DeviceConfig;
@@ -41,6 +51,10 @@
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -70,13 +84,29 @@
     private static final long SHORT_DURATION = TimeUnit.SECONDS.toMillis(1);
     private static final long LONG_DURATION = TimeUnit.SECONDS.toMillis(5);
     private TestLooper mTestLooper;
+    private Context mSpyContext;
+    @Mock
+    private NetworkStackClient mMockNetworkStackClient;
+    @Mock
+    private PackageManager mMockPackageManager;
+    @Captor
+    private ArgumentCaptor<NetworkStackHealthListener> mNetworkStackCallbackCaptor;
 
     @Before
     public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
         new File(InstrumentationRegistry.getContext().getFilesDir(),
                 "package-watchdog.xml").delete();
         adoptShellPermissions(Manifest.permission.READ_DEVICE_CONFIG);
         mTestLooper = new TestLooper();
+        mSpyContext = spy(InstrumentationRegistry.getContext());
+        when(mSpyContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getPackageInfo(anyString(), anyInt())).then(inv -> {
+            final PackageInfo res = new PackageInfo();
+            res.packageName = inv.getArgument(0);
+            res.setLongVersionCode(VERSION_CODE);
+            return res;
+        });
     }
 
     @After
@@ -696,6 +726,26 @@
         assertEquals(MonitoredPackage.STATE_PASSED, m4.handleElapsedTimeLocked(LONG_DURATION));
     }
 
+    @Test
+    public void testNetworkStackFailure() {
+        final PackageWatchdog wd = createWatchdog();
+
+        // Start observing with failure handling
+        TestObserver observer = new TestObserver(OBSERVER_NAME_1,
+                PackageHealthObserverImpact.USER_IMPACT_HIGH);
+        wd.startObservingHealth(observer, Collections.singletonList(APP_A), SHORT_DURATION);
+
+        // Notify of NetworkStack failure
+        mNetworkStackCallbackCaptor.getValue().onNetworkStackFailure(APP_A);
+
+        // Run handler so package failures are dispatched to observers
+        mTestLooper.dispatchAll();
+
+        // Verify the NetworkStack observer is notified
+        assertEquals(1, observer.mFailedPackages.size());
+        assertEquals(APP_A, observer.mFailedPackages.get(0));
+    }
+
     private void adoptShellPermissions(String... permissions) {
         InstrumentationRegistry
                 .getInstrumentation()
@@ -727,18 +777,23 @@
     }
 
     private PackageWatchdog createWatchdog(TestController controller, boolean withPackagesReady) {
-        Context context = InstrumentationRegistry.getContext();
         AtomicFile policyFile =
-                new AtomicFile(new File(context.getFilesDir(), "package-watchdog.xml"));
+                new AtomicFile(new File(mSpyContext.getFilesDir(), "package-watchdog.xml"));
         Handler handler = new Handler(mTestLooper.getLooper());
         PackageWatchdog watchdog =
-                new PackageWatchdog(context, policyFile, handler, handler, controller);
+                new PackageWatchdog(mSpyContext, policyFile, handler, handler, controller,
+                        mMockNetworkStackClient);
         // Verify controller is not automatically started
         assertFalse(controller.mIsEnabled);
         if (withPackagesReady) {
+            // Only capture the NetworkStack callback for the latest registered watchdog
+            reset(mMockNetworkStackClient);
             watchdog.onPackagesReady();
             // Verify controller by default is started when packages are ready
             assertTrue(controller.mIsEnabled);
+
+            verify(mMockNetworkStackClient).registerHealthListener(
+                    mNetworkStackCallbackCaptor.capture());
         }
         return watchdog;
     }
diff --git a/tests/RollbackTest/Android.bp b/tests/RollbackTest/Android.bp
index 1cf3d36..aec4055 100644
--- a/tests/RollbackTest/Android.bp
+++ b/tests/RollbackTest/Android.bp
@@ -95,7 +95,7 @@
         ":RollbackTestAppASplitV2",
     ],
     test_config: "RollbackTest.xml",
-    sdk_version: "test_current",
+    // TODO: sdk_version: "test_current" when Intent#resolveSystemservice is TestApi
 }
 
 java_test_host {
diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTestUtils.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTestUtils.java
index 81629aa..a9e20cd 100644
--- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTestUtils.java
+++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTestUtils.java
@@ -28,6 +28,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
@@ -82,13 +83,31 @@
      * Returns -1 if the package is not currently installed.
      */
     static long getInstalledVersion(String packageName) {
+        PackageInfo pi = getPackageInfo(packageName);
+        if (pi == null) {
+            return -1;
+        } else {
+            return pi.getLongVersionCode();
+        }
+    }
+
+    private static boolean isSystemAppWithoutUpdate(String packageName) {
+        PackageInfo pi = getPackageInfo(packageName);
+        if (pi == null) {
+            return false;
+        } else {
+            return ((pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0)
+                    && ((pi.applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0);
+        }
+    }
+
+    private static PackageInfo getPackageInfo(String packageName) {
         Context context = InstrumentationRegistry.getContext();
         PackageManager pm = context.getPackageManager();
         try {
-            PackageInfo info = pm.getPackageInfo(packageName, PackageManager.MATCH_APEX);
-            return info.getLongVersionCode();
+            return pm.getPackageInfo(packageName, PackageManager.MATCH_APEX);
         } catch (PackageManager.NameNotFoundException e) {
-            return -1;
+            return null;
         }
     }
 
@@ -109,8 +128,8 @@
      * @throws AssertionError if package can't be uninstalled.
      */
     static void uninstall(String packageName) throws InterruptedException, IOException {
-        // No need to uninstall if the package isn't installed.
-        if (getInstalledVersion(packageName) == -1) {
+        // No need to uninstall if the package isn't installed or is installed on /system.
+        if (getInstalledVersion(packageName) == -1 || isSystemAppWithoutUpdate(packageName)) {
             return;
         }
 
diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java
index 1ddfa6e..1a29c4c 100644
--- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java
+++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java
@@ -21,7 +21,9 @@
 
 import android.Manifest;
 import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.VersionedPackage;
 import android.content.rollback.RollbackInfo;
 import android.content.rollback.RollbackManager;
@@ -30,6 +32,8 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -54,6 +58,8 @@
     private static final String TEST_APP_A = "com.android.tests.rollback.testapp.A";
     private static final String TEST_APP_A_V1 = "RollbackTestAppAv1.apk";
     private static final String TEST_APP_A_CRASHING_V2 = "RollbackTestAppACrashingV2.apk";
+    private static final String NETWORK_STACK_CONNECTOR_CLASS =
+            "android.net.INetworkStackConnector";
 
     /**
      * Adopts common shell permissions needed for rollback tests.
@@ -157,4 +163,44 @@
         assertTrue(rollback.isStaged());
         assertNotEquals(-1, rollback.getCommittedSessionId());
     }
+
+    @Test
+    public void resetNetworkStack() throws Exception {
+        RollbackManager rm = RollbackTestUtils.getRollbackManager();
+        String networkStack = getNetworkStackPackageName();
+
+        rm.expireRollbackForPackage(networkStack);
+        RollbackTestUtils.uninstall(networkStack);
+
+        assertNull(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(),
+                        networkStack));
+    }
+
+    @Test
+    public void assertNetworkStackRollbackAvailable() throws Exception {
+        RollbackManager rm = RollbackTestUtils.getRollbackManager();
+        assertNotNull(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(),
+                        getNetworkStackPackageName()));
+    }
+
+    @Test
+    public void assertNetworkStackRollbackCommitted() throws Exception {
+        RollbackManager rm = RollbackTestUtils.getRollbackManager();
+        assertNotNull(getUniqueRollbackInfoForPackage(rm.getRecentlyCommittedRollbacks(),
+                        getNetworkStackPackageName()));
+    }
+
+    @Test
+    public void assertNoNetworkStackRollbackCommitted() throws Exception {
+        RollbackManager rm = RollbackTestUtils.getRollbackManager();
+        assertNull(getUniqueRollbackInfoForPackage(rm.getRecentlyCommittedRollbacks(),
+                        getNetworkStackPackageName()));
+    }
+
+    private String getNetworkStackPackageName() {
+        Intent intent = new Intent(NETWORK_STACK_CONNECTOR_CLASS);
+        ComponentName comp = intent.resolveSystemService(
+                InstrumentationRegistry.getContext().getPackageManager(), 0);
+        return comp.getPackageName();
+    }
 }
diff --git a/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java b/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java
index 75a95ad..bad2947 100644
--- a/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java
+++ b/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java
@@ -23,6 +23,8 @@
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -43,6 +45,20 @@
                     phase));
     }
 
+    @Before
+    public void setUp() throws Exception {
+        // Disconnect internet so we can test network health triggered rollbacks
+        getDevice().executeShellCommand("svc wifi disable");
+        getDevice().executeShellCommand("svc data disable");
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        // Reconnect internet after testing network health triggered rollbacks
+        getDevice().executeShellCommand("svc wifi enable");
+        getDevice().executeShellCommand("svc data enable");
+    }
+
     /**
      * Tests watchdog triggered staged rollbacks involving only apks.
      */
@@ -63,6 +79,90 @@
         }
 
         getDevice().waitForDeviceAvailable();
+
         runPhase("testBadApkOnlyConfirmRollback");
     }
+
+    /**
+     * Tests failed network health check triggers watchdog staged rollbacks.
+     */
+    @Test
+    public void testNetworkFailedRollback() throws Exception {
+        // Remove available rollbacks and uninstall NetworkStack on /data/
+        runPhase("resetNetworkStack");
+        // Reduce health check deadline
+        getDevice().executeShellCommand("device_config put rollback "
+                + "watchdog_request_timeout_millis 300000");
+        // Simulate re-installation of new NetworkStack with rollbacks enabled
+        getDevice().executeShellCommand("pm install -r --staged --enable-rollback "
+                + "/system/priv-app/NetworkStack/NetworkStack.apk");
+
+        // Sleep to allow writes to disk before reboot
+        Thread.sleep(5000);
+        // Reboot device to activate staged package
+        getDevice().reboot();
+        getDevice().waitForDeviceAvailable();
+
+        // Verify rollback was enabled
+        runPhase("assertNetworkStackRollbackAvailable");
+
+        // Sleep for < health check deadline
+        Thread.sleep(5000);
+        // Verify rollback was not executed before health check deadline
+        runPhase("assertNoNetworkStackRollbackCommitted");
+        try {
+            // This is expected to fail due to the device being rebooted out
+            // from underneath the test. If this fails for reasons other than
+            // the device reboot, those failures should result in failure of
+            // the assertNetworkStackExecutedRollback phase.
+            CLog.logAndDisplay(LogLevel.INFO, "Sleep and expect to fail while sleeping");
+            // Sleep for > health check deadline
+            Thread.sleep(260000);
+        } catch (AssertionError e) {
+            // AssertionError is expected.
+        }
+
+        getDevice().waitForDeviceAvailable();
+        // Verify rollback was executed after health check deadline
+        runPhase("assertNetworkStackRollbackCommitted");
+    }
+
+    /**
+     * Tests passed network health check does not trigger watchdog staged rollbacks.
+     */
+    @Test
+    public void testNetworkPassedDoesNotRollback() throws Exception {
+        // Remove available rollbacks and uninstall NetworkStack on /data/
+        runPhase("resetNetworkStack");
+        // Reduce health check deadline, here unlike the network failed case, we use
+        // a longer deadline because joining a network can take a much longer time for
+        // reasons external to the device than 'not joining'
+        getDevice().executeShellCommand("device_config put rollback "
+                + "watchdog_request_timeout_millis 300000");
+        // Simulate re-installation of new NetworkStack with rollbacks enabled
+        getDevice().executeShellCommand("pm install -r --staged --enable-rollback "
+                + "/system/priv-app/NetworkStack/NetworkStack.apk");
+
+        // Sleep to allow writes to disk before reboot
+        Thread.sleep(5000);
+        // Reboot device to activate staged package
+        getDevice().reboot();
+        getDevice().waitForDeviceAvailable();
+
+        // Verify rollback was enabled
+        runPhase("assertNetworkStackRollbackAvailable");
+
+        // Connect to internet so network health check passes
+        getDevice().executeShellCommand("svc wifi enable");
+        getDevice().executeShellCommand("svc data enable");
+
+        // Wait for device available because emulator device may restart after turning
+        // on mobile data
+        getDevice().waitForDeviceAvailable();
+
+        // Sleep for > health check deadline
+        Thread.sleep(310000);
+        // Verify rollback was not executed after health check deadline
+        runPhase("assertNoNetworkStackRollbackCommitted");
+    }
 }
diff --git a/tests/net/java/android/net/NetworkStatsTest.java b/tests/net/java/android/net/NetworkStatsTest.java
index b5b0384..c16a0f4 100644
--- a/tests/net/java/android/net/NetworkStatsTest.java
+++ b/tests/net/java/android/net/NetworkStatsTest.java
@@ -569,7 +569,7 @@
             .addValues(underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
                     DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L);
 
-        assertTrue(delta.toString(), delta.migrateTun(tunUid, tunIface, underlyingIface));
+        delta.migrateTun(tunUid, tunIface, new String[] {underlyingIface});
         assertEquals(20, delta.size());
 
         // tunIface and TEST_IFACE entries are not changed.
@@ -650,7 +650,7 @@
             .addValues(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
                     DEFAULT_NETWORK_NO,  75500L, 37L, 130000L, 70L, 0L);
 
-        assertTrue(delta.migrateTun(tunUid, tunIface, underlyingIface));
+        delta.migrateTun(tunUid, tunIface, new String[]{underlyingIface});
         assertEquals(9, delta.size());
 
         // tunIface entries should not be changed.
@@ -813,6 +813,37 @@
     }
 
     @Test
+    public void testFilterDebugEntries() {
+        NetworkStats.Entry entry1 = new NetworkStats.Entry(
+                "test1", 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats.Entry entry2 = new NetworkStats.Entry(
+                "test2", 10101, SET_DBG_VPN_IN, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats.Entry entry3 = new NetworkStats.Entry(
+                "test2", 10101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats.Entry entry4 = new NetworkStats.Entry(
+                "test2", 10101, SET_DBG_VPN_OUT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats stats = new NetworkStats(TEST_START, 4)
+                .addValues(entry1)
+                .addValues(entry2)
+                .addValues(entry3)
+                .addValues(entry4);
+
+        stats.filterDebugEntries();
+
+        assertEquals(2, stats.size());
+        assertEquals(entry1, stats.getValues(0, null));
+        assertEquals(entry3, stats.getValues(1, null));
+    }
+
+    @Test
     public void testApply464xlatAdjustments() {
         final String v4Iface = "v4-wlan0";
         final String baseIface = "wlan0";
diff --git a/tests/net/java/com/android/server/connectivity/VpnTest.java b/tests/net/java/com/android/server/connectivity/VpnTest.java
index 2cae250..ce50bef 100644
--- a/tests/net/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/net/java/com/android/server/connectivity/VpnTest.java
@@ -727,94 +727,4 @@
                 "::/1", "8000::/2", "c000::/3", "e000::/4", "f000::/5", "f800::/6",
                 "fe00::/8", "2605:ef80:e:af1d::/64");
     }
-
-    @Test
-    public void testProvidesRoutesToMostDestinations() {
-        final LinkProperties lp = new LinkProperties();
-
-        // Default route provides routes to all IPv4 destinations.
-        lp.addRoute(new RouteInfo(new IpPrefix("0.0.0.0/0")));
-        assertTrue(Vpn.providesRoutesToMostDestinations(lp));
-
-        // Empty LP provides routes to no destination
-        lp.clear();
-        assertFalse(Vpn.providesRoutesToMostDestinations(lp));
-
-        // All IPv4 routes except for local networks. This is the case most relevant
-        // to this function. It provides routes to almost the entire space.
-        // (clone the stream so that we can reuse it later)
-        publicIpV4Routes().forEach(s -> lp.addRoute(new RouteInfo(new IpPrefix(s))));
-        assertTrue(Vpn.providesRoutesToMostDestinations(lp));
-
-        // Removing a 16-bit prefix, which is 65536 addresses. This is still enough to
-        // provide routes to "most" destinations.
-        lp.removeRoute(new RouteInfo(new IpPrefix("192.169.0.0/16")));
-        assertTrue(Vpn.providesRoutesToMostDestinations(lp));
-
-        // Remove the /2 route, which represent a quarter of the available routing space.
-        // This LP does not provides routes to "most" destinations any more.
-        lp.removeRoute(new RouteInfo(new IpPrefix("64.0.0.0/2")));
-        assertFalse(Vpn.providesRoutesToMostDestinations(lp));
-
-        lp.clear();
-        publicIpV6Routes().forEach(s -> lp.addRoute(new RouteInfo(new IpPrefix(s))));
-        assertTrue(Vpn.providesRoutesToMostDestinations(lp));
-
-        lp.removeRoute(new RouteInfo(new IpPrefix("::/1")));
-        assertFalse(Vpn.providesRoutesToMostDestinations(lp));
-
-        // V6 does not provide sufficient coverage but v4 does
-        publicIpV4Routes().forEach(s -> lp.addRoute(new RouteInfo(new IpPrefix(s))));
-        assertTrue(Vpn.providesRoutesToMostDestinations(lp));
-
-        // V4 still does
-        lp.removeRoute(new RouteInfo(new IpPrefix("192.169.0.0/16")));
-        assertTrue(Vpn.providesRoutesToMostDestinations(lp));
-
-        // V4 does not any more
-        lp.removeRoute(new RouteInfo(new IpPrefix("64.0.0.0/2")));
-        assertFalse(Vpn.providesRoutesToMostDestinations(lp));
-
-        // V4 does not, but V6 has sufficient coverage again
-        lp.addRoute(new RouteInfo(new IpPrefix("::/1")));
-        assertTrue(Vpn.providesRoutesToMostDestinations(lp));
-
-        lp.clear();
-        // V4-unreachable route should not be treated as sufficient coverage
-        lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE));
-        assertFalse(Vpn.providesRoutesToMostDestinations(lp));
-
-        lp.clear();
-        // V6-unreachable route should not be treated as sufficient coverage
-        lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE));
-        assertFalse(Vpn.providesRoutesToMostDestinations(lp));
-    }
-
-    @Test
-    public void testDoesNotLockUpWithTooManyRoutes() {
-        final LinkProperties lp = new LinkProperties();
-        final byte[] ad = new byte[4];
-        // Actually evaluating this many routes under 1500ms is impossible on
-        // current hardware and for some time, as the algorithm is O(n²).
-        // Make sure the system has a safeguard against this and does not
-        // lock up.
-        final int MAX_ROUTES = 4000;
-        final long MAX_ALLOWED_TIME_MS = 1500;
-        for (int i = 0; i < MAX_ROUTES; ++i) {
-            ad[0] = (byte)((i >> 24) & 0xFF);
-            ad[1] = (byte)((i >> 16) & 0xFF);
-            ad[2] = (byte)((i >> 8) & 0xFF);
-            ad[3] = (byte)(i & 0xFF);
-            try {
-                lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.getByAddress(ad), 32)));
-            } catch (UnknownHostException e) {
-                // UnknownHostException is only thrown for an address of illegal length,
-                // which can't happen in the case above.
-            }
-        }
-        final long start = SystemClock.currentThreadTimeMillis();
-        assertTrue(Vpn.providesRoutesToMostDestinations(lp));
-        final long end = SystemClock.currentThreadTimeMillis();
-        assertTrue(end - start < MAX_ALLOWED_TIME_MS);
-    }
 }
diff --git a/tests/net/java/com/android/server/net/NetworkStatsAccessTest.java b/tests/net/java/com/android/server/net/NetworkStatsAccessTest.java
index 6e725dd..858358c 100644
--- a/tests/net/java/com/android/server/net/NetworkStatsAccessTest.java
+++ b/tests/net/java/com/android/server/net/NetworkStatsAccessTest.java
@@ -161,7 +161,7 @@
     }
 
     private void setHasCarrierPrivileges(boolean hasPrivileges) {
-        when(mTm.checkCarrierPrivilegesForPackage(TEST_PKG)).thenReturn(
+        when(mTm.checkCarrierPrivilegesForPackageAnyPhone(TEST_PKG)).thenReturn(
                 hasPrivileges ? TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS
                         : TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS);
     }
diff --git a/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java
index bce526d..d9f2c20 100644
--- a/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -57,11 +57,11 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -216,11 +216,16 @@
         expectNetworkStatsUidDetail(buildEmptyStats());
         expectSystemReady();
 
+        assertNull(mService.getTunAdjustedStats());
         mService.systemReady();
+        // Verify that system ready fetches realtime stats and initializes tun adjusted stats.
+        verify(mNetManager).getNetworkStatsUidDetail(UID_ALL, INTERFACES_ALL);
+        assertNotNull("failed to initialize TUN adjusted stats", mService.getTunAdjustedStats());
+        assertEquals(0, mService.getTunAdjustedStats().size());
+
         mSession = mService.openSession();
         assertNotNull("openSession() failed", mSession);
 
-
         // catch INetworkManagementEventObserver during systemReady()
         ArgumentCaptor<INetworkManagementEventObserver> networkObserver =
               ArgumentCaptor.forClass(INetworkManagementEventObserver.class);
@@ -733,11 +738,13 @@
 
         NetworkStats stats = mService.getDetailedUidStats(ifaceFilter);
 
-        verify(mNetManager, times(1)).getNetworkStatsUidDetail(eq(UID_ALL), argThat(ifaces ->
-                ifaces != null && ifaces.length == 2
-                        && ArrayUtils.contains(ifaces, TEST_IFACE)
-                        && ArrayUtils.contains(ifaces, stackedIface)));
-
+        // mNetManager#getNetworkStatsUidDetail(UID_ALL, INTERFACES_ALL) has following invocations:
+        // 1) NetworkStatsService#systemReady from #setUp.
+        // 2) mService#forceUpdateIfaces in the test above.
+        // 3) Finally, mService#getDetailedUidStats.
+        verify(mNetManager, times(3)).getNetworkStatsUidDetail(UID_ALL, INTERFACES_ALL);
+        assertTrue(ArrayUtils.contains(stats.getUniqueIfaces(), TEST_IFACE));
+        assertTrue(ArrayUtils.contains(stats.getUniqueIfaces(), stackedIface));
         assertEquals(2, stats.size());
         assertEquals(uidStats, stats.getValues(0, null));
         assertEquals(tetheredStats1, stats.getValues(1, null));
@@ -923,11 +930,11 @@
     }
 
     @Test
-    public void vpnWithOneUnderlyingIface() throws Exception {
+    public void vpnRewriteTrafficThroughItself() throws Exception {
         // WiFi network is connected and VPN is using WiFi (which has TEST_IFACE).
         expectDefaultSettings();
         NetworkState[] networkStates = new NetworkState[] {buildWifiState(), buildVpnState()};
-        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(TEST_IFACE)};
+        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
         expectNetworkStatsUidDetail(buildEmptyStats());
         expectBandwidthControlCheck();
 
@@ -938,23 +945,133 @@
                 getActiveIface(networkStates));
         // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
         // overhead per packet):
-        // 1000 bytes (100 packets) were sent/received by UID_RED over VPN.
-        // 500 bytes (50 packets) were sent/received by UID_BLUE over VPN.
-        // VPN sent/received 1650 bytes (150 packets) over WiFi.
-        // Of 1650 bytes over WiFi, expect 1000 bytes attributed to UID_RED, 500 bytes attributed to
-        // UID_BLUE, and 150 bytes attributed to UID_VPN for both rx/tx traffic.
+        //
+        // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED
+        // over VPN.
+        // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE
+        // over VPN.
+        //
+        // VPN UID rewrites packets read from TUN back to TUN, plus some of its own traffic
+        // (100 bytes).
         incrementCurrentTime(HOUR_IN_MILLIS);
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
-                .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1000L, 100L, 1000L, 100L, 1L)
-                .addValues(TUN_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 500L, 50L, 500L, 50L, 1L)
-                .addValues(
-                    TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 1650L, 150L, 1650L, 150L, 2L));
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 5)
+                .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 2000L, 200L, 1000L, 100L, 1L)
+                .addValues(TUN_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 1000L, 100L, 500L, 50L, 1L)
+                // VPN rewrites all the packets read from TUN + 100 additional bytes of VPN's
+                // own traffic.
+                .addValues(TUN_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 0L, 0L, 1600L, 160L, 2L)
+                // VPN sent 1760 bytes over WiFi in foreground (SET_FOREGROUND) i.e. 1600
+                // bytes (160 packets) + 1 byte/packet overhead (=160 bytes).
+                .addValues(TEST_IFACE, UID_VPN, SET_FOREGROUND, TAG_NONE, 0L, 0L, 1760L, 176L, 1L)
+                // VPN received 3300 bytes over WiFi in background (SET_DEFAULT) i.e. 3000 bytes
+                // (300 packets) + 1 byte/packet encryption overhead (=300 bytes).
+                .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 3300L, 300L, 0L, 0L, 1L));
 
         forcePollAndWaitForIdle();
 
-        assertUidTotal(sTemplateWifi, UID_RED, 1000L, 100L, 1000L, 100L, 1);
-        assertUidTotal(sTemplateWifi, UID_BLUE, 500L, 50L, 500L, 50L, 1);
-        assertUidTotal(sTemplateWifi, UID_VPN, 150L, 0L, 150L, 0L, 2);
+        // Verify increased TUN usage by UID_VPN does not get attributed to other apps.
+        NetworkStats tunStats =
+                mService.getDetailedUidStats(new String[] {TUN_IFACE});
+        assertValues(
+                tunStats, TUN_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 2000L, 200L, 1000L, 100L, 1);
+        assertValues(
+                tunStats, TUN_IFACE, UID_BLUE, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 1000L, 100L, 500L, 50L, 1);
+        assertValues(
+                tunStats, TUN_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 0L, 0L, 1600L, 160L, 2);
+
+        // Verify correct attribution over WiFi.
+        assertUidTotal(sTemplateWifi, UID_RED, 2000L, 200L, 1000L, 100L, 1);
+        assertUidTotal(sTemplateWifi, UID_BLUE, 1000L, 100L, 500L, 50L, 1);
+        assertUidTotal(sTemplateWifi, UID_VPN, 300L, 0L, 260L, 26L, 2);
+    }
+
+    @Test
+    public void vpnWithOneUnderlyingIface() throws Exception {
+        // WiFi network is connected and VPN is using WiFi (which has TEST_IFACE).
+        expectDefaultSettings();
+        NetworkState[] networkStates = new NetworkState[] {buildWifiState(), buildVpnState()};
+        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectBandwidthControlCheck();
+
+        mService.forceUpdateIfaces(
+                new Network[] {WIFI_NETWORK, VPN_NETWORK},
+                vpnInfos,
+                networkStates,
+                getActiveIface(networkStates));
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED
+        // over VPN.
+        // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE
+        // over VPN.
+        // VPN sent 1650 bytes (150 packets), and received 3300 (300 packets) over WiFi.
+        // Of 1650 bytes sent over WiFi, expect 1000 bytes attributed to UID_RED, 500 bytes
+        // attributed to UID_BLUE, and 150 bytes attributed to UID_VPN.
+        // Of 3300 bytes received over WiFi, expect 2000 bytes attributed to UID_RED, 1000 bytes
+        // attributed to UID_BLUE, and 300 bytes attributed to UID_VPN.
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
+                .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 2000L, 200L, 1000L, 100L, 1L)
+                .addValues(TUN_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 1000L, 100L, 500L, 50L, 1L)
+                // VPN received 3300 bytes over WiFi in background (SET_DEFAULT).
+                .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 3300L, 300L, 0L, 0L, 1L)
+                // VPN sent 1650 bytes over WiFi in foreground (SET_FOREGROUND).
+                .addValues(TEST_IFACE, UID_VPN, SET_FOREGROUND, TAG_NONE, 0L, 0L, 1650L, 150L, 1L));
+
+        forcePollAndWaitForIdle();
+
+        assertUidTotal(sTemplateWifi, UID_RED, 2000L, 200L, 1000L, 100L, 1);
+        assertUidTotal(sTemplateWifi, UID_BLUE, 1000L, 100L, 500L, 50L, 1);
+        assertUidTotal(sTemplateWifi, UID_VPN, 300L, 0L, 150L, 0L, 2);
+    }
+
+    @Test
+    public void vpnWithOneUnderlyingIfaceAndOwnTraffic() throws Exception {
+        // WiFi network is connected and VPN is using WiFi (which has TEST_IFACE).
+        expectDefaultSettings();
+        NetworkState[] networkStates = new NetworkState[] {buildWifiState(), buildVpnState()};
+        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectBandwidthControlCheck();
+
+        mService.forceUpdateIfaces(
+                new Network[] {WIFI_NETWORK, VPN_NETWORK},
+                vpnInfos,
+                networkStates,
+                getActiveIface(networkStates));
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED
+        // over VPN.
+        // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE
+        // over VPN.
+        // Additionally, the VPN sends 6000 bytes (600 packets) of its own traffic into the tun
+        // interface (passing that traffic to the VPN endpoint), and receives 5000 bytes (500
+        // packets) from it. Including overhead that is 6600/5500 bytes.
+        // VPN sent 8250 bytes (750 packets), and received 8800 (800 packets) over WiFi.
+        // Of 8250 bytes sent over WiFi, expect 1000 bytes attributed to UID_RED, 500 bytes
+        // attributed to UID_BLUE, and 6750 bytes attributed to UID_VPN.
+        // Of 8800 bytes received over WiFi, expect 2000 bytes attributed to UID_RED, 1000 bytes
+        // attributed to UID_BLUE, and 5800 bytes attributed to UID_VPN.
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
+                .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 2000L, 200L, 1000L, 100L, 1L)
+                .addValues(TUN_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 1000L, 100L, 500L, 50L, 1L)
+                .addValues(TUN_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 5000L, 500L, 6000L, 600L, 1L)
+                // VPN received 8800 bytes over WiFi in background (SET_DEFAULT).
+                .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 8800L, 800L, 0L, 0L, 1L)
+                // VPN sent 8250 bytes over WiFi in foreground (SET_FOREGROUND).
+                .addValues(TEST_IFACE, UID_VPN, SET_FOREGROUND, TAG_NONE, 0L, 0L, 8250L, 750L, 1L));
+
+        forcePollAndWaitForIdle();
+
+        assertUidTotal(sTemplateWifi, UID_RED, 2000L, 200L, 1000L, 100L, 1);
+        assertUidTotal(sTemplateWifi, UID_BLUE, 1000L, 100L, 500L, 50L, 1);
+        assertUidTotal(sTemplateWifi, UID_VPN, 5800L, 500L, 6750L, 600L, 2);
     }
 
     @Test
@@ -962,7 +1079,7 @@
         // WiFi network is connected and VPN is using WiFi (which has TEST_IFACE).
         expectDefaultSettings();
         NetworkState[] networkStates = new NetworkState[] {buildWifiState(), buildVpnState()};
-        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(TEST_IFACE)};
+        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
         expectNetworkStatsUidDetail(buildEmptyStats());
         expectBandwidthControlCheck();
 
@@ -993,6 +1110,136 @@
     }
 
     @Test
+    public void vpnWithTwoUnderlyingIfaces_packetDuplication() throws Exception {
+        // WiFi and Cell networks are connected and VPN is using WiFi (which has TEST_IFACE) and
+        // Cell (which has TEST_IFACE2) and has declared both of them in its underlying network set.
+        // Additionally, VPN is duplicating traffic across both WiFi and Cell.
+        expectDefaultSettings();
+        NetworkState[] networkStates =
+                new NetworkState[] {
+                    buildWifiState(), buildMobile4gState(TEST_IFACE2), buildVpnState()
+                };
+        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE, TEST_IFACE2})};
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectBandwidthControlCheck();
+
+        mService.forceUpdateIfaces(
+                new Network[] {WIFI_NETWORK, VPN_NETWORK},
+                vpnInfos,
+                networkStates,
+                getActiveIface(networkStates));
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        // 1000 bytes (100 packets) were sent/received by UID_RED and UID_BLUE over VPN.
+        // VPN sent/received 4400 bytes (400 packets) over both WiFi and Cell (8800 bytes in total).
+        // Of 8800 bytes over WiFi/Cell, expect:
+        // - 500 bytes rx/tx each over WiFi/Cell attributed to both UID_RED and UID_BLUE.
+        // - 1200 bytes rx/tx each over WiFi/Cell for VPN_UID.
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 4)
+                .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1000L, 100L, 1000L, 100L, 2L)
+                .addValues(TUN_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 1000L, 100L, 1000L, 100L, 2L)
+                .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 2200L, 200L, 2200L, 200L, 2L)
+                .addValues(
+                    TEST_IFACE2, UID_VPN, SET_DEFAULT, TAG_NONE, 2200L, 200L, 2200L, 200L, 2L));
+
+        forcePollAndWaitForIdle();
+
+        assertUidTotal(sTemplateWifi, UID_RED, 500L, 50L, 500L, 50L, 1);
+        assertUidTotal(sTemplateWifi, UID_BLUE, 500L, 50L, 500L, 50L, 1);
+        assertUidTotal(sTemplateWifi, UID_VPN, 1200L, 100L, 1200L, 100L, 2);
+
+        assertUidTotal(buildTemplateMobileWildcard(), UID_RED, 500L, 50L, 500L, 50L, 1);
+        assertUidTotal(buildTemplateMobileWildcard(), UID_BLUE, 500L, 50L, 500L, 50L, 1);
+        assertUidTotal(buildTemplateMobileWildcard(), UID_VPN, 1200L, 100L, 1200L, 100L, 2);
+    }
+
+    @Test
+    public void vpnWithTwoUnderlyingIfaces_splitTraffic() throws Exception {
+        // WiFi and Cell networks are connected and VPN is using WiFi (which has TEST_IFACE) and
+        // Cell (which has TEST_IFACE2) and has declared both of them in its underlying network set.
+        // Additionally, VPN is arbitrarily splitting traffic across WiFi and Cell.
+        expectDefaultSettings();
+        NetworkState[] networkStates =
+                new NetworkState[] {
+                    buildWifiState(), buildMobile4gState(TEST_IFACE2), buildVpnState()
+                };
+        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE, TEST_IFACE2})};
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectBandwidthControlCheck();
+
+        mService.forceUpdateIfaces(
+                new Network[] {WIFI_NETWORK, VPN_NETWORK},
+                vpnInfos,
+                networkStates,
+                getActiveIface(networkStates));
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        // 1000 bytes (100 packets) were sent, and 500 bytes (50 packets) received by UID_RED over
+        // VPN.
+        // VPN sent 660 bytes (60 packets) over WiFi and 440 bytes (40 packets) over Cell.
+        // And, it received 330 bytes (30 packets) over WiFi and 220 bytes (20 packets) over Cell.
+        // For UID_RED, expect 600 bytes attributed over WiFi and 400 bytes over Cell for sent (tx)
+        // traffic. For received (rx) traffic, expect 300 bytes over WiFi and 200 bytes over Cell.
+        //
+        // For UID_VPN, expect 60 bytes attributed over WiFi and 40 bytes over Cell for tx traffic.
+        // And, 30 bytes over WiFi and 20 bytes over Cell for rx traffic.
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
+              .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 500L, 50L, 1000L, 100L, 2L)
+              .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 330L, 30L, 660L, 60L, 1L)
+              .addValues(TEST_IFACE2, UID_VPN, SET_DEFAULT, TAG_NONE, 220L, 20L, 440L, 40L, 1L));
+
+        forcePollAndWaitForIdle();
+
+        assertUidTotal(sTemplateWifi, UID_RED, 300L, 30L, 600L, 60L, 1);
+        assertUidTotal(sTemplateWifi, UID_VPN, 30L, 0L, 60L, 0L, 1);
+
+        assertUidTotal(buildTemplateMobileWildcard(), UID_RED, 200L, 20L, 400L, 40L, 1);
+        assertUidTotal(buildTemplateMobileWildcard(), UID_VPN, 20L, 0L, 40L, 0L, 1);
+    }
+
+    @Test
+    public void vpnWithTwoUnderlyingIfaces_splitTrafficWithCompression() throws Exception {
+        // WiFi and Cell networks are connected and VPN is using WiFi (which has TEST_IFACE) and
+        // Cell (which has TEST_IFACE2) and has declared both of them in its underlying network set.
+        // Additionally, VPN is arbitrarily splitting compressed traffic across WiFi and Cell.
+        expectDefaultSettings();
+        NetworkState[] networkStates =
+                new NetworkState[] {
+                    buildWifiState(), buildMobile4gState(TEST_IFACE2), buildVpnState()
+                };
+        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE, TEST_IFACE2})};
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectBandwidthControlCheck();
+
+        mService.forceUpdateIfaces(
+                new Network[] {WIFI_NETWORK, VPN_NETWORK},
+                vpnInfos,
+                networkStates,
+                getActiveIface(networkStates));
+        // create some traffic (assume 10 bytes of MTU for VPN interface:
+        // 1000 bytes (100 packets) were sent/received by UID_RED over VPN.
+        // VPN sent/received 600 bytes (60 packets) over WiFi and 200 bytes (20 packets) over Cell.
+        // For UID_RED, expect 600 bytes attributed over WiFi and 200 bytes over Cell for both
+        // rx/tx.
+        // UID_VPN gets nothing attributed to it (avoiding negative stats).
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 4)
+              .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1000L, 100L, 1000L, 100L, 1L)
+              .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 600L, 60L, 600L, 60L, 0L)
+              .addValues(TEST_IFACE2, UID_VPN, SET_DEFAULT, TAG_NONE, 200L, 20L, 200L, 20L, 0L));
+
+        forcePollAndWaitForIdle();
+
+        assertUidTotal(sTemplateWifi, UID_RED, 600L, 60L, 600L, 60L, 0);
+        assertUidTotal(sTemplateWifi, UID_VPN, 0L, 0L, 0L, 0L, 0);
+
+        assertUidTotal(buildTemplateMobileWildcard(), UID_RED, 200L, 20L, 200L, 20L, 0);
+        assertUidTotal(buildTemplateMobileWildcard(), UID_VPN, 0L, 0L, 0L, 0L, 0);
+    }
+
+    @Test
     public void vpnWithIncorrectUnderlyingIface() throws Exception {
         // WiFi and Cell networks are connected and VPN is using Cell (which has TEST_IFACE2),
         // but has declared only WiFi (TEST_IFACE) in its underlying network set.
@@ -1001,7 +1248,7 @@
                 new NetworkState[] {
                     buildWifiState(), buildMobile4gState(TEST_IFACE2), buildVpnState()
                 };
-        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(TEST_IFACE)};
+        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
         expectNetworkStatsUidDetail(buildEmptyStats());
         expectBandwidthControlCheck();
 
@@ -1030,6 +1277,134 @@
     }
 
     @Test
+    public void recordSnapshot_migratesTunTrafficAndUpdatesTunAdjustedStats() throws Exception {
+        assertEquals(0, mService.getTunAdjustedStats().size());
+        // VPN using WiFi (TEST_IFACE).
+        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
+        expectBandwidthControlCheck();
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        // 1000 bytes (100 packets) were downloaded by UID_RED over VPN.
+        // VPN received 1100 bytes (100 packets) over WiFi.
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+              .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1000L, 100L, 0L, 0L, 0L)
+              .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 1100L, 100L, 0L, 0L, 0L));
+
+        // this should lead to NSS#recordSnapshotLocked
+        mService.forceUpdateIfaces(
+                new Network[0], vpnInfos, new NetworkState[0], null /* activeIface */);
+
+        // Verify TUN adjusted stats have traffic migrated correctly.
+        // Of 1100 bytes VPN received over WiFi, expect 1000 bytes attributed to UID_RED and 100
+        // bytes attributed to UID_VPN.
+        NetworkStats tunAdjStats = mService.getTunAdjustedStats();
+        assertValues(
+                tunAdjStats, TEST_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 1000L, 100L, 0L, 0L, 0);
+        assertValues(
+                tunAdjStats, TEST_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 100L, 0L, 0L, 0L, 0);
+    }
+
+    @Test
+    public void getDetailedUidStats_migratesTunTrafficAndUpdatesTunAdjustedStats()
+            throws Exception {
+        assertEquals(0, mService.getTunAdjustedStats().size());
+        // VPN using WiFi (TEST_IFACE).
+        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
+        expectBandwidthControlCheck();
+        mService.forceUpdateIfaces(
+                new Network[0], vpnInfos, new NetworkState[0], null /* activeIface */);
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        // 1000 bytes (100 packets) were downloaded by UID_RED over VPN.
+        // VPN received 1100 bytes (100 packets) over WiFi.
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+              .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1000L, 100L, 0L, 0L, 0L)
+              .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 1100L, 100L, 0L, 0L, 0L));
+
+        mService.getDetailedUidStats(INTERFACES_ALL);
+
+        // Verify internally maintained TUN adjusted stats
+        NetworkStats tunAdjStats = mService.getTunAdjustedStats();
+        // Verify stats for TEST_IFACE (WiFi):
+        // Of 1100 bytes VPN received over WiFi, expect 1000 bytes attributed to UID_RED and 100
+        // bytes attributed to UID_VPN.
+        assertValues(
+                tunAdjStats, TEST_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 1000L, 100L, 0L, 0L, 0);
+        assertValues(
+                tunAdjStats, TEST_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 100L, 0L, 0L, 0L, 0);
+        // Verify stats for TUN_IFACE; only UID_RED should have usage on it.
+        assertValues(
+                tunAdjStats, TUN_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 1000L, 100L, 0L, 0L, 0);
+        assertValues(
+                tunAdjStats, TUN_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 0L, 0L, 0L, 0L, 0);
+
+        // lets assume that since last time, VPN received another 1100 bytes (same assumptions as
+        // before i.e. UID_RED downloaded another 1000 bytes).
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        // Note - NetworkStatsFactory returns counters that are monotonically increasing.
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+              .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 2000L, 200L, 0L, 0L, 0L)
+              .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 2200L, 200L, 0L, 0L, 0L));
+
+        mService.getDetailedUidStats(INTERFACES_ALL);
+
+        tunAdjStats = mService.getTunAdjustedStats();
+        // verify TEST_IFACE stats:
+        assertValues(
+                tunAdjStats, TEST_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 2000L, 200L, 0L, 0L, 0);
+        assertValues(
+                tunAdjStats, TEST_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 200L, 0L, 0L, 0L, 0);
+        // verify TUN_IFACE stats:
+        assertValues(
+                tunAdjStats, TUN_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 2000L, 200L, 0L, 0L, 0);
+        assertValues(
+                tunAdjStats, TUN_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 0L, 0L, 0L, 0L, 0);
+    }
+
+    @Test
+    public void getDetailedUidStats_returnsCorrectStatsWithVpnRunning() throws Exception {
+        // VPN using WiFi (TEST_IFACE).
+        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
+        expectBandwidthControlCheck();
+        mService.forceUpdateIfaces(
+                new Network[0], vpnInfos, new NetworkState[0], null /* activeIface */);
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        // 1000 bytes (100 packets) were downloaded by UID_RED over VPN.
+        // VPN received 1100 bytes (100 packets) over WiFi.
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+              .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1000L, 100L, 0L, 0L, 0L)
+              .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 1100L, 100L, 0L, 0L, 0L));
+
+        // Query realtime stats for TEST_IFACE.
+        NetworkStats queriedStats =
+                mService.getDetailedUidStats(new String[] {TEST_IFACE});
+
+        assertEquals(HOUR_IN_MILLIS, queriedStats.getElapsedRealtime());
+        // verify that returned stats are only for TEST_IFACE and VPN traffic is migrated correctly.
+        assertEquals(new String[] {TEST_IFACE}, queriedStats.getUniqueIfaces());
+        assertValues(
+                queriedStats, TEST_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 1000L, 100L, 0L, 0L, 0);
+        assertValues(
+                queriedStats, TEST_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 100L, 0L, 0L, 0L, 0);
+    }
+
+    @Test
     public void testRegisterUsageCallback() throws Exception {
         // pretend that wifi network comes online; service should ask about full
         // network state, and poll any existing interfaces before updating.
@@ -1382,11 +1757,11 @@
         return new NetworkState(info, prop, new NetworkCapabilities(), VPN_NETWORK, null, null);
     }
 
-    private static VpnInfo createVpnInfo(String underlyingIface) {
+    private static VpnInfo createVpnInfo(String[] underlyingIfaces) {
         VpnInfo info = new VpnInfo();
         info.ownerUid = UID_VPN;
         info.vpnIface = TUN_IFACE;
-        info.primaryUnderlyingIface = underlyingIface;
+        info.underlyingIfaces = underlyingIfaces;
         return info;
     }