Merge "create factory to generated an onClickListener for notifications such that CarSysUI can close the notification window when a notifcation launches an app"
diff --git a/api/current.txt b/api/current.txt
index 9d865b6..f773094 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -29677,8 +29677,8 @@
 
   public class DiscoverySession implements java.lang.AutoCloseable {
     method public void close();
-    method public android.net.NetworkSpecifier createNetworkSpecifierOpen(android.net.wifi.aware.PeerHandle);
-    method public android.net.NetworkSpecifier createNetworkSpecifierPassphrase(android.net.wifi.aware.PeerHandle, java.lang.String);
+    method public deprecated android.net.NetworkSpecifier createNetworkSpecifierOpen(android.net.wifi.aware.PeerHandle);
+    method public deprecated android.net.NetworkSpecifier createNetworkSpecifierPassphrase(android.net.wifi.aware.PeerHandle, java.lang.String);
     method public void sendMessage(android.net.wifi.aware.PeerHandle, int, byte[]);
   }
 
@@ -29763,6 +29763,21 @@
     field public static final int WIFI_AWARE_DATA_PATH_ROLE_RESPONDER = 1; // 0x1
   }
 
+  public static class WifiAwareManager.NetworkSpecifierBuilder {
+    ctor public WifiAwareManager.NetworkSpecifierBuilder();
+    method public android.net.NetworkSpecifier build();
+    method public android.net.wifi.aware.WifiAwareManager.NetworkSpecifierBuilder setDiscoverySession(android.net.wifi.aware.DiscoverySession);
+    method public android.net.wifi.aware.WifiAwareManager.NetworkSpecifierBuilder setPeerHandle(android.net.wifi.aware.PeerHandle);
+    method public android.net.wifi.aware.WifiAwareManager.NetworkSpecifierBuilder setPskPassphrase(java.lang.String);
+  }
+
+  public final class WifiAwareNetworkInfo implements android.os.Parcelable android.net.TransportInfo {
+    method public int describeContents();
+    method public java.net.Inet6Address getPeerIpv6Addr();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.net.wifi.aware.WifiAwareNetworkInfo> CREATOR;
+  }
+
   public class WifiAwareSession implements java.lang.AutoCloseable {
     method public void close();
     method public android.net.NetworkSpecifier createNetworkSpecifierOpen(int, byte[]);
@@ -42225,7 +42240,9 @@
     field public static final int SIOCGIFBRDADDR;
     field public static final int SIOCGIFDSTADDR;
     field public static final int SIOCGIFNETMASK;
+    field public static final int SOCK_CLOEXEC;
     field public static final int SOCK_DGRAM;
+    field public static final int SOCK_NONBLOCK;
     field public static final int SOCK_RAW;
     field public static final int SOCK_SEQPACKET;
     field public static final int SOCK_STREAM;
@@ -44293,6 +44310,8 @@
     method public void removeOnOpportunisticSubscriptionsChangedListener(android.telephony.SubscriptionManager.OnOpportunisticSubscriptionsChangedListener);
     method public void removeOnSubscriptionsChangedListener(android.telephony.SubscriptionManager.OnSubscriptionsChangedListener);
     method public boolean removeSubscriptionsFromGroup(int[]);
+    method public boolean setMetered(boolean, int);
+    method public boolean setOpportunistic(boolean, int);
     method public java.lang.String setSubscriptionGroup(int[]);
     method public void setSubscriptionOverrideCongested(int, boolean, long);
     method public void setSubscriptionOverrideUnmetered(int, boolean, long);
@@ -56356,6 +56375,7 @@
     method public boolean isCursorVisible();
     method public boolean isElegantTextHeight();
     method public boolean isFallbackLineSpacing();
+    method public final boolean isHorizontallyScrolling();
     method public boolean isInputMethodTarget();
     method public boolean isSingleLine();
     method public boolean isSuggestionsEnabled();
diff --git a/api/system-current.txt b/api/system-current.txt
index cc93bba..017df40 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -2762,7 +2762,9 @@
     method public deprecated boolean addGpsNavigationMessageListener(android.location.GpsNavigationMessageEvent.Listener);
     method public void flushGnssBatch();
     method public int getGnssBatchSize();
+    method public java.lang.String getLocationControllerExtraPackage();
     method public java.lang.String getNetworkProviderPackage();
+    method public boolean isLocationControllerExtraPackageEnabled();
     method public boolean isLocationEnabledForUser(android.os.UserHandle);
     method public boolean isProviderEnabledForUser(java.lang.String, android.os.UserHandle);
     method public boolean registerGnssBatchedLocationCallback(long, boolean, android.location.BatchedLocationCallback, android.os.Handler);
@@ -2770,6 +2772,8 @@
     method public deprecated void removeGpsNavigationMessageListener(android.location.GpsNavigationMessageEvent.Listener);
     method public void requestLocationUpdates(android.location.LocationRequest, android.location.LocationListener, android.os.Looper);
     method public void requestLocationUpdates(android.location.LocationRequest, android.app.PendingIntent);
+    method public void setLocationControllerExtraPackage(java.lang.String);
+    method public void setLocationControllerExtraPackageEnabled(boolean);
     method public void setLocationEnabledForUser(boolean, android.os.UserHandle);
     method public boolean setProviderEnabledForUser(java.lang.String, boolean, android.os.UserHandle);
     method public boolean unregisterGnssBatchedLocationCallback(android.location.BatchedLocationCallback);
@@ -3884,7 +3888,11 @@
 package android.net.wifi.aware {
 
   public class DiscoverySession implements java.lang.AutoCloseable {
-    method public android.net.NetworkSpecifier createNetworkSpecifierPmk(android.net.wifi.aware.PeerHandle, byte[]);
+    method public deprecated android.net.NetworkSpecifier createNetworkSpecifierPmk(android.net.wifi.aware.PeerHandle, byte[]);
+  }
+
+  public static class WifiAwareManager.NetworkSpecifierBuilder {
+    method public android.net.wifi.aware.WifiAwareManager.NetworkSpecifierBuilder setPmk(byte[]);
   }
 
   public class WifiAwareSession implements java.lang.AutoCloseable {
@@ -4647,6 +4655,7 @@
 
   public final class Settings {
     field public static final java.lang.String ACTION_ENTERPRISE_PRIVACY_SETTINGS = "android.settings.ENTERPRISE_PRIVACY_SETTINGS";
+    field public static final java.lang.String ACTION_LOCATION_CONTROLLER_EXTRA_PACKAGE_SETTINGS = "android.settings.LOCATION_CONTROLLER_EXTRA_PACKAGE_SETTINGS";
     field public static final java.lang.String ACTION_SHOW_ADMIN_SUPPORT_DETAILS = "android.settings.SHOW_ADMIN_SUPPORT_DETAILS";
   }
 
@@ -5730,16 +5739,15 @@
   public abstract class NetworkService extends android.app.Service {
     ctor public NetworkService();
     method protected abstract android.telephony.NetworkService.NetworkServiceProvider createNetworkServiceProvider(int);
-    field public static final java.lang.String NETWORK_SERVICE_EXTRA_SLOT_ID = "android.telephony.extra.SLOT_ID";
     field public static final java.lang.String NETWORK_SERVICE_INTERFACE = "android.telephony.NetworkService";
   }
 
-  public class NetworkService.NetworkServiceProvider {
+  public abstract class NetworkService.NetworkServiceProvider implements java.lang.AutoCloseable {
     ctor public NetworkService.NetworkServiceProvider(int);
+    method public abstract void close();
     method public void getNetworkRegistrationState(int, android.telephony.NetworkServiceCallback);
     method public final int getSlotId();
     method public final void notifyNetworkRegistrationStateChanged();
-    method protected void onDestroy();
   }
 
   public class NetworkServiceCallback {
@@ -6079,20 +6087,19 @@
   public abstract class DataService extends android.app.Service {
     ctor public DataService();
     method public abstract android.telephony.data.DataService.DataServiceProvider createDataServiceProvider(int);
-    field public static final java.lang.String DATA_SERVICE_EXTRA_SLOT_ID = "android.telephony.data.extra.SLOT_ID";
     field public static final java.lang.String DATA_SERVICE_INTERFACE = "android.telephony.data.DataService";
     field public static final int REQUEST_REASON_HANDOVER = 3; // 0x3
     field public static final int REQUEST_REASON_NORMAL = 1; // 0x1
     field public static final int REQUEST_REASON_SHUTDOWN = 2; // 0x2
   }
 
-  public class DataService.DataServiceProvider {
+  public abstract class DataService.DataServiceProvider implements java.lang.AutoCloseable {
     ctor public DataService.DataServiceProvider(int);
+    method public abstract void close();
     method public void deactivateDataCall(int, int, android.telephony.data.DataServiceCallback);
     method public void getDataCallList(android.telephony.data.DataServiceCallback);
     method public final int getSlotId();
     method public final void notifyDataCallListChanged(java.util.List<android.telephony.data.DataCallResponse>);
-    method protected void onDestroy();
     method public void setDataProfile(java.util.List<android.telephony.data.DataProfile>, boolean, android.telephony.data.DataServiceCallback);
     method public void setInitialAttachApn(android.telephony.data.DataProfile, boolean, android.telephony.data.DataServiceCallback);
     method public void setupDataCall(int, android.telephony.data.DataProfile, boolean, boolean, int, android.net.LinkProperties, android.telephony.data.DataServiceCallback);
@@ -6614,8 +6621,8 @@
     field public static final int CODE_SIP_SERVER_TIMEOUT = 353; // 0x161
     field public static final int CODE_SIP_SERVICE_UNAVAILABLE = 352; // 0x160
     field public static final int CODE_SIP_TEMPRARILY_UNAVAILABLE = 336; // 0x150
-    field public static final int CODE_SIP_TRANSACTION_DOES_NOT_EXIST = 343; // 0x157
     field public static final int CODE_SIP_TOO_MANY_HOPS = 374; // 0x176
+    field public static final int CODE_SIP_TRANSACTION_DOES_NOT_EXIST = 343; // 0x157
     field public static final int CODE_SIP_UNDECIPHERABLE = 378; // 0x17a
     field public static final int CODE_SIP_USER_MARKED_UNWANTED = 365; // 0x16d
     field public static final int CODE_SIP_USER_REJECTED = 361; // 0x169
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index 410bd19..9ee0786 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -1222,7 +1222,7 @@
         STATE_CONNECTED = 1;
     }
     optional State state = 6;
-    optional int64 last_connect_duration_ms = 7;
+    optional int64 last_connect_duration_millis = 7;
 }
 
 
diff --git a/core/java/android/app/job/JobInfo.java b/core/java/android/app/job/JobInfo.java
index e84517d..e7fe161 100644
--- a/core/java/android/app/job/JobInfo.java
+++ b/core/java/android/app/job/JobInfo.java
@@ -1545,13 +1545,6 @@
          * @return The job object to hand to the JobScheduler. This object is immutable.
          */
         public JobInfo build() {
-            // Allow jobs with no constraints - What am I, a database?
-            if (!mHasEarlyConstraint && !mHasLateConstraint && mConstraintFlags == 0 &&
-                    mNetworkRequest == null &&
-                    mTriggerContentUris == null) {
-                throw new IllegalArgumentException("You're trying to build a job with no " +
-                        "constraints, this is not allowed.");
-            }
             // Check that network estimates require network type
             if ((mNetworkDownloadBytes > 0 || mNetworkUploadBytes > 0) && mNetworkRequest == null) {
                 throw new IllegalArgumentException(
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index cbcc492..3e451fe 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -150,6 +150,23 @@
             "android.settings.LOCATION_SOURCE_SETTINGS";
 
     /**
+     * Activity Action: Show settings to allow configuration of location controller extra package.
+     * <p>
+     * In some cases, a matching Activity may not exist, so ensure you
+     * safeguard against this.
+     * <p>
+     * Input: Nothing.
+     * <p>
+     * Output: Nothing.
+     *
+     * @hide
+     */
+    @SystemApi
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_LOCATION_CONTROLLER_EXTRA_PACKAGE_SETTINGS =
+            "android.settings.LOCATION_CONTROLLER_EXTRA_PACKAGE_SETTINGS";
+
+    /**
      * Activity Action: Show scanning settings to allow configuration of Wi-Fi
      * and Bluetooth scanning settings.
      * <p>
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 9fe0ddc..9a1e931 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -1876,6 +1876,7 @@
     }
 
     void dispatchApplyInsets(View host) {
+        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "dispatchApplyInsets");
         WindowInsets insets = getWindowInsets(true /* forceConstruct */);
         final boolean dispatchCutout = (mWindowAttributes.layoutInDisplayCutoutMode
                 == LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS);
@@ -1885,6 +1886,7 @@
             insets = insets.consumeDisplayCutout();
         }
         host.dispatchApplyWindowInsets(insets);
+        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
     }
 
     InsetsController getInsetsController() {
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 90da812..ddf1190 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -719,7 +719,7 @@
     @ViewDebug.ExportedProperty(category = "text")
     @UnsupportedAppUsage
     private int mGravity = Gravity.TOP | Gravity.START;
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
     private boolean mHorizontallyScrolling;
 
     private int mAutoLinkMask;
@@ -5095,13 +5095,24 @@
     }
 
     /**
-     * Returns whether the text is allowed to be wider than the View is.
+     * Returns whether the text is allowed to be wider than the View.
+     * If false, the text will be wrapped to the width of the View.
+     *
+     * @attr ref android.R.styleable#TextView_scrollHorizontally
+     * @see #setHorizontallyScrolling(boolean)
+     */
+    public final boolean isHorizontallyScrolling() {
+        return mHorizontallyScrolling;
+    }
+
+    /**
+     * Returns whether the text is allowed to be wider than the View.
      * If false, the text will be wrapped to the width of the View.
      *
      * @attr ref android.R.styleable#TextView_scrollHorizontally
      * @hide
      */
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
     public boolean getHorizontallyScrolling() {
         return mHorizontallyScrolling;
     }
diff --git a/core/jni/android/graphics/Paint.cpp b/core/jni/android/graphics/Paint.cpp
index df24bc4..e97c9bc 100644
--- a/core/jni/android/graphics/Paint.cpp
+++ b/core/jni/android/graphics/Paint.cpp
@@ -577,7 +577,7 @@
         }
     }
 
-    static SkScalar getMetricsInternal(jlong paintHandle, Paint::FontMetrics *metrics) {
+    static SkScalar getMetricsInternal(jlong paintHandle, SkFontMetrics *metrics) {
         const int kElegantTop = 2500;
         const int kElegantBottom = -1000;
         const int kElegantAscent = 1900;
@@ -609,7 +609,7 @@
     }
 
     static jfloat getFontMetrics(JNIEnv* env, jobject, jlong paintHandle, jobject metricsObj) {
-        Paint::FontMetrics metrics;
+        SkFontMetrics metrics;
         SkScalar spacing = getMetricsInternal(paintHandle, &metrics);
 
         if (metricsObj) {
@@ -624,7 +624,7 @@
     }
 
     static jint getFontMetricsInt(JNIEnv* env, jobject, jlong paintHandle, jobject metricsObj) {
-        Paint::FontMetrics metrics;
+        SkFontMetrics metrics;
 
         getMetricsInternal(paintHandle, &metrics);
         int ascent = SkScalarRoundToInt(metrics.fAscent);
@@ -970,19 +970,19 @@
     }
 
     static jfloat ascent(jlong paintHandle) {
-        Paint::FontMetrics metrics;
+        SkFontMetrics metrics;
         getMetricsInternal(paintHandle, &metrics);
         return SkScalarToFloat(metrics.fAscent);
     }
 
     static jfloat descent(jlong paintHandle) {
-        Paint::FontMetrics metrics;
+        SkFontMetrics metrics;
         getMetricsInternal(paintHandle, &metrics);
         return SkScalarToFloat(metrics.fDescent);
     }
 
     static jfloat getUnderlinePosition(jlong paintHandle) {
-        Paint::FontMetrics metrics;
+        SkFontMetrics metrics;
         getMetricsInternal(paintHandle, &metrics);
         SkScalar position;
         if (metrics.hasUnderlinePosition(&position)) {
@@ -994,7 +994,7 @@
     }
 
     static jfloat getUnderlineThickness(jlong paintHandle) {
-        Paint::FontMetrics metrics;
+        SkFontMetrics metrics;
         getMetricsInternal(paintHandle, &metrics);
         SkScalar thickness;
         if (metrics.hasUnderlineThickness(&thickness)) {
diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto
index 7fe3be8..0ec8c1a 100644
--- a/core/proto/android/server/jobscheduler.proto
+++ b/core/proto/android/server/jobscheduler.proto
@@ -220,6 +220,13 @@
     // will use heartbeats, false will use a rolling window.
     optional bool use_heartbeats = 23;
 
+    message TimeController {
+        // Whether or not TimeController should skip setting wakeup alarms for jobs that aren't
+        // ready now.
+        optional bool skip_not_ready_jobs = 1;
+    }
+    optional TimeController time_controller = 25;
+
     message QuotaController {
         // How much time each app will have to run jobs within their standby bucket window.
         optional int64 allowed_time_per_period_ms = 1;
@@ -242,8 +249,12 @@
         // expected to run only {@link QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS} within the past
         // WINDOW_SIZE_MS.
         optional int64 rare_window_size_ms = 6;
+        // The maximum amount of time an app can have its jobs running within a 24 hour window.
+        optional int64 max_execution_time_ms = 7;
     }
     optional QuotaController quota_controller = 24;
+
+    // Next tag: 26
 }
 
 message StateControllerProto {
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index cab01f9..f25427a 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -3200,6 +3200,8 @@
     <!-- [CHAR LIMIT=40] Title of dialog that is shown when system is starting. -->
     <string name="android_start_title" product="default">Phone is starting\u2026</string>
     <!-- [CHAR LIMIT=40] Title of dialog that is shown when system is starting. -->
+    <string name="android_start_title" product="automotive">Android is starting\u2026</string>
+    <!-- [CHAR LIMIT=40] Title of dialog that is shown when system is starting. -->
     <string name="android_start_title" product="tablet">Tablet is starting\u2026</string>
     <!-- [CHAR LIMIT=40] Title of dialog that is shown when system is starting. -->
     <string name="android_start_title" product="device">Device is starting\u2026</string>
diff --git a/libs/hwui/hwui/Canvas.cpp b/libs/hwui/hwui/Canvas.cpp
index e2ea2bc..a09da6b 100644
--- a/libs/hwui/hwui/Canvas.cpp
+++ b/libs/hwui/hwui/Canvas.cpp
@@ -52,7 +52,7 @@
         const SkScalar left = x;
         const SkScalar right = x + length;
         if (flags & SkPaint::kUnderlineText_ReserveFlag) {
-            Paint::FontMetrics metrics;
+            SkFontMetrics metrics;
             paint.getFontMetrics(&metrics);
             SkScalar position;
             if (!metrics.hasUnderlinePosition(&position)) {
diff --git a/location/java/android/location/ILocationManager.aidl b/location/java/android/location/ILocationManager.aidl
index 32c7520..05d49e5 100644
--- a/location/java/android/location/ILocationManager.aidl
+++ b/location/java/android/location/ILocationManager.aidl
@@ -88,6 +88,10 @@
     boolean providerMeetsCriteria(String provider, in Criteria criteria);
     ProviderProperties getProviderProperties(String provider);
     String getNetworkProviderPackage();
+    void setLocationControllerExtraPackage(String packageName);
+    String getLocationControllerExtraPackage();
+    void setLocationControllerExtraPackageEnabled(boolean enabled);
+    boolean isLocationControllerExtraPackageEnabled();
 
     boolean isProviderEnabledForUser(String provider, int userId);
     boolean setProviderEnabledForUser(String provider, boolean enabled, int userId);
diff --git a/location/java/android/location/LocationManager.java b/location/java/android/location/LocationManager.java
index 334170e..1cd3d86 100644
--- a/location/java/android/location/LocationManager.java
+++ b/location/java/android/location/LocationManager.java
@@ -2396,4 +2396,65 @@
             return null;
         }
     }
+
+    /**
+     * Set the extra location controller package for location services on the device.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(Manifest.permission.LOCATION_HARDWARE)
+    public void setLocationControllerExtraPackage(String packageName) {
+        try {
+            mService.setLocationControllerExtraPackage(packageName);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the extra location controller package on the device.
+     *
+     * @hide
+     */
+    @SystemApi
+    public @Nullable String getLocationControllerExtraPackage() {
+        try {
+            return mService.getLocationControllerExtraPackage();
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+            return null;
+        }
+    }
+
+    /**
+     * Set whether the extra location controller package is currently enabled on the device.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(Manifest.permission.LOCATION_HARDWARE)
+    public void setLocationControllerExtraPackageEnabled(boolean enabled) {
+        try {
+            mService.setLocationControllerExtraPackageEnabled(enabled);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns whether extra location controller package is currently enabled on the device.
+     *
+     * @hide
+     */
+    @SystemApi
+    public boolean isLocationControllerExtraPackageEnabled() {
+        try {
+            return mService.isLocationControllerExtraPackageEnabled();
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+            return false;
+        }
+    }
+
 }
diff --git a/media/java/android/media/DataSourceCallback.java b/media/java/android/media/DataSourceCallback.java
index 0d4d531..1afcd20 100644
--- a/media/java/android/media/DataSourceCallback.java
+++ b/media/java/android/media/DataSourceCallback.java
@@ -47,7 +47,7 @@
      * @param offset the offset within buffer to read the data into.
      * @param size the number of bytes to read.
      * @throws IOException on fatal errors.
-     * @return the number of bytes read, or -1 if there was an error.
+     * @return the number of bytes read, or -1 if end of stream is reached.
      */
     public abstract int readAt(long position, byte[] buffer, int offset, int size)
             throws IOException;
diff --git a/media/java/android/media/MediaDataSource.java b/media/java/android/media/MediaDataSource.java
index 4ba2120..4bdc1ad 100644
--- a/media/java/android/media/MediaDataSource.java
+++ b/media/java/android/media/MediaDataSource.java
@@ -46,7 +46,7 @@
      * @param offset the offset within buffer to read the data into.
      * @param size the number of bytes to read.
      * @throws IOException on fatal errors.
-     * @return the number of bytes read, or -1 if there was an error.
+     * @return the number of bytes read, or -1 if end of stream is reached.
      */
     public abstract int readAt(long position, byte[] buffer, int offset, int size)
             throws IOException;
diff --git a/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java b/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java
index 1737b64..85dab57 100644
--- a/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java
+++ b/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java
@@ -107,36 +107,44 @@
     private CarAudioManager mCarAudioManager;
     private final CarAudioManager.CarVolumeCallback mVolumeChangeCallback =
             new CarAudioManager.CarVolumeCallback() {
-        @Override
-        public void onGroupVolumeChanged(int zoneId, int groupId, int flags) {
-            // TODO: Include zoneId into consideration.
-            // For instance
-            // - single display + single-zone, ignore zoneId
-            // - multi-display + single-zone, zoneId is fixed, may show volume bar on all displays
-            // - single-display + multi-zone, may show volume bar on primary display only
-            // - multi-display + multi-zone, may show volume bar on display specified by zoneId
-            VolumeItem volumeItem = mAvailableVolumeItems.get(groupId);
-            int value = getSeekbarValue(mCarAudioManager, groupId);
-            // Do not update the progress if it is the same as before. When car audio manager sets
-            // its group volume caused by the seekbar progress changed, it also triggers this
-            // callback. Updating the seekbar at the same time could block the continuous seeking.
-            if (value != volumeItem.progress) {
-                volumeItem.listItem.setProgress(value);
-                volumeItem.progress = value;
-            }
-            if ((flags & AudioManager.FLAG_SHOW_UI) != 0) {
-                mHandler.obtainMessage(H.SHOW, Events.SHOW_REASON_VOLUME_CHANGED).sendToTarget();
-            }
-        }
+                @Override
+                public void onGroupVolumeChanged(int zoneId, int groupId, int flags) {
+                    // TODO: Include zoneId into consideration.
+                    // For instance
+                    // - single display + single-zone, ignore zoneId
+                    // - multi-display + single-zone, zoneId is fixed, may show volume bar on all
+                    // displays
+                    // - single-display + multi-zone, may show volume bar on primary display only
+                    // - multi-display + multi-zone, may show volume bar on display specified by
+                    // zoneId
+                    VolumeItem volumeItem = mAvailableVolumeItems.get(groupId);
+                    int value = getSeekbarValue(mCarAudioManager, groupId);
+                    // Do not update the progress if it is the same as before. When car audio
+                    // manager sets
+                    // its group volume caused by the seekbar progress changed, it also triggers
+                    // this
+                    // callback. Updating the seekbar at the same time could block the continuous
+                    // seeking.
+                    if (value != volumeItem.progress) {
+                        volumeItem.listItem.setProgress(value);
+                        volumeItem.progress = value;
+                    }
+                    if ((flags & AudioManager.FLAG_SHOW_UI) != 0) {
+                        mHandler.obtainMessage(H.SHOW,
+                                Events.SHOW_REASON_VOLUME_CHANGED).sendToTarget();
+                    }
+                }
 
-        @Override
-        public void onMasterMuteChanged(int zoneId, int flags) {
-            // ignored
-        }
-    };
+                @Override
+                public void onMasterMuteChanged(int zoneId, int flags) {
+                    // ignored
+                }
+            };
     private boolean mHovering;
     private boolean mShowing;
     private boolean mExpanded;
+    private View mExpandIcon;
+    private VolumeItem mDefaultVolumeItem;
     private final ServiceConnection mServiceConnection = new ServiceConnection() {
         @Override
         public void onServiceConnected(ComponentName name, IBinder service) {
@@ -151,10 +159,8 @@
                     mAvailableVolumeItems.add(volumeItem);
                     // The first one is the default item.
                     if (groupId == 0) {
-                        volumeItem.defaultItem = true;
-                        addSeekbarListItem(volumeItem, groupId,
-                                R.drawable.car_ic_keyboard_arrow_down,
-                                new ExpandIconListener());
+                        mDefaultVolumeItem = volumeItem;
+                        setupDefaultListItem();
                     }
                 }
 
@@ -178,6 +184,13 @@
         }
     };
 
+    private void setupDefaultListItem() {
+        mDefaultVolumeItem.defaultItem = true;
+        addSeekbarListItem(mDefaultVolumeItem, /* volumeGroupId = */0,
+                R.drawable.car_ic_keyboard_arrow_down, new ExpandIconListener()
+        );
+    }
+
     public CarVolumeDialogImpl(Context context) {
         mContext = new ContextThemeWrapper(context, com.android.systemui.R.style.qs_theme);
         mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
@@ -294,7 +307,9 @@
             return;
         }
         mShowing = true;
-
+        if (mVolumeLineItems.isEmpty()) {
+            setupDefaultListItem();
+        }
         mDialog.show();
         Events.writeEvent(mContext, Events.EVENT_SHOW_DIALOG, reason, mKeyguard.isKeyguardLocked());
     }
@@ -340,6 +355,13 @@
                     }
                     mDialog.dismiss();
                     mShowing = false;
+                    mShowing = false;
+                    // if mExpandIcon is null that means user never clicked on the expanded arrow
+                    // which implies that the dialog is still not expanded. In that case we do
+                    // not want to reset the state
+                    if (mExpandIcon != null && mExpanded) {
+                        toggleDialogExpansion(/* isClicked = */ false);
+                    }
                 }, DISMISS_DELAY_IN_MILLIS))
                 .start();
 
@@ -517,52 +539,62 @@
     }
 
     private final class ExpandIconListener implements View.OnClickListener {
-
         @Override
         public void onClick(final View v) {
-            mExpanded = !mExpanded;
-            Animator inAnimator;
-            if (mExpanded) {
-                for (int groupId = 0; groupId < mAvailableVolumeItems.size(); ++groupId) {
-                    // Adding the items which are not coming from the default item.
-                    VolumeItem volumeItem = mAvailableVolumeItems.get(groupId);
-                    if (volumeItem.defaultItem) {
-                        // Set progress here due to the progress of seekbar may not be updated.
-                        volumeItem.listItem.setProgress(volumeItem.progress);
-                    } else {
-                        addSeekbarListItem(volumeItem, groupId, 0, null);
-                    }
-                }
-                inAnimator = AnimatorInflater.loadAnimator(
-                        mContext, R.anim.car_arrow_fade_in_rotate_up);
-            } else {
-                // Only keeping the default stream if it is not expended.
-                Iterator itr = mVolumeLineItems.iterator();
-                while (itr.hasNext()) {
-                    SeekbarListItem seekbarListItem = (SeekbarListItem) itr.next();
-                    VolumeItem volumeItem = findVolumeItem(seekbarListItem);
-                    if (!volumeItem.defaultItem) {
-                        itr.remove();
-                    } else {
-                        // Set progress here due to the progress of seekbar may not be updated.
-                        seekbarListItem.setProgress(volumeItem.progress);
-                    }
-                }
-                inAnimator = AnimatorInflater.loadAnimator(
-                        mContext, R.anim.car_arrow_fade_in_rotate_down);
-            }
-
-            Animator outAnimator = AnimatorInflater.loadAnimator(
-                    mContext, R.anim.car_arrow_fade_out);
-            inAnimator.setStartDelay(ARROW_FADE_IN_START_DELAY_IN_MILLIS);
-            AnimatorSet animators = new AnimatorSet();
-            animators.playTogether(outAnimator, inAnimator);
-            animators.setTarget(v);
-            animators.start();
-            mPagedListAdapter.notifyDataSetChanged();
+            mExpandIcon = v;
+            toggleDialogExpansion(true);
         }
     }
 
+    private void toggleDialogExpansion(boolean isClicked) {
+        mExpanded = !mExpanded;
+        Animator inAnimator;
+        if (mExpanded) {
+            for (int groupId = 0; groupId < mAvailableVolumeItems.size(); ++groupId) {
+                // Adding the items which are not coming from the default item.
+                VolumeItem volumeItem = mAvailableVolumeItems.get(groupId);
+                if (volumeItem.defaultItem) {
+                    // Set progress here due to the progress of seekbar may not be updated.
+                    volumeItem.listItem.setProgress(volumeItem.progress);
+                } else {
+                    addSeekbarListItem(volumeItem, groupId, 0, null);
+                }
+            }
+            inAnimator = AnimatorInflater.loadAnimator(
+                    mContext, R.anim.car_arrow_fade_in_rotate_up);
+
+        } else {
+            // Only keeping the default stream if it is not expended.
+            Iterator itr = mVolumeLineItems.iterator();
+            while (itr.hasNext()) {
+                SeekbarListItem seekbarListItem = (SeekbarListItem) itr.next();
+                VolumeItem volumeItem = findVolumeItem(seekbarListItem);
+                if (!volumeItem.defaultItem) {
+                    itr.remove();
+                } else {
+                    // Set progress here due to the progress of seekbar may not be updated.
+                    seekbarListItem.setProgress(volumeItem.progress);
+                }
+            }
+            inAnimator = AnimatorInflater.loadAnimator(
+                    mContext, R.anim.car_arrow_fade_in_rotate_down);
+        }
+
+        Animator outAnimator = AnimatorInflater.loadAnimator(
+                mContext, R.anim.car_arrow_fade_out);
+        inAnimator.setStartDelay(ARROW_FADE_IN_START_DELAY_IN_MILLIS);
+        AnimatorSet animators = new AnimatorSet();
+        animators.playTogether(outAnimator, inAnimator);
+        if (!isClicked) {
+            // Do not animate when the state is called to reset the dialogs view and not clicked
+            // by user.
+            animators.setDuration(0);
+        }
+        animators.setTarget(mExpandIcon);
+        animators.start();
+        mPagedListAdapter.notifyDataSetChanged();
+    }
+
     private final class VolumeSeekBarChangeListener implements OnSeekBarChangeListener {
 
         private final int mVolumeGroupId;
@@ -601,4 +633,4 @@
         public void onStopTrackingTouch(SeekBar seekBar) {
         }
     }
-}
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/ActionButtonsPreference/Android.bp b/packages/SettingsLib/ActionButtonsPreference/Android.bp
index e518e0b..cd3fb0c 100644
--- a/packages/SettingsLib/ActionButtonsPreference/Android.bp
+++ b/packages/SettingsLib/ActionButtonsPreference/Android.bp
@@ -1,5 +1,5 @@
 android_library {
-    name: "ActionButtonsPreference",
+    name: "SettingsLibActionButtonsPreference",
 
     srcs: ["src/**/*.java"],
     resource_dirs: ["res"],
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index 0126e7e5..042808a0 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -16,8 +16,8 @@
         "SettingsLibAppPreference",
         "SettingsLibSearchWidget",
         "SettingsLibSettingsSpinner",
-        "SettingsLayoutPreference",
-        "ActionButtonsPreference",
+        "SettingsLibLayoutPreference",
+        "SettingsLibActionButtonsPreference",
         "SettingsLibEntityHeaderWidgets",
     ],
 
diff --git a/packages/SettingsLib/SettingsLayoutPreference/Android.bp b/packages/SettingsLib/LayoutPreference/Android.bp
similarity index 83%
rename from packages/SettingsLib/SettingsLayoutPreference/Android.bp
rename to packages/SettingsLib/LayoutPreference/Android.bp
index 489d360..a1f9a76 100644
--- a/packages/SettingsLib/SettingsLayoutPreference/Android.bp
+++ b/packages/SettingsLib/LayoutPreference/Android.bp
@@ -1,5 +1,5 @@
 android_library {
-    name: "SettingsLayoutPreference",
+    name: "SettingsLibLayoutPreference",
 
     srcs: ["src/**/*.java"],
     resource_dirs: ["res"],
diff --git a/packages/SettingsLib/SettingsLayoutPreference/AndroidManifest.xml b/packages/SettingsLib/LayoutPreference/AndroidManifest.xml
similarity index 100%
rename from packages/SettingsLib/SettingsLayoutPreference/AndroidManifest.xml
rename to packages/SettingsLib/LayoutPreference/AndroidManifest.xml
diff --git a/packages/SettingsLib/SettingsLayoutPreference/res/layout/layout_preference_frame.xml b/packages/SettingsLib/LayoutPreference/res/layout/layout_preference_frame.xml
similarity index 100%
rename from packages/SettingsLib/SettingsLayoutPreference/res/layout/layout_preference_frame.xml
rename to packages/SettingsLib/LayoutPreference/res/layout/layout_preference_frame.xml
diff --git a/packages/SettingsLib/SettingsLayoutPreference/res/layout/settings_entity_header.xml b/packages/SettingsLib/LayoutPreference/res/layout/settings_entity_header.xml
similarity index 100%
rename from packages/SettingsLib/SettingsLayoutPreference/res/layout/settings_entity_header.xml
rename to packages/SettingsLib/LayoutPreference/res/layout/settings_entity_header.xml
diff --git a/packages/SettingsLib/SettingsLayoutPreference/res/values/styles.xml b/packages/SettingsLib/LayoutPreference/res/values/styles.xml
similarity index 100%
rename from packages/SettingsLib/SettingsLayoutPreference/res/values/styles.xml
rename to packages/SettingsLib/LayoutPreference/res/values/styles.xml
diff --git a/packages/SettingsLib/SettingsLayoutPreference/src/com/android/settingslib/widget/LayoutPreference.java b/packages/SettingsLib/LayoutPreference/src/com/android/settingslib/widget/LayoutPreference.java
similarity index 100%
rename from packages/SettingsLib/SettingsLayoutPreference/src/com/android/settingslib/widget/LayoutPreference.java
rename to packages/SettingsLib/LayoutPreference/src/com/android/settingslib/widget/LayoutPreference.java
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/AlertTransferListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/AlertTransferListener.java
new file mode 100644
index 0000000..13e991b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/AlertTransferListener.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification;
+
+/**
+ * Listener interface for when NotificationEntryManager needs to tell
+ * NotificationGroupAlertTransferHelper things. Will eventually grow to be a general-purpose
+ * listening interface for the NotificationEntryManager.
+ */
+public interface AlertTransferListener {
+    /**
+     * Called when a new notification is posted. At this point, the notification is "pending": its
+     * views haven't been inflated yet and most of the system pretends like it doesn't exist yet.
+     */
+    void onPendingEntryAdded(NotificationData.Entry entry);
+
+    /**
+     * Called when an existing notification's views are reinflated (usually due to an update being
+     * posted to that notification).
+     */
+    void onEntryReinflated(NotificationData.Entry entry);
+
+    /**
+     * Called when a notification has been removed (either because the user swiped it away or
+     * because the developer retracted it).
+     */
+    void onEntryRemoved(NotificationData.Entry entry);
+}
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 5e244f8..aab3fc4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
@@ -62,7 +62,6 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.EventLogTags;
 import com.android.systemui.ForegroundServiceController;
-import com.android.systemui.InitController;
 import com.android.systemui.R;
 import com.android.systemui.UiOffloadThread;
 import com.android.systemui.bubbles.BubbleController;
@@ -84,7 +83,6 @@
 import com.android.systemui.statusbar.notification.row.NotificationInflater.InflationFlag;
 import com.android.systemui.statusbar.notification.row.RowInflaterTask;
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
-import com.android.systemui.statusbar.phone.NotificationGroupAlertTransferHelper;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.phone.StatusBar;
@@ -121,8 +119,6 @@
 
     private final NotificationGroupManager mGroupManager =
             Dependency.get(NotificationGroupManager.class);
-    private final NotificationGroupAlertTransferHelper mGroupAlertTransferHelper =
-            Dependency.get(NotificationGroupAlertTransferHelper.class);
     private final NotificationGutsManager mGutsManager =
             Dependency.get(NotificationGutsManager.class);
     private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
@@ -164,6 +160,7 @@
             = new ArrayList<>();
     private ExpandableNotificationRow.OnAppOpsClickListener mOnAppOpsClickListener;
     private NotificationViewHierarchyManager.StatusBarStateListener mStatusBarStateListener;
+    @Nullable private AlertTransferListener mAlertTransferListener;
 
     private final class NotificationClicker implements View.OnClickListener {
 
@@ -265,13 +262,11 @@
         mMessagingUtil = new NotificationMessagingUtil(context);
         mBubbleController.setDismissListener(this /* bubbleEventListener */);
         mNotificationData = new NotificationData();
-        Dependency.get(InitController.class).addPostInitTask(this::onPostInit);
         mDeferredNotificationViewUpdateHandler = new Handler();
     }
 
-    private void onPostInit() {
-        mGroupAlertTransferHelper.setPendingEntries(mPendingNotifications);
-        mGroupManager.addOnGroupChangeListener(mGroupAlertTransferHelper);
+    public void setAlertTransferListener(AlertTransferListener listener) {
+        mAlertTransferListener = listener;
     }
 
     /**
@@ -607,7 +602,9 @@
                     mVisualStabilityManager.onLowPriorityUpdated(entry);
                     mPresenter.updateNotificationViews();
                 }
-                mGroupAlertTransferHelper.onInflationFinished(entry);
+                if (mAlertTransferListener != null) {
+                    mAlertTransferListener.onEntryReinflated(entry);
+                }
             }
         }
         entry.setLowPriorityStateUpdated(false);
@@ -620,8 +617,12 @@
 
     private void removeNotificationInternal(String key,
             @Nullable NotificationListenerService.RankingMap ranking, boolean forceRemove) {
+        final NotificationData.Entry entry = mNotificationData.get(key);
+
         abortExistingInflation(key);
-        mGroupAlertTransferHelper.cleanUpPendingAlertInfo(key);
+        if (mAlertTransferListener != null && entry != null) {
+            mAlertTransferListener.onEntryRemoved(entry);
+        }
 
         // Attempt to remove notifications from their alert managers (heads up, ambient pulse).
         // Though the remove itself may fail, it lets the manager know to remove as soon as
@@ -640,8 +641,6 @@
             mAmbientPulseManager.removeNotification(key, false /* ignoreEarliestRemovalTime */);
         }
 
-        NotificationData.Entry entry = mNotificationData.get(key);
-
         if (entry == null) {
             mCallback.onNotificationRemoved(key, null /* old */);
             return;
@@ -866,7 +865,9 @@
                 mNotificationData.getImportance(key));
 
         mPendingNotifications.put(key, shadeEntry);
-        mGroupAlertTransferHelper.onPendingEntryAdded(shadeEntry);
+        if (mAlertTransferListener != null) {
+            mAlertTransferListener.onPendingEntryAdded(shadeEntry);
+        }
     }
 
     @VisibleForTesting
@@ -1253,6 +1254,15 @@
     }
 
     /**
+     * @return An iterator for all "pending" notifications. Pending notifications are newly-posted
+     * notifications whose views have not yet been inflated. In general, the system pretends like
+     * these don't exist, although there are a couple exceptions.
+     */
+    public Iterable<NotificationData.Entry> getPendingNotificationsIterator() {
+        return mPendingNotifications.values();
+    }
+
+    /**
      * Callback for NotificationEntryManager.
      */
     public interface Callback {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelper.java
index 2a68fa5..dd81c4e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelper.java
@@ -29,7 +29,9 @@
 import com.android.systemui.statusbar.InflationTask;
 import com.android.systemui.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.StatusBarStateController.StateListener;
+import com.android.systemui.statusbar.notification.AlertTransferListener;
 import com.android.systemui.statusbar.notification.NotificationData.Entry;
+import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.row.NotificationInflater.AsyncInflationTask;
 import com.android.systemui.statusbar.notification.row.NotificationInflater.InflationFlag;
 import com.android.systemui.statusbar.phone.NotificationGroupManager.NotificationGroup;
@@ -38,8 +40,6 @@
 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
 
 import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
 import java.util.Objects;
 
 /**
@@ -47,8 +47,8 @@
  * {@link HeadsUpManager}, {@link AmbientPulseManager}. In particular, this class deals with keeping
  * the correct notification in a group alerting based off the group suppression.
  */
-public class NotificationGroupAlertTransferHelper implements OnGroupChangeListener,
-        OnHeadsUpChangedListener, OnAmbientChangedListener, StateListener {
+public class NotificationGroupAlertTransferHelper implements OnHeadsUpChangedListener,
+        OnAmbientChangedListener, StateListener {
 
     private static final long ALERT_TRANSFER_TIMEOUT = 300;
 
@@ -69,15 +69,7 @@
     private final NotificationGroupManager mGroupManager =
             Dependency.get(NotificationGroupManager.class);
 
-    // TODO(b/119637830): It would be good if GroupManager already had all pending notifications as
-    // normal children (i.e. add notifications to GroupManager before inflation) so that we don't
-    // have to have this dependency. We'd also have to worry less about the suppression not being up
-    // to date.
-    /**
-     * Notifications that are currently inflating for the first time. Used to remove an incorrectly
-     * alerting notification faster.
-     */
-    private HashMap<String, Entry> mPendingNotifications;
+    private NotificationEntryManager mEntryManager;
 
     private boolean mIsDozing;
 
@@ -85,6 +77,23 @@
         Dependency.get(StatusBarStateController.class).addCallback(this);
     }
 
+    /** Causes the TransferHelper to register itself as a listener to the appropriate classes. */
+    public void bind(NotificationEntryManager entryManager,
+            NotificationGroupManager groupManager) {
+        if (mEntryManager != null) {
+            throw new IllegalStateException("Already bound.");
+        }
+
+        // TODO(b/119637830): It would be good if GroupManager already had all pending notifications
+        // as normal children (i.e. add notifications to GroupManager before inflation) so that we
+        // don't have to have this dependency. We'd also have to worry less about the suppression
+        // not being up to date.
+        mEntryManager = entryManager;
+
+        mEntryManager.setAlertTransferListener(mAlertTransferListener);
+        groupManager.addOnGroupChangeListener(mOnGroupChangeListener);
+    }
+
     /**
      * Whether or not a notification has transferred its alert state to the notification and
      * the notification should alert after inflating.
@@ -97,25 +106,10 @@
         return alertInfo != null && alertInfo.isStillValid();
     }
 
-    /**
-     * Removes any alerts pending on this entry. Note that this will not stop any inflation tasks
-     * started by a transfer, so this should only be used as clean-up for when inflation is stopped
-     * and the pending alert no longer needs to happen.
-     *
-     * @param key notification key that may have info that needs to be cleaned up
-     */
-    public void cleanUpPendingAlertInfo(@NonNull String key) {
-        mPendingAlerts.remove(key);
-    }
-
     public void setHeadsUpManager(HeadsUpManager headsUpManager) {
         mHeadsUpManager = headsUpManager;
     }
 
-    public void setPendingEntries(HashMap<String, Entry> pendingNotifications) {
-        mPendingNotifications = pendingNotifications;
-    }
-
     @Override
     public void onStateChanged(int newState) {}
 
@@ -130,43 +124,45 @@
         mIsDozing = isDozing;
     }
 
-    @Override
-    public void onGroupCreated(NotificationGroup group, String groupKey) {
-        mGroupAlertEntries.put(groupKey, new GroupAlertEntry(group));
-    }
+    private final OnGroupChangeListener mOnGroupChangeListener = new OnGroupChangeListener() {
+        @Override
+        public void onGroupCreated(NotificationGroup group, String groupKey) {
+            mGroupAlertEntries.put(groupKey, new GroupAlertEntry(group));
+        }
 
-    @Override
-    public void onGroupRemoved(NotificationGroup group, String groupKey) {
-        mGroupAlertEntries.remove(groupKey);
-    }
+        @Override
+        public void onGroupRemoved(NotificationGroup group, String groupKey) {
+            mGroupAlertEntries.remove(groupKey);
+        }
 
-    @Override
-    public void onGroupSuppressionChanged(NotificationGroup group, boolean suppressed) {
-        AlertingNotificationManager alertManager = getActiveAlertManager();
-        if (suppressed) {
-            if (alertManager.isAlerting(group.summary.key)) {
-                handleSuppressedSummaryAlerted(group.summary, alertManager);
-            }
-        } else {
-            // Group summary can be null if we are no longer suppressed because the summary was
-            // removed. In that case, we don't need to alert the summary.
-            if (group.summary == null) {
-                return;
-            }
-            GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(mGroupManager.getGroupKey(
-                    group.summary.notification));
-            // Group is no longer suppressed. We should check if we need to transfer the alert
-            // back to the summary now that it's no longer suppressed.
-            if (groupAlertEntry.mAlertSummaryOnNextAddition) {
-                if (!alertManager.isAlerting(group.summary.key)) {
-                    alertNotificationWhenPossible(group.summary, alertManager);
+        @Override
+        public void onGroupSuppressionChanged(NotificationGroup group, boolean suppressed) {
+            AlertingNotificationManager alertManager = getActiveAlertManager();
+            if (suppressed) {
+                if (alertManager.isAlerting(group.summary.key)) {
+                    handleSuppressedSummaryAlerted(group.summary, alertManager);
                 }
-                groupAlertEntry.mAlertSummaryOnNextAddition = false;
             } else {
-                checkShouldTransferBack(groupAlertEntry);
+                // Group summary can be null if we are no longer suppressed because the summary was
+                // removed. In that case, we don't need to alert the summary.
+                if (group.summary == null) {
+                    return;
+                }
+                GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(mGroupManager.getGroupKey(
+                        group.summary.notification));
+                // Group is no longer suppressed. We should check if we need to transfer the alert
+                // back to the summary now that it's no longer suppressed.
+                if (groupAlertEntry.mAlertSummaryOnNextAddition) {
+                    if (!alertManager.isAlerting(group.summary.key)) {
+                        alertNotificationWhenPossible(group.summary, alertManager);
+                    }
+                    groupAlertEntry.mAlertSummaryOnNextAddition = false;
+                } else {
+                    checkShouldTransferBack(groupAlertEntry);
+                }
             }
         }
-    }
+    };
 
     @Override
     public void onAmbientStateChanged(Entry entry, boolean isAmbient) {
@@ -185,37 +181,42 @@
         }
     }
 
-    /**
-     * Called when the entry's reinflation has finished. If there is an alert pending, we then
-     * show the alert.
-     *
-     * @param entry entry whose inflation has finished
-     */
-    public void onInflationFinished(@NonNull Entry entry) {
-        PendingAlertInfo alertInfo = mPendingAlerts.remove(entry.key);
-        if (alertInfo != null) {
-            if (alertInfo.isStillValid()) {
-                alertNotificationWhenPossible(entry, getActiveAlertManager());
-            } else {
-                // The transfer is no longer valid. Free the content.
-                entry.getRow().freeContentViewWhenSafe(alertInfo.mAlertManager.getContentFlag());
+    private final AlertTransferListener mAlertTransferListener = new AlertTransferListener() {
+        // Called when a new notification has been posted but is not inflated yet. We use this to
+        // see as early as we can if we need to abort a transfer.
+        @Override
+        public void onPendingEntryAdded(Entry entry) {
+            String groupKey = mGroupManager.getGroupKey(entry.notification);
+            GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(groupKey);
+            if (groupAlertEntry != null) {
+                checkShouldTransferBack(groupAlertEntry);
             }
         }
-    }
 
-    /**
-     * Called when a new notification has been posted but is not inflated yet. We use this to see
-     * as early as we can if we need to abort a transfer.
-     *
-     * @param entry entry that has been added
-     */
-    public void onPendingEntryAdded(@NonNull Entry entry) {
-        String groupKey = mGroupManager.getGroupKey(entry.notification);
-        GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(groupKey);
-        if (groupAlertEntry != null) {
-            checkShouldTransferBack(groupAlertEntry);
+        // Called when the entry's reinflation has finished. If there is an alert pending, we
+        // then show the alert.
+        @Override
+        public void onEntryReinflated(Entry entry) {
+            PendingAlertInfo alertInfo = mPendingAlerts.remove(entry.key);
+            if (alertInfo != null) {
+                if (alertInfo.isStillValid()) {
+                    alertNotificationWhenPossible(entry, getActiveAlertManager());
+                } else {
+                    // The transfer is no longer valid. Free the content.
+                    entry.getRow().freeContentViewWhenSafe(
+                            alertInfo.mAlertManager.getContentFlag());
+                }
+            }
         }
-    }
+
+        @Override
+        public void onEntryRemoved(Entry entry) {
+            // Removes any alerts pending on this entry. Note that this will not stop any inflation
+            // tasks started by a transfer, so this should only be used as clean-up for when
+            // inflation is stopped and the pending alert no longer needs to happen.
+            mPendingAlerts.remove(entry.key);
+        }
+    };
 
     /**
      * Gets the number of new notifications pending inflation that will be added to the group
@@ -225,11 +226,11 @@
      * @return the number of new notifications that will be added to the group
      */
     private int getPendingChildrenNotAlerting(@NonNull NotificationGroup group) {
-        if (mPendingNotifications == null) {
+        if (mEntryManager == null) {
             return 0;
         }
         int number = 0;
-        Collection<Entry> values = mPendingNotifications.values();
+        Iterable<Entry> values = mEntryManager.getPendingNotificationsIterator();
         for (Entry entry : values) {
             if (isPendingNotificationInGroup(entry, group) && onlySummaryAlerts(entry)) {
                 number++;
@@ -245,10 +246,10 @@
      * @return true if a pending notification will add to this group
      */
     private boolean pendingInflationsWillAddChildren(@NonNull NotificationGroup group) {
-        if (mPendingNotifications == null) {
+        if (mEntryManager == null) {
             return false;
         }
-        Collection<Entry> values = mPendingNotifications.values();
+        Iterable<Entry> values = mEntryManager.getPendingNotificationsIterator();
         for (Entry entry : values) {
             if (isPendingNotificationInGroup(entry, group)) {
                 return true;
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 1d6a1e8..a508f1b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -628,6 +628,8 @@
         mBubbleController = Dependency.get(BubbleController.class);
         mBubbleController.setExpandListener(mBubbleExpandListener);
 
+        mGroupAlertTransferHelper.bind(mEntryManager, mGroupManager);
+
         mColorExtractor.addOnColorsChangedListener(this);
         mStatusBarStateController.addCallback(this, StatusBarStateController.RANK_STATUS_BAR);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java
index c3bc511..ee39e10 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java
@@ -32,14 +32,19 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.statusbar.AmbientPulseManager;
+import com.android.systemui.statusbar.notification.AlertTransferListener;
 import com.android.systemui.statusbar.notification.NotificationData;
 import com.android.systemui.statusbar.notification.NotificationData.Entry;
+import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
@@ -49,13 +54,15 @@
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
 public class NotificationGroupAlertTransferHelperTest extends SysuiTestCase {
-    @Rule
-    public MockitoRule rule = MockitoJUnit.rule();
+    @Rule public MockitoRule rule = MockitoJUnit.rule();
 
     private NotificationGroupAlertTransferHelper mGroupAlertTransferHelper;
     private NotificationGroupManager mGroupManager;
     private AmbientPulseManager mAmbientPulseManager;
     private HeadsUpManager mHeadsUpManager;
+    @Mock private NotificationEntryManager mNotificationEntryManager;
+    @Captor private ArgumentCaptor<AlertTransferListener> mListenerCaptor;
+    private AlertTransferListener mAlertTransferListener;
     private final HashMap<String, Entry> mPendingEntries = new HashMap<>();
     private final NotificationGroupTestHelper mGroupTestHelper =
             new NotificationGroupTestHelper(mContext);
@@ -67,15 +74,19 @@
         mDependency.injectTestDependency(AmbientPulseManager.class, mAmbientPulseManager);
         mHeadsUpManager = new HeadsUpManager(mContext) {};
 
+        when(mNotificationEntryManager.getPendingNotificationsIterator())
+                .thenReturn(mPendingEntries.values());
+
         mGroupManager = new NotificationGroupManager();
         mDependency.injectTestDependency(NotificationGroupManager.class, mGroupManager);
         mGroupManager.setHeadsUpManager(mHeadsUpManager);
 
         mGroupAlertTransferHelper = new NotificationGroupAlertTransferHelper();
         mGroupAlertTransferHelper.setHeadsUpManager(mHeadsUpManager);
-        mGroupAlertTransferHelper.setPendingEntries(mPendingEntries);
 
-        mGroupManager.addOnGroupChangeListener(mGroupAlertTransferHelper);
+        mGroupAlertTransferHelper.bind(mNotificationEntryManager, mGroupManager);
+        verify(mNotificationEntryManager).setAlertTransferListener(mListenerCaptor.capture());
+        mAlertTransferListener = mListenerCaptor.getValue();
         mHeadsUpManager.addListener(mGroupAlertTransferHelper);
         mAmbientPulseManager.addListener(mGroupAlertTransferHelper);
     }
@@ -110,7 +121,7 @@
 
         // Add second child notification so that summary is no longer suppressed.
         mPendingEntries.put(childEntry2.key, childEntry2);
-        mGroupAlertTransferHelper.onPendingEntryAdded(childEntry2);
+        mAlertTransferListener.onPendingEntryAdded(childEntry2);
         mGroupManager.onEntryAdded(childEntry2);
 
         // The alert state should transfer back to the summary as there is now more than one
@@ -137,7 +148,7 @@
 
         // Add second child notification so that summary is no longer suppressed.
         mPendingEntries.put(childEntry2.key, childEntry2);
-        mGroupAlertTransferHelper.onPendingEntryAdded(childEntry2);
+        mAlertTransferListener.onPendingEntryAdded(childEntry2);
         mGroupManager.onEntryAdded(childEntry2);
 
         // Dozing changed so no reason to re-alert summary.
@@ -175,7 +186,7 @@
 
         when(childEntry.getRow().isInflationFlagSet(mHeadsUpManager.getContentFlag()))
             .thenReturn(true);
-        mGroupAlertTransferHelper.onInflationFinished(childEntry);
+        mAlertTransferListener.onEntryReinflated(childEntry);
 
         // Alert is immediately removed from summary, and we show child as its content is inflated.
         assertFalse(mHeadsUpManager.isAlerting(summaryEntry.key));
@@ -199,13 +210,13 @@
 
         // Add second child notification so that summary is no longer suppressed.
         mPendingEntries.put(childEntry2.key, childEntry2);
-        mGroupAlertTransferHelper.onPendingEntryAdded(childEntry2);
+        mAlertTransferListener.onPendingEntryAdded(childEntry2);
         mGroupManager.onEntryAdded(childEntry2);
 
         // Child entry finishes its inflation.
         when(childEntry.getRow().isInflationFlagSet(mHeadsUpManager.getContentFlag()))
             .thenReturn(true);
-        mGroupAlertTransferHelper.onInflationFinished(childEntry);
+        mAlertTransferListener.onEntryReinflated(childEntry);
 
         verify(childEntry.getRow(), times(1)).freeContentViewWhenSafe(mHeadsUpManager
             .getContentFlag());
@@ -225,7 +236,7 @@
         mGroupManager.onEntryAdded(summaryEntry);
         mGroupManager.onEntryAdded(childEntry);
 
-        mGroupAlertTransferHelper.cleanUpPendingAlertInfo(childEntry.key);
+        mAlertTransferListener.onEntryRemoved(childEntry);
 
         assertFalse(mGroupAlertTransferHelper.isAlertTransferPending(childEntry));
     }
diff --git a/services/core/java/com/android/server/LocationManagerService.java b/services/core/java/com/android/server/LocationManagerService.java
index 8b992eb..9dbfe5f 100644
--- a/services/core/java/com/android/server/LocationManagerService.java
+++ b/services/core/java/com/android/server/LocationManagerService.java
@@ -22,6 +22,7 @@
 
 import static com.android.internal.util.Preconditions.checkState;
 
+import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
@@ -189,6 +190,8 @@
     private LocationBlacklist mBlacklist;
     private GnssMeasurementsProvider mGnssMeasurementsProvider;
     private GnssNavigationMessageProvider mGnssNavigationMessageProvider;
+    private String mLocationControllerExtraPackage;
+    private boolean mLocationControllerExtraPackageEnabled;
     private IGpsGeofenceHardware mGpsGeofenceProxy;
 
     // --- fields below are protected by mLock ---
@@ -2717,6 +2720,39 @@
         return null;
     }
 
+    @Override
+    public void setLocationControllerExtraPackage(String packageName) {
+        mContext.enforceCallingPermission(Manifest.permission.LOCATION_HARDWARE,
+                Manifest.permission.LOCATION_HARDWARE + " permission required");
+        synchronized (mLock) {
+            mLocationControllerExtraPackage = packageName;
+        }
+    }
+
+    @Override
+    public String getLocationControllerExtraPackage() {
+        synchronized (mLock) {
+            return mLocationControllerExtraPackage;
+        }
+    }
+
+    @Override
+    public void setLocationControllerExtraPackageEnabled(boolean enabled) {
+        mContext.enforceCallingPermission(Manifest.permission.LOCATION_HARDWARE,
+                Manifest.permission.LOCATION_HARDWARE + " permission required");
+        synchronized (mLock) {
+            mLocationControllerExtraPackageEnabled = enabled;
+        }
+    }
+
+    @Override
+    public boolean isLocationControllerExtraPackageEnabled() {
+        synchronized (mLock) {
+            return mLocationControllerExtraPackageEnabled
+                    && (mLocationControllerExtraPackage != null);
+        }
+    }
+
     /**
      * Returns the current location enabled/disabled status for a user
      *
@@ -3492,6 +3528,11 @@
                 }
             }
 
+            if (mLocationControllerExtraPackage != null) {
+                pw.println(" Location controller extra package: " + mLocationControllerExtraPackage
+                        + " enabled: " + mLocationControllerExtraPackageEnabled);
+            }
+
             if (!mBackgroundThrottlePackageWhitelist.isEmpty()) {
                 pw.println("  Throttling Whitelisted Packages:");
                 for (String packageName : mBackgroundThrottlePackageWhitelist) {
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index b04ae17..b6c4921 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -1996,6 +1996,13 @@
                     android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, null);
         }
 
+        if ((events & PhoneStateListener.LISTEN_PREFERRED_DATA_SUBID_CHANGE) != 0) {
+            // It can have either READ_PHONE_STATE or READ_PRIVILEGED_PHONE_STATE.
+            TelephonyPermissions.checkReadPhoneState(mContext,
+                    SubscriptionManager.INVALID_SUBSCRIPTION_ID, Binder.getCallingPid(),
+                    Binder.getCallingUid(), callingPackage, "listen to "
+                            + "LISTEN_PREFERRED_DATA_SUBID_CHANGE");
+        }
 
         return true;
     }
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index 1325f04..10dc156 100644
--- a/services/core/java/com/android/server/job/JobSchedulerService.java
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -67,6 +67,7 @@
 import android.os.Temperature;
 import android.os.UserHandle;
 import android.os.UserManagerInternal;
+import android.os.WorkSource;
 import android.provider.Settings;
 import android.text.format.DateUtils;
 import android.util.KeyValueListParser;
@@ -364,6 +365,8 @@
         private static final String KEY_CONN_CONGESTION_DELAY_FRAC = "conn_congestion_delay_frac";
         private static final String KEY_CONN_PREFETCH_RELAX_FRAC = "conn_prefetch_relax_frac";
         private static final String KEY_USE_HEARTBEATS = "use_heartbeats";
+        private static final String KEY_TIME_CONTROLLER_SKIP_NOT_READY_JOBS =
+                "tc_skip_not_ready_jobs";
         private static final String KEY_QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS =
                 "qc_allowed_time_per_period_ms";
         private static final String KEY_QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS =
@@ -376,6 +379,8 @@
                 "qc_window_size_frequent_ms";
         private static final String KEY_QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS =
                 "qc_window_size_rare_ms";
+        private static final String KEY_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS =
+                "qc_max_execution_time_ms";
 
         private static final int DEFAULT_MIN_IDLE_COUNT = 1;
         private static final int DEFAULT_MIN_CHARGING_COUNT = 1;
@@ -402,6 +407,7 @@
         private static final float DEFAULT_CONN_CONGESTION_DELAY_FRAC = 0.5f;
         private static final float DEFAULT_CONN_PREFETCH_RELAX_FRAC = 0.5f;
         private static final boolean DEFAULT_USE_HEARTBEATS = true;
+        private static final boolean DEFAULT_TIME_CONTROLLER_SKIP_NOT_READY_JOBS = false;
         private static final long DEFAULT_QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS =
                 10 * 60 * 1000L; // 10 minutes
         private static final long DEFAULT_QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS =
@@ -414,6 +420,8 @@
                 8 * 60 * 60 * 1000L; // 8 hours
         private static final long DEFAULT_QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS =
                 24 * 60 * 60 * 1000L; // 24 hours
+        private static final long DEFAULT_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS =
+                4 * 60 * 60 * 1000L; // 4 hours
 
         /**
          * Minimum # of idle jobs that must be ready in order to force the JMS to schedule things
@@ -538,6 +546,13 @@
          */
         public boolean USE_HEARTBEATS = DEFAULT_USE_HEARTBEATS;
 
+        /**
+         * Whether or not TimeController should skip setting wakeup alarms for jobs that aren't
+         * ready now.
+         */
+        public boolean TIME_CONTROLLER_SKIP_NOT_READY_JOBS =
+                DEFAULT_TIME_CONTROLLER_SKIP_NOT_READY_JOBS;
+
         /** How much time each app will have to run jobs within their standby bucket window. */
         public long QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS =
                 DEFAULT_QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS;
@@ -581,6 +596,12 @@
         public long QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS =
                 DEFAULT_QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS;
 
+        /**
+         * The maximum amount of time an app can have its jobs running within a 24 hour window.
+         */
+        public long QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS =
+                DEFAULT_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS;
+
         private final KeyValueListParser mParser = new KeyValueListParser(',');
 
         void updateConstantsLocked(String value) {
@@ -653,6 +674,9 @@
             CONN_PREFETCH_RELAX_FRAC = mParser.getFloat(KEY_CONN_PREFETCH_RELAX_FRAC,
                     DEFAULT_CONN_PREFETCH_RELAX_FRAC);
             USE_HEARTBEATS = mParser.getBoolean(KEY_USE_HEARTBEATS, DEFAULT_USE_HEARTBEATS);
+            TIME_CONTROLLER_SKIP_NOT_READY_JOBS = mParser.getBoolean(
+                    KEY_TIME_CONTROLLER_SKIP_NOT_READY_JOBS,
+                    DEFAULT_TIME_CONTROLLER_SKIP_NOT_READY_JOBS);
             QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS = mParser.getDurationMillis(
                     KEY_QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS,
                     DEFAULT_QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS);
@@ -671,6 +695,9 @@
             QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = mParser.getDurationMillis(
                     KEY_QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS,
                     DEFAULT_QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS);
+            QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = mParser.getDurationMillis(
+                    KEY_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS,
+                    DEFAULT_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS);
         }
 
         void dump(IndentingPrintWriter pw) {
@@ -705,6 +732,8 @@
             pw.printPair(KEY_CONN_CONGESTION_DELAY_FRAC, CONN_CONGESTION_DELAY_FRAC).println();
             pw.printPair(KEY_CONN_PREFETCH_RELAX_FRAC, CONN_PREFETCH_RELAX_FRAC).println();
             pw.printPair(KEY_USE_HEARTBEATS, USE_HEARTBEATS).println();
+            pw.printPair(KEY_TIME_CONTROLLER_SKIP_NOT_READY_JOBS,
+                    TIME_CONTROLLER_SKIP_NOT_READY_JOBS).println();
             pw.printPair(KEY_QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS,
                     QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS).println();
             pw.printPair(KEY_QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS,
@@ -717,6 +746,8 @@
                     QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS).println();
             pw.printPair(KEY_QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS,
                     QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS).println();
+            pw.printPair(KEY_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS,
+                    QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS).println();
             pw.decreaseIndent();
         }
 
@@ -748,6 +779,11 @@
             proto.write(ConstantsProto.CONN_PREFETCH_RELAX_FRAC, CONN_PREFETCH_RELAX_FRAC);
             proto.write(ConstantsProto.USE_HEARTBEATS, USE_HEARTBEATS);
 
+            final long tcToken = proto.start(ConstantsProto.TIME_CONTROLLER);
+            proto.write(ConstantsProto.TimeController.SKIP_NOT_READY_JOBS,
+                    TIME_CONTROLLER_SKIP_NOT_READY_JOBS);
+            proto.end(tcToken);
+
             final long qcToken = proto.start(ConstantsProto.QUOTA_CONTROLLER);
             proto.write(ConstantsProto.QuotaController.ALLOWED_TIME_PER_PERIOD_MS,
                     QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS);
@@ -761,6 +797,8 @@
                     QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS);
             proto.write(ConstantsProto.QuotaController.RARE_WINDOW_SIZE_MS,
                     QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS);
+            proto.write(ConstantsProto.QuotaController.MAX_EXECUTION_TIME_MS,
+                    QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS);
             proto.end(qcToken);
 
             proto.end(token);
@@ -952,9 +990,15 @@
         return mConstants;
     }
 
+    public boolean isChainedAttributionEnabled() {
+        return WorkSource.isChainedBatteryAttributionEnabled(getContext());
+    }
+
     @Override
     public void onStartUser(int userHandle) {
-        mStartedUsers = ArrayUtils.appendInt(mStartedUsers, userHandle);
+        synchronized (mLock) {
+            mStartedUsers = ArrayUtils.appendInt(mStartedUsers, userHandle);
+        }
         // Let's kick any outstanding jobs for this user.
         mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
     }
@@ -967,7 +1011,9 @@
 
     @Override
     public void onStopUser(int userHandle) {
-        mStartedUsers = ArrayUtils.removeInt(mStartedUsers, userHandle);
+        synchronized (mLock) {
+            mStartedUsers = ArrayUtils.removeInt(mStartedUsers, userHandle);
+        }
     }
 
     /**
@@ -2150,8 +2196,7 @@
 
         final boolean jobExists = mJobs.containsJob(job);
 
-        final int userId = job.getUserId();
-        final boolean userStarted = ArrayUtils.contains(mStartedUsers, userId);
+        final boolean userStarted = areUsersStartedLocked(job);
 
         if (DEBUG) {
             Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString()
@@ -2239,7 +2284,7 @@
         try {
             componentPresent = (AppGlobals.getPackageManager().getServiceInfo(
                     job.getServiceComponent(), PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
-                    userId) != null);
+                    job.getUserId()) != null);
         } catch (RemoteException e) {
             throw e.rethrowAsRuntimeException();
         }
@@ -3052,6 +3097,13 @@
                     printed = true;
                     pw.println("user-stopped");
                 }
+                if (!ArrayUtils.contains(mStartedUsers, js.getSourceUserId())) {
+                    if (printed) {
+                        pw.print(" ");
+                    }
+                    printed = true;
+                    pw.println("source-user-stopped");
+                }
                 if (mBackingUpUids.indexOfKey(js.getSourceUid()) >= 0) {
                     if (printed) {
                         pw.print(" ");
@@ -3203,7 +3255,7 @@
                     pw.print(" (job=");
                     pw.print(job.isReady());
                     pw.print(" user=");
-                    pw.print(ArrayUtils.contains(mStartedUsers, job.getUserId()));
+                    pw.print(areUsersStartedLocked(job));
                     pw.print(" !pending=");
                     pw.print(!mPendingJobs.contains(job));
                     pw.print(" !active=");
@@ -3373,7 +3425,7 @@
                     proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_READY,
                             job.isReady());
                     proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_USER_STARTED,
-                            ArrayUtils.contains(mStartedUsers, job.getUserId()));
+                            areUsersStartedLocked(job));
                     proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_PENDING,
                             mPendingJobs.contains(job));
                     proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_CURRENTLY_ACTIVE,
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 58ee217..ac2dbdf 100644
--- a/services/core/java/com/android/server/job/controllers/QuotaController.java
+++ b/services/core/java/com/android/server/job/controllers/QuotaController.java
@@ -54,14 +54,13 @@
 import com.android.server.job.StateControllerProto;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
 /**
- * Controller that tracks whether a package has exceeded its standby bucket quota.
+ * 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,
@@ -203,6 +202,80 @@
         }
     }
 
+    private static int hashLong(long val) {
+        return (int) (val ^ (val >>> 32));
+    }
+
+    @VisibleForTesting
+    static class ExecutionStats {
+        /**
+         * The time at which this record should be considered invalid, in the elapsed realtime
+         * timebase.
+         */
+        public long invalidTimeElapsed;
+
+        public long windowSizeMs;
+
+        /** The total amount of time the app ran in its respective bucket window size. */
+        public long executionTimeInWindowMs;
+        public int bgJobCountInWindow;
+
+        /** The total amount of time the app ran in the last {@link MAX_PERIOD_MS}. */
+        public long executionTimeInMaxPeriodMs;
+        public int bgJobCountInMaxPeriod;
+
+        /**
+         * 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}.
+         */
+        public long quotaCutoffTimeElapsed;
+
+        @Override
+        public String toString() {
+            return new StringBuilder()
+                    .append("invalidTime=").append(invalidTimeElapsed).append(", ")
+                    .append("windowSize=").append(windowSizeMs).append(", ")
+                    .append("executionTimeInWindow=").append(executionTimeInWindowMs).append(", ")
+                    .append("bgJobCountInWindow=").append(bgJobCountInWindow).append(", ")
+                    .append("executionTimeInMaxPeriod=").append(executionTimeInMaxPeriodMs)
+                    .append(", ")
+                    .append("bgJobCountInMaxPeriod=").append(bgJobCountInMaxPeriod).append(", ")
+                    .append("quotaCutoffTime=").append(quotaCutoffTimeElapsed)
+                    .toString();
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj instanceof ExecutionStats) {
+                ExecutionStats other = (ExecutionStats) obj;
+                return this.invalidTimeElapsed == other.invalidTimeElapsed
+                        && this.windowSizeMs == other.windowSizeMs
+                        && this.executionTimeInWindowMs == other.executionTimeInWindowMs
+                        && this.bgJobCountInWindow == other.bgJobCountInWindow
+                        && this.executionTimeInMaxPeriodMs == other.executionTimeInMaxPeriodMs
+                        && this.bgJobCountInMaxPeriod == other.bgJobCountInMaxPeriod
+                        && this.quotaCutoffTimeElapsed == other.quotaCutoffTimeElapsed;
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public int hashCode() {
+            int result = 0;
+            result = 31 * result + hashLong(invalidTimeElapsed);
+            result = 31 * result + hashLong(windowSizeMs);
+            result = 31 * result + hashLong(executionTimeInWindowMs);
+            result = 31 * result + bgJobCountInWindow;
+            result = 31 * result + hashLong(executionTimeInMaxPeriodMs);
+            result = 31 * result + bgJobCountInMaxPeriod;
+            result = 31 * result + hashLong(quotaCutoffTimeElapsed);
+            return result;
+        }
+    }
+
     /** List of all tracked jobs keyed by source package-userId combo. */
     private final UserPackageMap<ArraySet<JobStatus>> mTrackedJobs = new UserPackageMap<>();
 
@@ -218,6 +291,9 @@
      */
     private final UserPackageMap<QcAlarmListener> mInQuotaAlarmListeners = new UserPackageMap<>();
 
+    /** Cached calculation results for each app, with the standby buckets as the array indices. */
+    private final UserPackageMap<ExecutionStats[]> mExecutionStatsCache = new UserPackageMap<>();
+
     private final AlarmManager mAlarmManager;
     private final ChargingTracker mChargeTracker;
     private final Handler mHandler;
@@ -235,11 +311,29 @@
     private long mAllowedTimePerPeriodMs = 10 * MINUTE_IN_MILLIS;
 
     /**
-     * How much time the package should have before transitioning from out-of-quota to in-quota.
-     * This should not affect processing if the package is already in-quota.
+     * 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;
+
+    /**
+     * 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
 
+    /**
+     * {@link mAllowedTimePerPeriodMs} - {@link mQuotaBufferMs}. This can be used to determine when
+     * an app will have enough quota to transition from out-of-quota to in-quota.
+     */
+    private long mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs;
+
+    /**
+     * {@link mMaxExecutionTimeMs} - {@link mQuotaBufferMs}. This can be used to determine when an
+     * app will have enough quota to transition from out-of-quota to in-quota.
+     */
+    private long mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
+
     private long mNextCleanupTimeElapsed = 0;
     private final AlarmManager.OnAlarmListener mSessionCleanupAlarmListener =
             new AlarmManager.OnAlarmListener() {
@@ -263,7 +357,7 @@
     /** The maximum period any bucket can have. */
     private static final long MAX_PERIOD_MS = 24 * 60 * MINUTE_IN_MILLIS;
 
-    /** A package has reached its quota. The message should contain a {@link Package} object. */
+    /** An app has reached its quota. The message should contain a {@link Package} object. */
     private static final int MSG_REACHED_QUOTA = 0;
     /** Drop any old timing sessions. */
     private static final int MSG_CLEAN_UP_SESSIONS = 1;
@@ -341,12 +435,15 @@
                 Math.max(MINUTE_IN_MILLIS, mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS));
         if (mAllowedTimePerPeriodMs != newAllowedTimeMs) {
             mAllowedTimePerPeriodMs = newAllowedTimeMs;
+            mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs;
             changed = true;
         }
         long newQuotaBufferMs = Math.max(0,
                 Math.min(5 * MINUTE_IN_MILLIS, mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS));
         if (mQuotaBufferMs != newQuotaBufferMs) {
             mQuotaBufferMs = newQuotaBufferMs;
+            mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs;
+            mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
             changed = true;
         }
         long newActivePeriodMs = Math.max(mAllowedTimePerPeriodMs,
@@ -373,6 +470,13 @@
             mBucketPeriodsMs[RARE_INDEX] = newRarePeriodMs;
             changed = true;
         }
+        long newMaxExecutionTimeMs = Math.max(60 * MINUTE_IN_MILLIS,
+                Math.min(MAX_PERIOD_MS, mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS));
+        if (mMaxExecutionTimeMs != newMaxExecutionTimeMs) {
+            mMaxExecutionTimeMs = newMaxExecutionTimeMs;
+            mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
+            changed = true;
+        }
 
         if (changed) {
             // Update job bookkeeping out of band.
@@ -406,6 +510,7 @@
             mAlarmManager.cancel(alarmListener);
             mInQuotaAlarmListeners.delete(userId, packageName);
         }
+        mExecutionStatsCache.delete(userId, packageName);
     }
 
     @Override
@@ -414,6 +519,7 @@
         mPkgTimers.delete(userId);
         mTimingSessions.delete(userId);
         mInQuotaAlarmListeners.delete(userId);
+        mExecutionStatsCache.delete(userId);
     }
 
     /**
@@ -439,7 +545,6 @@
     private boolean isWithinQuotaLocked(final int userId, @NonNull final String packageName,
             final int standbyBucket) {
         if (standbyBucket == NEVER_INDEX) return false;
-        if (standbyBucket == ACTIVE_INDEX) return true;
         // This check is needed in case the flag is toggled after a job has been registered.
         if (!mShouldThrottle) return true;
 
@@ -472,46 +577,152 @@
         if (standbyBucket == NEVER_INDEX) {
             return 0;
         }
-        final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket];
-        final long trailingRunDurationMs = getTrailingExecutionTimeLocked(
-                userId, packageName, bucketWindowSizeMs);
-        return mAllowedTimePerPeriodMs - trailingRunDurationMs;
+        final ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
+        return Math.min(mAllowedTimePerPeriodMs - stats.executionTimeInWindowMs,
+                mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs);
     }
 
-    /** Returns how long the uid has had jobs running within the most recent window. */
+    /** Returns the execution stats of the app in the most recent window. */
     @VisibleForTesting
-    long getTrailingExecutionTimeLocked(final int userId, @NonNull final String packageName,
-            final long windowSizeMs) {
-        long totalTime = 0;
+    @NonNull
+    ExecutionStats getExecutionStatsLocked(final int userId, @NonNull final String packageName,
+            final int standbyBucket) {
+        if (standbyBucket == NEVER_INDEX) {
+            Slog.wtf(TAG, "getExecutionStatsLocked called for a NEVER app.");
+            return new ExecutionStats();
+        }
+        ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName);
+        if (appStats == null) {
+            appStats = new ExecutionStats[mBucketPeriodsMs.length];
+            mExecutionStatsCache.add(userId, packageName, appStats);
+        }
+        ExecutionStats stats = appStats[standbyBucket];
+        if (stats == null) {
+            stats = new ExecutionStats();
+            appStats[standbyBucket] = stats;
+        }
+        final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket];
+        Timer timer = mPkgTimers.get(userId, packageName);
+        if ((timer != null && timer.isActive())
+                || stats.invalidTimeElapsed <= sElapsedRealtimeClock.millis()
+                || stats.windowSizeMs != bucketWindowSizeMs) {
+            // The stats are no longer valid.
+            stats.windowSizeMs = bucketWindowSizeMs;
+            updateExecutionStatsLocked(userId, packageName, stats);
+        }
+
+        return stats;
+    }
+
+    @VisibleForTesting
+    void updateExecutionStatsLocked(final int userId, @NonNull final String packageName,
+            @NonNull ExecutionStats stats) {
+        stats.executionTimeInWindowMs = 0;
+        stats.bgJobCountInWindow = 0;
+        stats.executionTimeInMaxPeriodMs = 0;
+        stats.bgJobCountInMaxPeriod = 0;
+        stats.quotaCutoffTimeElapsed = 0;
 
         Timer timer = mPkgTimers.get(userId, packageName);
         final long nowElapsed = sElapsedRealtimeClock.millis();
+        stats.invalidTimeElapsed = nowElapsed + MAX_PERIOD_MS;
         if (timer != null && timer.isActive()) {
-            totalTime = timer.getCurrentDuration(nowElapsed);
+            stats.executionTimeInWindowMs =
+                    stats.executionTimeInMaxPeriodMs = timer.getCurrentDuration(nowElapsed);
+            stats.bgJobCountInWindow = stats.bgJobCountInMaxPeriod = timer.getBgJobCount();
+            // If the timer is active, the value will be stale at the next method call, so
+            // invalidate now.
+            stats.invalidTimeElapsed = nowElapsed;
+            if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) {
+                stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
+                        nowElapsed - mAllowedTimeIntoQuotaMs);
+            }
+            if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
+                stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
+                        nowElapsed - mMaxExecutionTimeIntoQuotaMs);
+            }
         }
 
         List<TimingSession> sessions = mTimingSessions.get(userId, packageName);
         if (sessions == null || sessions.size() == 0) {
-            return totalTime;
+            return;
         }
 
-        final long startElapsed = nowElapsed - windowSizeMs;
+        final long startWindowElapsed = nowElapsed - stats.windowSizeMs;
+        final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS;
+        // The minimum time between the start time and the beginning of the sessions that were
+        // looked at --> how much time the stats will be valid for.
+        long emptyTimeMs = Long.MAX_VALUE;
         // Sessions are non-overlapping and in order of occurrence, so iterating backwards will get
         // the most recent ones.
         for (int i = sessions.size() - 1; i >= 0; --i) {
             TimingSession session = sessions.get(i);
-            if (startElapsed < session.startTimeElapsed) {
-                totalTime += session.endTimeElapsed - session.startTimeElapsed;
-            } else if (startElapsed < session.endTimeElapsed) {
+
+            // Window management.
+            if (startWindowElapsed < session.startTimeElapsed) {
+                stats.executionTimeInWindowMs += session.endTimeElapsed - session.startTimeElapsed;
+                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);
+                }
+            } 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.
-                totalTime += session.endTimeElapsed - startElapsed;
+                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);
+                }
+            }
+
+            // Max period check.
+            if (startMaxElapsed < session.startTimeElapsed) {
+                stats.executionTimeInMaxPeriodMs +=
+                        session.endTimeElapsed - session.startTimeElapsed;
+                stats.bgJobCountInMaxPeriod += session.bgJobCount;
+                emptyTimeMs = Math.min(emptyTimeMs, session.startTimeElapsed - startMaxElapsed);
+                if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
+                    stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
+                            session.startTimeElapsed + stats.executionTimeInMaxPeriodMs
+                                    - mMaxExecutionTimeIntoQuotaMs);
+                }
+            } else if (startMaxElapsed < session.endTimeElapsed) {
+                // The session started before the window but ended within the window. Only include
+                // the portion that was within the window.
+                stats.executionTimeInMaxPeriodMs += session.endTimeElapsed - startMaxElapsed;
+                stats.bgJobCountInMaxPeriod += session.bgJobCount;
+                emptyTimeMs = 0;
+                if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
+                    stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
+                            startMaxElapsed + stats.executionTimeInMaxPeriodMs
+                                    - mMaxExecutionTimeIntoQuotaMs);
+                }
             } else {
                 // This session ended before the window. No point in going any further.
-                return totalTime;
+                break;
             }
         }
-        return totalTime;
+        stats.invalidTimeElapsed = nowElapsed + emptyTimeMs;
+    }
+
+    private void invalidateAllExecutionStatsLocked(final int userId,
+            @NonNull final String packageName) {
+        ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName);
+        if (appStats != null) {
+            final long nowElapsed = sElapsedRealtimeClock.millis();
+            for (int i = 0; i < appStats.length; ++i) {
+                ExecutionStats stats = appStats[i];
+                if (stats != null) {
+                    stats.invalidTimeElapsed = nowElapsed;
+                }
+            }
+        }
     }
 
     @VisibleForTesting
@@ -524,6 +735,8 @@
                 mTimingSessions.add(userId, packageName, sessions);
             }
             sessions.add(session);
+            // Adding a new session means that the current stats are now incorrect.
+            invalidateAllExecutionStatsLocked(userId, packageName);
 
             maybeScheduleCleanupAlarmLocked();
         }
@@ -657,87 +870,43 @@
     @VisibleForTesting
     void maybeScheduleStartAlarmLocked(final int userId, @NonNull final String packageName,
             final int standbyBucket) {
-        final String pkgString = string(userId, packageName);
         if (standbyBucket == NEVER_INDEX) {
             return;
-        } else if (standbyBucket == ACTIVE_INDEX) {
-            // ACTIVE apps are "always" in quota.
-            if (DEBUG) {
-                Slog.w(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString
-                        + " even though it is active");
-            }
-            mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
+        }
 
-            QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
+        final String pkgString = string(userId, packageName);
+        ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
+        QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
+        if (stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs
+                && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs) {
+            // Already in quota. Why was this method called?
+            if (DEBUG) {
+                Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString
+                        + " even though it already has "
+                        + getRemainingExecutionTimeLocked(userId, packageName, standbyBucket)
+                        + "ms in its quota.");
+            }
             if (alarmListener != null) {
                 // Cancel any pending alarm.
                 mAlarmManager.cancel(alarmListener);
                 // Set the trigger time to 0 so that the alarm doesn't think it's still waiting.
                 alarmListener.setTriggerTime(0);
             }
-            return;
-        }
-
-        List<TimingSession> sessions = mTimingSessions.get(userId, packageName);
-        if (sessions == null || sessions.size() == 0) {
-            // If there are no sessions, then the job is probably in quota.
-            if (DEBUG) {
-                Slog.wtf(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString
-                        + " even though it is likely within its quota.");
-            }
             mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
             return;
         }
-
-        final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket];
-        final long nowElapsed = sElapsedRealtimeClock.millis();
-        // How far back we need to look.
-        final long startElapsed = nowElapsed - bucketWindowSizeMs;
-
-        long totalTime = 0;
-        long cutoffTimeElapsed = nowElapsed;
-        for (int i = sessions.size() - 1; i >= 0; i--) {
-            TimingSession session = sessions.get(i);
-            if (startElapsed < session.startTimeElapsed) {
-                cutoffTimeElapsed = session.startTimeElapsed;
-                totalTime += session.endTimeElapsed - session.startTimeElapsed;
-            } else if (startElapsed < session.endTimeElapsed) {
-                // The session started before the window but ended within the window. Only
-                // include the portion that was within the window.
-                cutoffTimeElapsed = startElapsed;
-                totalTime += session.endTimeElapsed - startElapsed;
-            } else {
-                // This session ended before the window. No point in going any further.
-                break;
-            }
-            if (totalTime >= mAllowedTimePerPeriodMs) {
-                break;
-            }
-        }
-        if (totalTime < mAllowedTimePerPeriodMs) {
-            // Already in quota. Why was this method called?
-            if (DEBUG) {
-                Slog.w(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString
-                        + " even though it already has " + (mAllowedTimePerPeriodMs - totalTime)
-                        + "ms in its quota.");
-            }
-            mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
-            return;
-        }
-
-        QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
         if (alarmListener == null) {
             alarmListener = new QcAlarmListener(userId, packageName);
             mInQuotaAlarmListeners.add(userId, packageName, alarmListener);
         }
 
-        // We add all the way back to the beginning of a session (or the window) even when we don't
-        // need to (in order to simplify the for loop above), so there might be some extra we
-        // need to add back.
-        final long extraTimeMs = totalTime - mAllowedTimePerPeriodMs;
         // The time this app will have quota again.
-        final long inQuotaTimeElapsed =
-                cutoffTimeElapsed + extraTimeMs + mQuotaBufferMs + bucketWindowSizeMs;
+        long inQuotaTimeElapsed =
+                stats.quotaCutoffTimeElapsed + stats.windowSizeMs;
+        if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeMs) {
+            inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed,
+                    stats.quotaCutoffTimeElapsed + MAX_PERIOD_MS);
+        }
         // Only schedule the alarm if:
         // 1. There isn't one currently scheduled
         // 2. The new alarm is significantly earlier than the previous alarm (which could be the
@@ -747,13 +916,15 @@
         // TODO: this might be overengineering. Simplify if proven safe.
         if (!alarmListener.isWaiting()
                 || inQuotaTimeElapsed < alarmListener.getTriggerTimeElapsed() - 3 * MINUTE_IN_MILLIS
-                || alarmListener.getTriggerTimeElapsed() < inQuotaTimeElapsed - mQuotaBufferMs) {
+                || alarmListener.getTriggerTimeElapsed() < inQuotaTimeElapsed) {
             if (DEBUG) Slog.d(TAG, "Scheduling start alarm for " + pkgString);
             // If the next time this app will have quota is at least 3 minutes before the
             // alarm is supposed to go off, reschedule the alarm.
             mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, inQuotaTimeElapsed,
                     ALARM_TAG_QUOTA_CHECK, alarmListener, mHandler);
             alarmListener.setTriggerTime(inQuotaTimeElapsed);
+        } else if (DEBUG) {
+            Slog.d(TAG, "No need to scheduling start alarm for " + pkgString);
         }
     }
 
@@ -816,10 +987,18 @@
         // How many background jobs ran during this session.
         public final int bgJobCount;
 
-        TimingSession(long startElapsed, long endElapsed, int jobCount) {
+        private final int mHashCode;
+
+        TimingSession(long startElapsed, long endElapsed, int bgJobCount) {
             this.startTimeElapsed = startElapsed;
             this.endTimeElapsed = endElapsed;
-            this.bgJobCount = jobCount;
+            this.bgJobCount = bgJobCount;
+
+            int hashCode = 0;
+            hashCode = 31 * hashCode + hashLong(startTimeElapsed);
+            hashCode = 31 * hashCode + hashLong(endTimeElapsed);
+            hashCode = 31 * hashCode + bgJobCount;
+            mHashCode = hashCode;
         }
 
         @Override
@@ -842,7 +1021,7 @@
 
         @Override
         public int hashCode() {
-            return Arrays.hashCode(new long[] {startTimeElapsed, endTimeElapsed, bgJobCount});
+            return mHashCode;
         }
 
         public void dump(IndentingPrintWriter pw) {
@@ -902,6 +1081,9 @@
                     if (mRunningBgJobs.size() == 1) {
                         // Started tracking the first job.
                         mStartTimeElapsed = sElapsedRealtimeClock.millis();
+                        // Starting the timer means that all cached execution stats are now
+                        // incorrect.
+                        invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName);
                         scheduleCutoff();
                     }
                 }
@@ -966,6 +1148,12 @@
             }
         }
 
+        int getBgJobCount() {
+            synchronized (mLock) {
+                return mBgJobCount;
+            }
+        }
+
         void onChargingChanged(long nowElapsed, boolean isCharging) {
             synchronized (mLock) {
                 if (isCharging) {
@@ -978,6 +1166,9 @@
                         // repeatedly plugged in and unplugged, the job count for a package may be
                         // artificially high.
                         mBgJobCount = mRunningBgJobs.size();
+                        // Starting the timer means that all cached execution stats are now
+                        // incorrect.
+                        invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName);
                         // Schedule cutoff since we're now actively tracking for quotas again.
                         scheduleCutoff();
                     }
@@ -1239,6 +1430,11 @@
     }
 
     @VisibleForTesting
+    long getMaxExecutionTimeMs() {
+        return mMaxExecutionTimeMs;
+    }
+
+    @VisibleForTesting
     @Nullable
     List<TimingSession> getTimingSessions(int userId, String packageName) {
         return mTimingSessions.get(userId, packageName);
diff --git a/services/core/java/com/android/server/job/controllers/TimeController.java b/services/core/java/com/android/server/job/controllers/TimeController.java
index 04d5795..26f3caf 100644
--- a/services/core/java/com/android/server/job/controllers/TimeController.java
+++ b/services/core/java/com/android/server/job/controllers/TimeController.java
@@ -30,6 +30,7 @@
 import android.util.TimeUtils;
 import android.util.proto.ProtoOutputStream;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.job.JobSchedulerService;
 import com.android.server.job.StateControllerProto;
@@ -68,7 +69,7 @@
 
         mNextJobExpiredElapsedMillis = Long.MAX_VALUE;
         mNextDelayExpiredElapsedMillis = Long.MAX_VALUE;
-        mChainedAttributionEnabled = WorkSource.isChainedBatteryAttributionEnabled(mContext);
+        mChainedAttributionEnabled = mService.isChainedAttributionEnabled();
     }
 
     /**
@@ -110,11 +111,24 @@
                 it.next();
             }
             it.add(job);
+
             job.setTrackingController(JobStatus.TRACKING_TIME);
-            maybeUpdateAlarmsLocked(
-                    job.hasTimingDelayConstraint() ? job.getEarliestRunTime() : Long.MAX_VALUE,
-                    job.hasDeadlineConstraint() ? job.getLatestRunTimeElapsed() : Long.MAX_VALUE,
-                    deriveWorkSource(job.getSourceUid(), job.getSourcePackageName()));
+            WorkSource ws = deriveWorkSource(job.getSourceUid(), job.getSourcePackageName());
+            final long deadlineExpiredElapsed =
+                    job.hasDeadlineConstraint() ? job.getLatestRunTimeElapsed() : Long.MAX_VALUE;
+            final long delayExpiredElapsed =
+                    job.hasTimingDelayConstraint() ? job.getEarliestRunTime() : Long.MAX_VALUE;
+            if (mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS) {
+                if (wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_TIMING_DELAY)) {
+                    maybeUpdateDelayAlarmLocked(delayExpiredElapsed, ws);
+                }
+                if (wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_DEADLINE)) {
+                    maybeUpdateDeadlineAlarmLocked(deadlineExpiredElapsed, ws);
+                }
+            } else {
+                maybeUpdateDelayAlarmLocked(delayExpiredElapsed, ws);
+                maybeUpdateDeadlineAlarmLocked(deadlineExpiredElapsed, ws);
+            }
         }
     }
 
@@ -133,6 +147,34 @@
         }
     }
 
+    @Override
+    public void onConstantsUpdatedLocked() {
+        checkExpiredDelaysAndResetAlarm();
+        checkExpiredDeadlinesAndResetAlarm();
+    }
+
+    @Override
+    public void evaluateStateLocked(JobStatus job) {
+        if (!mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS) {
+            return;
+        }
+
+        if (job.hasTimingDelayConstraint()
+                && job.getEarliestRunTime() <= mNextDelayExpiredElapsedMillis) {
+            checkExpiredDelaysAndResetAlarm();
+        }
+        if (job.hasDeadlineConstraint()
+                && job.getLatestRunTimeElapsed() <= mNextJobExpiredElapsedMillis) {
+            checkExpiredDeadlinesAndResetAlarm();
+        }
+    }
+
+    @Override
+    public void reevaluateStateLocked(int uid) {
+        checkExpiredDelaysAndResetAlarm();
+        checkExpiredDeadlinesAndResetAlarm();
+    }
+
     /**
      * Determines whether this controller can stop tracking the given job.
      * The controller is no longer interested in a job once its time constraint is satisfied, and
@@ -156,14 +198,15 @@
      * Checks list of jobs for ones that have an expired deadline, sending them to the JobScheduler
      * if so, removing them from this list, and updating the alarm for the next expiry time.
      */
-    private void checkExpiredDeadlinesAndResetAlarm() {
+    @VisibleForTesting
+    void checkExpiredDeadlinesAndResetAlarm() {
         synchronized (mLock) {
             long nextExpiryTime = Long.MAX_VALUE;
             int nextExpiryUid = 0;
             String nextExpiryPackageName = null;
             final long nowElapsedMillis = sElapsedRealtimeClock.millis();
 
-            Iterator<JobStatus> it = mTrackedJobs.iterator();
+            ListIterator<JobStatus> it = mTrackedJobs.listIterator();
             while (it.hasNext()) {
                 JobStatus job = it.next();
                 if (!job.hasDeadlineConstraint()) {
@@ -171,9 +214,22 @@
                 }
 
                 if (evaluateDeadlineConstraint(job, nowElapsedMillis)) {
-                    mStateChangedListener.onRunJobNow(job);
+                    if (job.isReady()) {
+                        // If the job still isn't ready, there's no point trying to rush the
+                        // Scheduler.
+                        mStateChangedListener.onRunJobNow(job);
+                    }
                     it.remove();
                 } else {  // Sorted by expiry time, so take the next one and stop.
+                    if (mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS
+                            && !wouldBeReadyWithConstraintLocked(
+                            job, JobStatus.CONSTRAINT_DEADLINE)) {
+                        if (DEBUG) {
+                            Slog.i(TAG,
+                                    "Skipping " + job + " because deadline won't make it ready.");
+                        }
+                        continue;
+                    }
                     nextExpiryTime = job.getLatestRunTimeElapsed();
                     nextExpiryUid = job.getSourceUid();
                     nextExpiryPackageName = job.getSourcePackageName();
@@ -202,7 +258,8 @@
      * Handles alarm that notifies us that a job's delay has expired. Iterates through the list of
      * tracked jobs and marks them as ready as appropriate.
      */
-    private void checkExpiredDelaysAndResetAlarm() {
+    @VisibleForTesting
+    void checkExpiredDelaysAndResetAlarm() {
         synchronized (mLock) {
             final long nowElapsedMillis = sElapsedRealtimeClock.millis();
             long nextDelayTime = Long.MAX_VALUE;
@@ -223,6 +280,15 @@
                         ready = true;
                     }
                 } else if (!job.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY)) {
+                    if (mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS
+                            && !wouldBeReadyWithConstraintLocked(
+                            job, JobStatus.CONSTRAINT_TIMING_DELAY)) {
+                        if (DEBUG) {
+                            Slog.i(TAG,
+                                    "Skipping " + job + " because delay won't make it ready.");
+                        }
+                        continue;
+                    }
                     // If this job still doesn't have its delay constraint satisfied,
                     // then see if it is the next upcoming delay time for the alarm.
                     final long jobDelayTime = job.getEarliestRunTime();
@@ -262,11 +328,13 @@
         return false;
     }
 
-    private void maybeUpdateAlarmsLocked(long delayExpiredElapsed, long deadlineExpiredElapsed,
-            WorkSource ws) {
+    private void maybeUpdateDelayAlarmLocked(long delayExpiredElapsed, WorkSource ws) {
         if (delayExpiredElapsed < mNextDelayExpiredElapsedMillis) {
             setDelayExpiredAlarmLocked(delayExpiredElapsed, ws);
         }
+    }
+
+    private void maybeUpdateDeadlineAlarmLocked(long deadlineExpiredElapsed, WorkSource ws) {
         if (deadlineExpiredElapsed < mNextJobExpiredElapsedMillis) {
             setDeadlineExpiredAlarmLocked(deadlineExpiredElapsed, ws);
         }
@@ -297,11 +365,7 @@
     }
 
     private long maybeAdjustAlarmTime(long proposedAlarmTimeElapsedMillis) {
-        final long earliestWakeupTimeElapsed = sElapsedRealtimeClock.millis();
-        if (proposedAlarmTimeElapsedMillis < earliestWakeupTimeElapsed) {
-            return earliestWakeupTimeElapsed;
-        }
-        return proposedAlarmTimeElapsedMillis;
+        return Math.max(proposedAlarmTimeElapsedMillis, sElapsedRealtimeClock.millis());
     }
 
     private void updateAlarmWithListenerLocked(String tag, OnAlarmListener listener,
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 6cfb846..ae29180 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -1064,9 +1064,6 @@
                         + verificationId + " packageName:" + packageName);
                 return;
             }
-            if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG,
-                    "Updating IntentFilterVerificationInfo for package " + packageName
-                            +" verificationId:" + verificationId);
 
             synchronized (mPackages) {
                 if (verified) {
@@ -1084,19 +1081,47 @@
                     int updatedStatus = INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED;
                     boolean needUpdate = false;
 
-                    // We cannot override the STATUS_ALWAYS / STATUS_NEVER states if they have
-                    // already been set by the User thru the Disambiguation dialog
+                    if (DEBUG_DOMAIN_VERIFICATION) {
+                        Slog.d(TAG,
+                                "Updating IntentFilterVerificationInfo for package " + packageName
+                                + " verificationId:" + verificationId
+                                + " verified=" + verified);
+                    }
+
+                    // In a success case, we promote from undefined or ASK to ALWAYS.  This
+                    // supports a flow where the app fails validation but then ships an updated
+                    // APK that passes, and therefore deserves to be in ALWAYS.
+                    //
+                    // If validation failed, the undefined state winds up in the basic ASK behavior,
+                    // but apps that previously passed and became ALWAYS are *demoted* out of
+                    // that state, since they would not deserve the ALWAYS behavior in case of a
+                    // clean install.
                     switch (userStatus) {
+                        case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS:
+                            if (!verified) {
+                                // updatedStatus is already UNDEFINED
+                                needUpdate = true;
+
+                                if (DEBUG_DOMAIN_VERIFICATION) {
+                                    Slog.d(TAG, "Formerly validated but now failing; demoting");
+                                }
+                            }
+                            break;
+
                         case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED:
+                            // Stay in 'undefined' on verification failure
                             if (verified) {
                                 updatedStatus = INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS;
-                            } else {
-                                updatedStatus = INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ASK;
                             }
                             needUpdate = true;
+                            if (DEBUG_DOMAIN_VERIFICATION) {
+                                Slog.d(TAG, "Applying update; old=" + userStatus
+                                        + " new=" + updatedStatus);
+                            }
                             break;
 
                         case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ASK:
+                            // Keep in 'ask' on failure
                             if (verified) {
                                 updatedStatus = INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS;
                                 needUpdate = true;
@@ -1112,6 +1137,8 @@
                                 packageName, updatedStatus, userId);
                         scheduleWritePackageRestrictionsLocked(userId);
                     }
+                } else {
+                    Slog.i(TAG, "autoVerify ignored when installing for all users");
                 }
             }
         }
@@ -15269,7 +15296,7 @@
                         | (killApp ? 0 : PackageManager.DELETE_DONT_KILL_APP);
                 deletePackageAction = mayDeletePackageLocked(res.removedInfo,
                         prepareResult.originalPs, prepareResult.disabledPs,
-                        prepareResult.childPackageSettings, deleteFlags, installArgs.user);
+                        prepareResult.childPackageSettings, deleteFlags, null /* all users */);
                 if (deletePackageAction == null) {
                     throw new ReconcileFailure(
                             PackageManager.INSTALL_FAILED_REPLACE_COULDNT_DELETE,
@@ -16708,6 +16735,7 @@
         int status = ivi.getStatus();
         switch (status) {
             case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED:
+            case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS:
             case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ASK:
                 return true;
 
@@ -17356,28 +17384,22 @@
      * make sure this flag is set for partially installed apps. If not its meaningless to
      * delete a partially installed application.
      */
-    private void removePackageDataLIF(PackageSetting ps, int[] allUserHandles,
+    private void removePackageDataLIF(final PackageSetting deletedPs, int[] allUserHandles,
             PackageRemovedInfo outInfo, int flags, boolean writeSettings) {
-        String packageName = ps.name;
-        if (DEBUG_REMOVE) Slog.d(TAG, "removePackageDataLI: " + ps);
+        String packageName = deletedPs.name;
+        if (DEBUG_REMOVE) Slog.d(TAG, "removePackageDataLI: " + deletedPs);
         // Retrieve object to delete permissions for shared user later on
-        final PackageParser.Package deletedPkg;
-        final PackageSetting deletedPs;
-        // reader
-        synchronized (mPackages) {
-            deletedPkg = mPackages.get(packageName);
-            deletedPs = mSettings.mPackages.get(packageName);
-            if (outInfo != null) {
-                outInfo.removedPackage = packageName;
-                outInfo.installerPackageName = ps.installerPackageName;
-                outInfo.isStaticSharedLib = deletedPkg != null
-                        && deletedPkg.staticSharedLibName != null;
-                outInfo.populateUsers(deletedPs == null ? null
-                        : deletedPs.queryInstalledUsers(sUserManager.getUserIds(), true), deletedPs);
-            }
+        final PackageParser.Package deletedPkg = deletedPs.pkg;
+        if (outInfo != null) {
+            outInfo.removedPackage = packageName;
+            outInfo.installerPackageName = deletedPs.installerPackageName;
+            outInfo.isStaticSharedLib = deletedPkg != null
+                    && deletedPkg.staticSharedLibName != null;
+            outInfo.populateUsers(deletedPs == null ? null
+                    : deletedPs.queryInstalledUsers(sUserManager.getUserIds(), true), deletedPs);
         }
 
-        removePackageLI(ps.name, (flags & PackageManager.DELETE_CHATTY) != 0);
+        removePackageLI(deletedPs.name, (flags & PackageManager.DELETE_CHATTY) != 0);
 
         if ((flags & PackageManager.DELETE_KEEP_DATA) == 0) {
             final PackageParser.Package resolvedPkg;
@@ -17386,8 +17408,8 @@
             } else {
                 // We don't have a parsed package when it lives on an ejected
                 // adopted storage device, so fake something together
-                resolvedPkg = new PackageParser.Package(ps.name);
-                resolvedPkg.setVolumeUuid(ps.volumeUuid);
+                resolvedPkg = new PackageParser.Package(deletedPs.name);
+                resolvedPkg.setVolumeUuid(deletedPs.volumeUuid);
             }
             destroyAppDataLIF(resolvedPkg, UserHandle.USER_ALL,
                     StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE);
@@ -17447,10 +17469,10 @@
                         if (DEBUG_REMOVE) {
                             Slog.d(TAG, "    user " + userId + " => " + installed);
                         }
-                        if (installed != ps.getInstalled(userId)) {
+                        if (installed != deletedPs.getInstalled(userId)) {
                             installedStateChanged = true;
                         }
-                        ps.setInstalled(installed, userId);
+                        deletedPs.setInstalled(installed, userId);
                     }
                 }
             }
@@ -17460,7 +17482,7 @@
                 mSettings.writeLPr();
             }
             if (installedStateChanged) {
-                mSettings.writeKernelMappingLPr(ps);
+                mSettings.writeKernelMappingLPr(deletedPs);
             }
         }
         if (removedAppId != -1) {
@@ -17530,12 +17552,12 @@
     /*
      * Tries to delete system package.
      */
-    private void deleteSystemPackageLIF(DeletePackageAction action,
-            PackageParser.Package deletedPkg, PackageSetting deletedPs, int[] allUserHandles,
-            int flags, PackageRemovedInfo outInfo, boolean writeSettings)
+    private void deleteSystemPackageLIF(DeletePackageAction action, PackageSetting deletedPs,
+            int[] allUserHandles, int flags, PackageRemovedInfo outInfo, boolean writeSettings)
             throws SystemDeleteException {
         final boolean applyUserRestrictions
                 = (allUserHandles != null) && (outInfo.origUsers != null);
+        final PackageParser.Package deletedPkg = deletedPs.pkg;
         // Confirm if the system package has been updated
         // An updated system app can be deleted. This will also have to restore
         // the system pkg from system partition
@@ -17895,16 +17917,18 @@
             PackageSetting[] children = mSettings.getChildSettingsLPr(ps);
             action = mayDeletePackageLocked(outInfo, ps, disabledPs, children, flags, user);
         }
+        if (DEBUG_REMOVE) Slog.d(TAG, "deletePackageLI: " + packageName + " user " + user);
         if (null == action) {
+            if (DEBUG_REMOVE) Slog.d(TAG, "deletePackageLI: action was null");
             return false;
         }
 
-        if (DEBUG_REMOVE) Slog.d(TAG, "deletePackageLI: " + packageName + " user " + user);
 
         try {
             executeDeletePackageLIF(action, packageName, deleteCodeAndResources,
                     allUserHandles, writeSettings, replacingPackage);
         } catch (SystemDeleteException e) {
+            if (DEBUG_REMOVE) Slog.d(TAG, "deletePackageLI: system deletion failure", e);
             return false;
         }
         return true;
@@ -17951,42 +17975,48 @@
             unsuspendForSuspendingPackage(packageName, userId);
         }
 
-
         if (!systemApp || action.mayDeleteUnupdatedSystemApp) {
             // The caller is asking that the package only be deleted for a single
             // user.  To do this, we just mark its uninstalled state and delete
             // its data. If this is a system app, we only allow this to happen if
             // they have set the special DELETE_SYSTEM_APP which requests different
             // semantics than normal for uninstalling system apps.
-            markPackageUninstalledForUserLPw(ps, user);
-
-            if (!systemApp) {
-                // Do not uninstall the APK if an app should be cached
-                boolean keepUninstalledPackage = shouldKeepUninstalledPackageLPr(packageName);
-                if (ps.isAnyInstalled(sUserManager.getUserIds()) || keepUninstalledPackage) {
-                    // Other user still have this package installed, so all
+            synchronized (mPackages) {
+                markPackageUninstalledForUserLPw(ps, user);
+                if (!systemApp) {
+                    // Do not uninstall the APK if an app should be cached
+                    boolean keepUninstalledPackage = shouldKeepUninstalledPackageLPr(packageName);
+                    if (ps.isAnyInstalled(sUserManager.getUserIds()) || keepUninstalledPackage) {
+                        // Other users still have this package installed, so all
+                        // we need to do is clear this user's data and save that
+                        // it is uninstalled.
+                        if (DEBUG_REMOVE) Slog.d(TAG, "Still installed by other users");
+                        clearPackageStateForUserLIF(ps, userId, outInfo, flags);
+                        scheduleWritePackageRestrictionsLocked(user);
+                        return;
+                    } else {
+                        // We need to set it back to 'installed' so the uninstall
+                        // broadcasts will be sent correctly.
+                        if (DEBUG_REMOVE) Slog.d(TAG, "Not installed by other users, full delete");
+                        if (userId != UserHandle.USER_ALL) {
+                            ps.setInstalled(true, userId);
+                        } else {
+                            for (int origUserId : outInfo.origUsers) {
+                                ps.setInstalled(true, origUserId);
+                            }
+                        }
+                        mSettings.writeKernelMappingLPr(ps);
+                    }
+                } else {
+                    // This is a system app, so we assume that the
+                    // other users still have this package installed, so all
                     // we need to do is clear this user's data and save that
                     // it is uninstalled.
-                    if (DEBUG_REMOVE) Slog.d(TAG, "Still installed by other users");
-                    clearPackageStateForUserLIF(ps, user.getIdentifier(), outInfo, flags);
+                    if (DEBUG_REMOVE) Slog.d(TAG, "Deleting system app");
+                    clearPackageStateForUserLIF(ps, userId, outInfo, flags);
                     scheduleWritePackageRestrictionsLocked(user);
                     return;
-                } else {
-                    // We need to set it back to 'installed' so the uninstall
-                    // broadcasts will be sent correctly.
-                    if (DEBUG_REMOVE) Slog.d(TAG, "Not installed by other users, full delete");
-                    ps.setInstalled(true, user.getIdentifier());
-                    mSettings.writeKernelMappingLPr(ps);
                 }
-            } else {
-                // This is a system app, so we assume that the
-                // other users still have this package installed, so all
-                // we need to do is clear this user's data and save that
-                // it is uninstalled.
-                if (DEBUG_REMOVE) Slog.d(TAG, "Deleting system app");
-                clearPackageStateForUserLIF(ps, user.getIdentifier(), outInfo, flags);
-                scheduleWritePackageRestrictionsLocked(user);
-                return;
             }
         }
 
@@ -18015,8 +18045,7 @@
             if (DEBUG_REMOVE) Slog.d(TAG, "Removing system package: " + ps.name);
             // When an updated system application is deleted we delete the existing resources
             // as well and fall back to existing code in system partition
-            deleteSystemPackageLIF(
-                    action, ps.pkg, ps, allUserHandles, flags, outInfo, writeSettings);
+            deleteSystemPackageLIF(action, ps, allUserHandles, flags, outInfo, writeSettings);
         } else {
             if (DEBUG_REMOVE) Slog.d(TAG, "Removing non-system package: " + ps.name);
             deleteInstalledPackageLIF(ps, deleteCodeAndResources, flags, allUserHandles,
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 effb5a7..f1cd0cd 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
@@ -30,6 +30,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -62,6 +63,7 @@
 import com.android.server.LocalServices;
 import com.android.server.job.JobSchedulerService;
 import com.android.server.job.JobSchedulerService.Constants;
+import com.android.server.job.controllers.QuotaController.ExecutionStats;
 import com.android.server.job.controllers.QuotaController.TimingSession;
 
 import org.junit.After;
@@ -131,13 +133,18 @@
         doReturn(mock(PackageManagerInternal.class))
                 .when(() -> LocalServices.getService(PackageManagerInternal.class));
 
-        // Freeze the clocks at this moment in time
+        // Freeze the clocks at 24 hours after this moment in time. Several tests create sessions
+        // in the past, and QuotaController sometimes floors values at 0, so if the test time
+        // causes sessions with negative timestamps, they will fail.
         JobSchedulerService.sSystemClock =
-                Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC);
-        JobSchedulerService.sUptimeMillisClock =
-                Clock.fixed(SystemClock.uptimeMillisClock().instant(), ZoneOffset.UTC);
-        JobSchedulerService.sElapsedRealtimeClock =
-                Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC);
+                getAdvancedClock(Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC),
+                        24 * HOUR_IN_MILLIS);
+        JobSchedulerService.sUptimeMillisClock = getAdvancedClock(
+                Clock.fixed(SystemClock.uptimeMillisClock().instant(), ZoneOffset.UTC),
+                24 * HOUR_IN_MILLIS);
+        JobSchedulerService.sElapsedRealtimeClock = getAdvancedClock(
+                Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC),
+                24 * HOUR_IN_MILLIS);
 
         // Initialize real objects.
         // Capture the listeners.
@@ -291,9 +298,17 @@
         mQuotaController.saveTimingSession(0, "com.android.test.stay", two);
         mQuotaController.saveTimingSession(0, "com.android.test.stay", one);
 
+        ExecutionStats expectedStats = new ExecutionStats();
+        expectedStats.invalidTimeElapsed = now + 24 * HOUR_IN_MILLIS;
+        expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
+
         mQuotaController.onAppRemovedLocked("com.android.test.remove", 10001);
         assertNull(mQuotaController.getTimingSessions(0, "com.android.test.remove"));
         assertEquals(expected, mQuotaController.getTimingSessions(0, "com.android.test.stay"));
+        assertEquals(expectedStats,
+                mQuotaController.getExecutionStatsLocked(0, "com.android.test.remove", RARE_INDEX));
+        assertNotEquals(expectedStats,
+                mQuotaController.getExecutionStatsLocked(0, "com.android.test.stay", RARE_INDEX));
     }
 
     @Test
@@ -318,13 +333,21 @@
         mQuotaController.saveTimingSession(10, "com.android.test", two);
         mQuotaController.saveTimingSession(10, "com.android.test", one);
 
+        ExecutionStats expectedStats = new ExecutionStats();
+        expectedStats.invalidTimeElapsed = now + 24 * HOUR_IN_MILLIS;
+        expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
+
         mQuotaController.onUserRemovedLocked(0);
         assertNull(mQuotaController.getTimingSessions(0, "com.android.test"));
         assertEquals(expected, mQuotaController.getTimingSessions(10, "com.android.test"));
+        assertEquals(expectedStats,
+                mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX));
+        assertNotEquals(expectedStats,
+                mQuotaController.getExecutionStatsLocked(10, "com.android.test", RARE_INDEX));
     }
 
     @Test
-    public void testGetTrailingExecutionTimeLocked_NoTimer() {
+    public void testUpdateExecutionStatsLocked_NoTimer() {
         final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
         // Added in chronological order.
         mQuotaController.saveTimingSession(0, "com.android.test",
@@ -340,32 +363,288 @@
         mQuotaController.saveTimingSession(0, "com.android.test",
                 createTimingSession(now - 5 * MINUTE_IN_MILLIS, 4 * MINUTE_IN_MILLIS, 3));
 
-        assertEquals(0, mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
-                MINUTE_IN_MILLIS));
-        assertEquals(2 * MINUTE_IN_MILLIS,
-                mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
-                        3 * MINUTE_IN_MILLIS));
-        assertEquals(4 * MINUTE_IN_MILLIS,
-                mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
-                        5 * MINUTE_IN_MILLIS));
-        assertEquals(4 * MINUTE_IN_MILLIS,
-                mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
-                        49 * MINUTE_IN_MILLIS));
-        assertEquals(5 * MINUTE_IN_MILLIS,
-                mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
-                        50 * MINUTE_IN_MILLIS));
-        assertEquals(6 * MINUTE_IN_MILLIS,
-                mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
-                        HOUR_IN_MILLIS));
-        assertEquals(11 * MINUTE_IN_MILLIS,
-                mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
-                        2 * HOUR_IN_MILLIS));
-        assertEquals(12 * MINUTE_IN_MILLIS,
-                mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
-                        3 * HOUR_IN_MILLIS));
-        assertEquals(22 * MINUTE_IN_MILLIS,
-                mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
-                        6 * HOUR_IN_MILLIS));
+        // Test an app that hasn't had any activity.
+        ExecutionStats expectedStats = new ExecutionStats();
+        ExecutionStats inputStats = new ExecutionStats();
+
+        inputStats.windowSizeMs = expectedStats.windowSizeMs = 12 * HOUR_IN_MILLIS;
+        // Invalid time is now +24 hours since there are no sessions at all for the app.
+        expectedStats.invalidTimeElapsed = now + 24 * HOUR_IN_MILLIS;
+        mQuotaController.updateExecutionStatsLocked(0, "com.android.test.not.run", inputStats);
+        assertEquals(expectedStats, inputStats);
+
+        inputStats.windowSizeMs = expectedStats.windowSizeMs = MINUTE_IN_MILLIS;
+        // Invalid time is now +18 hours since there are no sessions in the window but the earliest
+        // session is 6 hours ago.
+        expectedStats.invalidTimeElapsed = now + 18 * HOUR_IN_MILLIS;
+        expectedStats.executionTimeInWindowMs = 0;
+        expectedStats.bgJobCountInWindow = 0;
+        expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInMaxPeriod = 15;
+        mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+        assertEquals(expectedStats, inputStats);
+
+        inputStats.windowSizeMs = expectedStats.windowSizeMs = 3 * MINUTE_IN_MILLIS;
+        // Invalid time is now since the session straddles the window cutoff time.
+        expectedStats.invalidTimeElapsed = now;
+        expectedStats.executionTimeInWindowMs = 2 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInWindow = 3;
+        expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInMaxPeriod = 15;
+        mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+        assertEquals(expectedStats, inputStats);
+
+        inputStats.windowSizeMs = expectedStats.windowSizeMs = 5 * MINUTE_IN_MILLIS;
+        // Invalid time is now since the start of the session is at the very edge of the window
+        // cutoff time.
+        expectedStats.invalidTimeElapsed = now;
+        expectedStats.executionTimeInWindowMs = 4 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInWindow = 3;
+        expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInMaxPeriod = 15;
+        mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+        assertEquals(expectedStats, inputStats);
+
+        inputStats.windowSizeMs = expectedStats.windowSizeMs = 49 * MINUTE_IN_MILLIS;
+        // Invalid time is now +44 minutes since the earliest session in the window is now-5
+        // minutes.
+        expectedStats.invalidTimeElapsed = now + 44 * MINUTE_IN_MILLIS;
+        expectedStats.executionTimeInWindowMs = 4 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInWindow = 3;
+        expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInMaxPeriod = 15;
+        mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+        assertEquals(expectedStats, inputStats);
+
+        inputStats.windowSizeMs = expectedStats.windowSizeMs = 50 * MINUTE_IN_MILLIS;
+        // Invalid time is now since the session is at the very edge of the window cutoff time.
+        expectedStats.invalidTimeElapsed = now;
+        expectedStats.executionTimeInWindowMs = 5 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInWindow = 4;
+        expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInMaxPeriod = 15;
+        mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+        assertEquals(expectedStats, inputStats);
+
+        inputStats.windowSizeMs = expectedStats.windowSizeMs = HOUR_IN_MILLIS;
+        // Invalid time is now since the start of the session is at the very edge of the window
+        // cutoff time.
+        expectedStats.invalidTimeElapsed = now;
+        expectedStats.executionTimeInWindowMs = 6 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInWindow = 5;
+        expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInMaxPeriod = 15;
+        mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+        assertEquals(expectedStats, inputStats);
+
+        inputStats.windowSizeMs = expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS;
+        // Invalid time is now since the session straddles the window cutoff time.
+        expectedStats.invalidTimeElapsed = now;
+        expectedStats.executionTimeInWindowMs = 11 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInWindow = 10;
+        expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInMaxPeriod = 15;
+        expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
+                + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+        mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+        assertEquals(expectedStats, inputStats);
+
+        inputStats.windowSizeMs = expectedStats.windowSizeMs = 3 * HOUR_IN_MILLIS;
+        // Invalid time is now +59 minutes since the earliest session in the window is now-121
+        // minutes.
+        expectedStats.invalidTimeElapsed = now + 59 * MINUTE_IN_MILLIS;
+        expectedStats.executionTimeInWindowMs = 12 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInWindow = 10;
+        expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInMaxPeriod = 15;
+        expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
+                + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+        mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+        assertEquals(expectedStats, inputStats);
+
+        inputStats.windowSizeMs = expectedStats.windowSizeMs = 6 * HOUR_IN_MILLIS;
+        // Invalid time is now since the start of the session is at the very edge of the window
+        // cutoff time.
+        expectedStats.invalidTimeElapsed = now;
+        expectedStats.executionTimeInWindowMs = 22 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInWindow = 15;
+        expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInMaxPeriod = 15;
+        expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
+                + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+        mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+        assertEquals(expectedStats, inputStats);
+
+        // Make sure invalidTimeElapsed is set correctly when it's dependent on the max period.
+        mQuotaController.getTimingSessions(0, "com.android.test")
+                .add(0,
+                        createTimingSession(now - (23 * HOUR_IN_MILLIS), MINUTE_IN_MILLIS, 3));
+        inputStats.windowSizeMs = expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
+        // 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.invalidTimeElapsed = now + HOUR_IN_MILLIS;
+        expectedStats.executionTimeInWindowMs = 22 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInWindow = 15;
+        expectedStats.executionTimeInMaxPeriodMs = 23 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInMaxPeriod = 18;
+        expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
+                + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+        mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+        assertEquals(expectedStats, inputStats);
+
+        mQuotaController.getTimingSessions(0, "com.android.test")
+                .add(0,
+                        createTimingSession(now - (24 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS),
+                                2 * MINUTE_IN_MILLIS, 2));
+        inputStats.windowSizeMs = expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
+        // Invalid time is now since the earlist session straddles the max period cutoff time.
+        expectedStats.invalidTimeElapsed = now;
+        expectedStats.executionTimeInWindowMs = 22 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInWindow = 15;
+        expectedStats.executionTimeInMaxPeriodMs = 24 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInMaxPeriod = 20;
+        expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
+                + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+        mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+        assertEquals(expectedStats, inputStats);
+    }
+
+    /**
+     * Tests that getExecutionStatsLocked returns the correct stats.
+     */
+    @Test
+    public void testGetExecutionStatsLocked_Values() {
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+        mQuotaController.saveTimingSession(0, "com.android.test",
+                createTimingSession(now - (23 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+        mQuotaController.saveTimingSession(0, "com.android.test",
+                createTimingSession(now - (7 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+        mQuotaController.saveTimingSession(0, "com.android.test",
+                createTimingSession(now - (2 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+        mQuotaController.saveTimingSession(0, "com.android.test",
+                createTimingSession(now - (6 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+
+        ExecutionStats expectedStats = new ExecutionStats();
+
+        // Active
+        expectedStats.windowSizeMs = 10 * MINUTE_IN_MILLIS;
+        expectedStats.invalidTimeElapsed = now + 4 * MINUTE_IN_MILLIS;
+        expectedStats.executionTimeInWindowMs = 3 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInWindow = 5;
+        expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInMaxPeriod = 20;
+        assertEquals(expectedStats,
+                mQuotaController.getExecutionStatsLocked(0, "com.android.test", ACTIVE_INDEX));
+
+        // Working
+        expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS;
+        expectedStats.invalidTimeElapsed = now;
+        expectedStats.executionTimeInWindowMs = 13 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInWindow = 10;
+        expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInMaxPeriod = 20;
+        expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS)
+                + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+        assertEquals(expectedStats,
+                mQuotaController.getExecutionStatsLocked(0, "com.android.test", WORKING_INDEX));
+
+        // Frequent
+        expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
+        expectedStats.invalidTimeElapsed = now + HOUR_IN_MILLIS;
+        expectedStats.executionTimeInWindowMs = 23 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInWindow = 15;
+        expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInMaxPeriod = 20;
+        expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS)
+                + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+        assertEquals(expectedStats,
+                mQuotaController.getExecutionStatsLocked(0, "com.android.test", FREQUENT_INDEX));
+
+        // Rare
+        expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
+        expectedStats.invalidTimeElapsed = now + HOUR_IN_MILLIS;
+        expectedStats.executionTimeInWindowMs = 33 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInWindow = 20;
+        expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
+        expectedStats.bgJobCountInMaxPeriod = 20;
+        expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS)
+                + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+        assertEquals(expectedStats,
+                mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX));
+    }
+
+    /**
+     * Tests that getExecutionStatsLocked properly caches the stats and returns the cached object.
+     */
+    @Test
+    public void testGetExecutionStatsLocked_Caching() {
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+        mQuotaController.saveTimingSession(0, "com.android.test",
+                createTimingSession(now - (23 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+        mQuotaController.saveTimingSession(0, "com.android.test",
+                createTimingSession(now - (7 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+        mQuotaController.saveTimingSession(0, "com.android.test",
+                createTimingSession(now - (2 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+        mQuotaController.saveTimingSession(0, "com.android.test",
+                createTimingSession(now - (6 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+        final ExecutionStats originalStatsActive = mQuotaController.getExecutionStatsLocked(0,
+                "com.android.test", ACTIVE_INDEX);
+        final ExecutionStats originalStatsWorking = mQuotaController.getExecutionStatsLocked(0,
+                "com.android.test", WORKING_INDEX);
+        final ExecutionStats originalStatsFrequent = mQuotaController.getExecutionStatsLocked(0,
+                "com.android.test", FREQUENT_INDEX);
+        final ExecutionStats originalStatsRare = mQuotaController.getExecutionStatsLocked(0,
+                "com.android.test", RARE_INDEX);
+
+        // Advance clock so that the working stats shouldn't be the same.
+        advanceElapsedClock(MINUTE_IN_MILLIS);
+        // Change frequent bucket size so that the stats need to be recalculated.
+        mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = 6 * HOUR_IN_MILLIS;
+        mQuotaController.onConstantsUpdatedLocked();
+
+        ExecutionStats expectedStats = new ExecutionStats();
+        expectedStats.windowSizeMs = originalStatsActive.windowSizeMs;
+        expectedStats.invalidTimeElapsed = originalStatsActive.invalidTimeElapsed;
+        expectedStats.executionTimeInWindowMs = originalStatsActive.executionTimeInWindowMs;
+        expectedStats.bgJobCountInWindow = originalStatsActive.bgJobCountInWindow;
+        expectedStats.executionTimeInMaxPeriodMs = originalStatsActive.executionTimeInMaxPeriodMs;
+        expectedStats.bgJobCountInMaxPeriod = originalStatsActive.bgJobCountInMaxPeriod;
+        expectedStats.quotaCutoffTimeElapsed = originalStatsActive.quotaCutoffTimeElapsed;
+        final ExecutionStats newStatsActive = mQuotaController.getExecutionStatsLocked(0,
+                "com.android.test", ACTIVE_INDEX);
+        // Stats for the same bucket should use the same object.
+        assertTrue(originalStatsActive == newStatsActive);
+        assertEquals(expectedStats, newStatsActive);
+
+        expectedStats.windowSizeMs = originalStatsWorking.windowSizeMs;
+        expectedStats.invalidTimeElapsed = originalStatsWorking.invalidTimeElapsed;
+        expectedStats.executionTimeInWindowMs = originalStatsWorking.executionTimeInWindowMs;
+        expectedStats.bgJobCountInWindow = originalStatsWorking.bgJobCountInWindow;
+        expectedStats.quotaCutoffTimeElapsed = originalStatsWorking.quotaCutoffTimeElapsed;
+        final ExecutionStats newStatsWorking = mQuotaController.getExecutionStatsLocked(0,
+                "com.android.test", WORKING_INDEX);
+        assertTrue(originalStatsWorking == newStatsWorking);
+        assertNotEquals(expectedStats, newStatsWorking);
+
+        expectedStats.windowSizeMs = originalStatsFrequent.windowSizeMs;
+        expectedStats.invalidTimeElapsed = originalStatsFrequent.invalidTimeElapsed;
+        expectedStats.executionTimeInWindowMs = originalStatsFrequent.executionTimeInWindowMs;
+        expectedStats.bgJobCountInWindow = originalStatsFrequent.bgJobCountInWindow;
+        expectedStats.quotaCutoffTimeElapsed = originalStatsFrequent.quotaCutoffTimeElapsed;
+        final ExecutionStats newStatsFrequent = mQuotaController.getExecutionStatsLocked(0,
+                "com.android.test", FREQUENT_INDEX);
+        assertTrue(originalStatsFrequent == newStatsFrequent);
+        assertNotEquals(expectedStats, newStatsFrequent);
+
+        expectedStats.windowSizeMs = originalStatsRare.windowSizeMs;
+        expectedStats.invalidTimeElapsed = originalStatsRare.invalidTimeElapsed;
+        expectedStats.executionTimeInWindowMs = originalStatsRare.executionTimeInWindowMs;
+        expectedStats.bgJobCountInWindow = originalStatsRare.bgJobCountInWindow;
+        expectedStats.quotaCutoffTimeElapsed = originalStatsRare.quotaCutoffTimeElapsed;
+        final ExecutionStats newStatsRare = mQuotaController.getExecutionStatsLocked(0,
+                "com.android.test", RARE_INDEX);
+        assertTrue(originalStatsRare == newStatsRare);
+        assertEquals(expectedStats, newStatsRare);
     }
 
     @Test
@@ -394,6 +673,56 @@
     }
 
     @Test
+    public void testMaybeScheduleStartAlarmLocked_Active() {
+        // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
+        // because it schedules an alarm too. Prevent it from doing so.
+        spyOn(mQuotaController);
+        doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked();
+
+        // Active window size is 10 minutes.
+        final int standbyBucket = ACTIVE_INDEX;
+
+        // No sessions saved yet.
+        mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE,
+                standbyBucket);
+        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+        // Test with timing sessions out of window but still under max execution limit.
+        final long expectedAlarmTime =
+                (now - 18 * HOUR_IN_MILLIS) + 24 * HOUR_IN_MILLIS
+                        + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - 18 * HOUR_IN_MILLIS, HOUR_IN_MILLIS, 1));
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - 12 * HOUR_IN_MILLIS, HOUR_IN_MILLIS, 1));
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - 7 * HOUR_IN_MILLIS, HOUR_IN_MILLIS, 1));
+        mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE,
+                standbyBucket);
+        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - 2 * HOUR_IN_MILLIS, 55 * MINUTE_IN_MILLIS, 1));
+        mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE,
+                standbyBucket);
+        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+
+        JobStatus jobStatus = createJobStatus("testMaybeScheduleStartAlarmLocked_Active", 1);
+        mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
+        mQuotaController.prepareForExecutionLocked(jobStatus);
+        advanceElapsedClock(5 * MINUTE_IN_MILLIS);
+        // Timer has only been going for 5 minutes in the past 10 minutes, which is under the window
+        // size limit, but the total execution time for the past 24 hours is 6 hours, so the job no
+        // longer has quota.
+        assertEquals(0, mQuotaController.getRemainingExecutionTimeLocked(jobStatus));
+        mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE,
+                standbyBucket);
+        verify(mAlarmManager, times(1)).set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK),
+                any(), any());
+    }
+
+    @Test
     public void testMaybeScheduleStartAlarmLocked_WorkingSet() {
         // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
         // because it schedules an alarm too. Prevent it from doing so.
@@ -579,8 +908,8 @@
 
         // Start in ACTIVE bucket.
         mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", ACTIVE_INDEX);
-        inOrder.verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(),
-                any());
+        inOrder.verify(mAlarmManager, never())
+                .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
         inOrder.verify(mAlarmManager, never()).cancel(any(AlarmManager.OnAlarmListener.class));
 
         // And down from there.
@@ -620,6 +949,124 @@
         inOrder.verify(mAlarmManager, times(1)).cancel(any(AlarmManager.OnAlarmListener.class));
     }
 
+
+    /**
+     * Tests that the start alarm is properly rescheduled if the earliest session that contributes
+     * to the app being out of quota contributes less than the quota buffer time.
+     */
+    @Test
+    public void testMaybeScheduleStartAlarmLocked_SmallRollingQuota_DefaultValues() {
+        // Use the default values
+        runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck();
+        mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
+        runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck();
+    }
+
+    @Test
+    public void testMaybeScheduleStartAlarmLocked_SmallRollingQuota_UpdatedBufferSize() {
+        // Make sure any new value is used correctly.
+        mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS *= 2;
+        mQuotaController.onConstantsUpdatedLocked();
+        runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck();
+        mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
+        runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck();
+    }
+
+    @Test
+    public void testMaybeScheduleStartAlarmLocked_SmallRollingQuota_UpdatedAllowedTime() {
+        // Make sure any new value is used correctly.
+        mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS /= 2;
+        mQuotaController.onConstantsUpdatedLocked();
+        runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck();
+        mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
+        runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck();
+    }
+
+    @Test
+    public void testMaybeScheduleStartAlarmLocked_SmallRollingQuota_UpdatedMaxTime() {
+        // Make sure any new value is used correctly.
+        mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS /= 2;
+        mQuotaController.onConstantsUpdatedLocked();
+        runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck();
+        mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
+        runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck();
+    }
+
+    @Test
+    public void testMaybeScheduleStartAlarmLocked_SmallRollingQuota_UpdatedEverything() {
+        // Make sure any new value is used correctly.
+        mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS *= 2;
+        mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS /= 2;
+        mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS /= 2;
+        mQuotaController.onConstantsUpdatedLocked();
+        runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck();
+        mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
+        runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck();
+    }
+
+    private void runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck() {
+        // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
+        // because it schedules an alarm too. Prevent it from doing so.
+        spyOn(mQuotaController);
+        doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked();
+
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+        // Working set window size is 2 hours.
+        final int standbyBucket = WORKING_INDEX;
+        final long contributionMs = mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS / 2;
+        final long remainingTimeMs =
+                mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS - contributionMs;
+
+        // Session straddles edge of bucket window. Only the contribution should be counted towards
+        // the quota.
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - (2 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS),
+                        3 * MINUTE_IN_MILLIS + contributionMs, 3));
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - HOUR_IN_MILLIS, remainingTimeMs, 2));
+        // Expected alarm time should be when the app will have QUOTA_BUFFER_MS time of quota, which
+        // is 2 hours + (QUOTA_BUFFER_MS - contributionMs) after the start of the second session.
+        final long expectedAlarmTime = now - HOUR_IN_MILLIS + 2 * HOUR_IN_MILLIS
+                + (mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS - contributionMs);
+        mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE,
+                standbyBucket);
+        verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+    }
+
+
+    private void runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck() {
+        // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
+        // because it schedules an alarm too. Prevent it from doing so.
+        spyOn(mQuotaController);
+        doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked();
+
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+        // Working set window size is 2 hours.
+        final int standbyBucket = WORKING_INDEX;
+        final long contributionMs = mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS / 2;
+        final long remainingTimeMs =
+                mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS - contributionMs;
+
+        // Session straddles edge of 24 hour window. Only the contribution should be counted towards
+        // the quota.
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - (24 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS),
+                        3 * MINUTE_IN_MILLIS + contributionMs, 3));
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - 20 * HOUR_IN_MILLIS, remainingTimeMs, 300));
+        // Expected alarm time should be when the app will have QUOTA_BUFFER_MS time of quota, which
+        // is 24 hours + (QUOTA_BUFFER_MS - contributionMs) after the start of the second session.
+        final long expectedAlarmTime = now - 20 * HOUR_IN_MILLIS
+                //+ mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS
+                + 24 * HOUR_IN_MILLIS
+                + (mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS - contributionMs);
+        mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE,
+                standbyBucket);
+        verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+    }
+
     /** Tests that QuotaController doesn't throttle if throttling is turned off. */
     @Test
     public void testThrottleToggling() throws Exception {
@@ -652,6 +1099,7 @@
         mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_WORKING_MS = 30 * MINUTE_IN_MILLIS;
         mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = 45 * MINUTE_IN_MILLIS;
         mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = 60 * MINUTE_IN_MILLIS;
+        mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = 3 * HOUR_IN_MILLIS;
 
         mQuotaController.onConstantsUpdatedLocked();
 
@@ -662,6 +1110,7 @@
         assertEquals(45 * MINUTE_IN_MILLIS,
                 mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]);
         assertEquals(60 * MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]);
+        assertEquals(3 * HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs());
     }
 
     @Test
@@ -673,6 +1122,7 @@
         mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_WORKING_MS = -MINUTE_IN_MILLIS;
         mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = -MINUTE_IN_MILLIS;
         mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = -MINUTE_IN_MILLIS;
+        mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = -MINUTE_IN_MILLIS;
 
         mQuotaController.onConstantsUpdatedLocked();
 
@@ -682,6 +1132,7 @@
         assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[WORKING_INDEX]);
         assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]);
         assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]);
+        assertEquals(HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs());
 
         // Test larger than a day. Controller should cap at one day.
         mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS = 25 * HOUR_IN_MILLIS;
@@ -690,6 +1141,7 @@
         mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_WORKING_MS = 25 * HOUR_IN_MILLIS;
         mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = 25 * HOUR_IN_MILLIS;
         mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = 25 * HOUR_IN_MILLIS;
+        mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = 25 * HOUR_IN_MILLIS;
 
         mQuotaController.onConstantsUpdatedLocked();
 
@@ -699,6 +1151,7 @@
         assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[WORKING_INDEX]);
         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());
     }
 
     /** Tests that TimingSessions aren't saved when the device is charging. */
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/TimeControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/TimeControllerTest.java
new file mode 100644
index 0000000..494677d
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/TimeControllerTest.java
@@ -0,0 +1,886 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.job.controllers;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static org.junit.Assert.assertFalse;
+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.anyString;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.AlarmManager;
+import android.app.job.JobInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManagerInternal;
+import android.os.SystemClock;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.LocalServices;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.JobSchedulerService.Constants;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.ZoneOffset;
+
+@RunWith(AndroidJUnit4.class)
+public class TimeControllerTest {
+    private static final long SECOND_IN_MILLIS = 1000L;
+    private static final long MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS;
+    private static final long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
+    private static final String TAG_DEADLINE = "*job.deadline*";
+    private static final String TAG_DELAY = "*job.delay*";
+    private static final String SOURCE_PACKAGE = "com.android.frameworks.mockingservicestests";
+    private static final int SOURCE_USER_ID = 0;
+
+    private Constants mConstants;
+    private TimeController mTimeController;
+
+    private MockitoSession mMockingSession;
+    @Mock
+    private AlarmManager mAlarmManager;
+    @Mock
+    private Context mContext;
+    @Mock
+    private JobSchedulerService mJobSchedulerService;
+
+    @Before
+    public void setUp() {
+        mMockingSession = mockitoSession()
+                .initMocks(this)
+                .strictness(Strictness.LENIENT)
+                .mockStatic(LocalServices.class)
+                .startMocking();
+        // Use default constants for now.
+        mConstants = new Constants();
+
+        // Called in StateController constructor.
+        when(mJobSchedulerService.getTestableContext()).thenReturn(mContext);
+        when(mJobSchedulerService.getLock()).thenReturn(mJobSchedulerService);
+        when(mJobSchedulerService.getConstants()).thenReturn(mConstants);
+        // Called in TimeController constructor.
+        when(mContext.getSystemService(Context.ALARM_SERVICE)).thenReturn(mAlarmManager);
+        // Used in JobStatus.
+        doReturn(mock(PackageManagerInternal.class))
+                .when(() -> LocalServices.getService(PackageManagerInternal.class));
+
+        // Freeze the clocks at this moment in time
+        JobSchedulerService.sSystemClock =
+                Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC);
+        JobSchedulerService.sUptimeMillisClock =
+                Clock.fixed(SystemClock.uptimeMillisClock().instant(), ZoneOffset.UTC);
+        JobSchedulerService.sElapsedRealtimeClock =
+                Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC);
+
+        // Initialize real objects.
+        mTimeController = new TimeController(mJobSchedulerService);
+        spyOn(mTimeController);
+    }
+
+    @After
+    public void tearDown() {
+        if (mMockingSession != null) {
+            mMockingSession.finishMocking();
+        }
+    }
+
+    private Clock getAdvancedClock(Clock clock, long incrementMs) {
+        return Clock.offset(clock, Duration.ofMillis(incrementMs));
+    }
+
+    private void advanceElapsedClock(long incrementMs) {
+        JobSchedulerService.sElapsedRealtimeClock = getAdvancedClock(
+                JobSchedulerService.sElapsedRealtimeClock, incrementMs);
+    }
+
+    private static JobInfo.Builder createJob() {
+        return new JobInfo.Builder(101, new ComponentName("foo", "bar"));
+    }
+
+    private JobStatus createJobStatus(String testTag, JobInfo.Builder job) {
+        JobInfo jobInfo = job.build();
+        return JobStatus.createFromJobInfo(
+                jobInfo, 1000, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
+    }
+
+    @Test
+    public void testMaybeStartTrackingJobLocked_AlreadySatisfied() {
+        JobStatus delaySatisfied = createJobStatus(
+                "testMaybeStartTrackingJobLocked_AlreadySatisfied",
+                createJob().setMinimumLatency(1));
+        JobStatus deadlineSatisfied = createJobStatus(
+                "testMaybeStartTrackingJobLocked_AlreadySatisfied",
+                createJob().setOverrideDeadline(1));
+
+        advanceElapsedClock(5);
+
+        mTimeController.maybeStartTrackingJobLocked(delaySatisfied, null);
+        mTimeController.maybeStartTrackingJobLocked(deadlineSatisfied, null);
+        verify(mAlarmManager, never())
+                .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+    }
+
+    @Test
+    public void testMaybeStartTrackingJobLocked_DelayInOrder_NoSkipping() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = false;
+        mTimeController.onConstantsUpdatedLocked();
+
+        runTestMaybeStartTrackingJobLocked_DelayInOrder();
+    }
+
+    @Test
+    public void testMaybeStartTrackingJobLocked_DelayInOrder_WithSkipping_AllReady() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+        mTimeController.onConstantsUpdatedLocked();
+
+        doReturn(true).when(mTimeController).wouldBeReadyWithConstraintLocked(any(), anyInt());
+
+        runTestMaybeStartTrackingJobLocked_DelayInOrder();
+    }
+
+    private void runTestMaybeStartTrackingJobLocked_DelayInOrder() {
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+        JobStatus jobLatest = createJobStatus("testMaybeStartTrackingJobLocked_DelayInOrder",
+                createJob().setMinimumLatency(HOUR_IN_MILLIS));
+        JobStatus jobMiddle = createJobStatus("testMaybeStartTrackingJobLocked_DelayInOrder",
+                createJob().setMinimumLatency(30 * MINUTE_IN_MILLIS));
+        JobStatus jobEarliest = createJobStatus("testMaybeStartTrackingJobLocked_DelayInOrder",
+                createJob().setMinimumLatency(5 * MINUTE_IN_MILLIS));
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY),
+                        any(), any(), any());
+        mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+        inOrder.verify(mAlarmManager, never())
+                .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+        mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+        inOrder.verify(mAlarmManager, never())
+                .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+    }
+
+    @Test
+    public void testMaybeStartTrackingJobLocked_DelayInOrder_WithSkipping_SomeNotReady() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+        mTimeController.onConstantsUpdatedLocked();
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+        JobStatus jobLatest = createJobStatus("testMaybeStartTrackingJobLocked_DelayInOrder",
+                createJob().setMinimumLatency(HOUR_IN_MILLIS));
+        JobStatus jobMiddle = createJobStatus("testMaybeStartTrackingJobLocked_DelayInOrder",
+                createJob().setMinimumLatency(30 * MINUTE_IN_MILLIS));
+        JobStatus jobEarliest = createJobStatus("testMaybeStartTrackingJobLocked_DelayInOrder",
+                createJob().setMinimumLatency(5 * MINUTE_IN_MILLIS));
+
+        doReturn(true).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+        doReturn(false).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobMiddle), anyInt());
+        doReturn(true).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY),
+                        any(), any(), any());
+        mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+        inOrder.verify(mAlarmManager, never())
+                .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+        mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+        inOrder.verify(mAlarmManager, never())
+                .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+    }
+
+    @Test
+    public void testMaybeStartTrackingJobLocked_DelayReverseOrder_NoSkipping() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = false;
+        mTimeController.onConstantsUpdatedLocked();
+
+        runTestMaybeStartTrackingJobLocked_DelayReverseOrder();
+    }
+
+    @Test
+    public void testMaybeStartTrackingJobLocked_DelayReverseOrder_WithSkipping_AllReady() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+        mTimeController.onConstantsUpdatedLocked();
+
+        doReturn(true).when(mTimeController).wouldBeReadyWithConstraintLocked(any(), anyInt());
+
+        runTestMaybeStartTrackingJobLocked_DelayReverseOrder();
+    }
+
+    private void runTestMaybeStartTrackingJobLocked_DelayReverseOrder() {
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+        JobStatus jobLatest = createJobStatus("testMaybeStartTrackingJobLocked_DelayReverseOrder",
+                createJob().setMinimumLatency(HOUR_IN_MILLIS));
+        JobStatus jobMiddle = createJobStatus("testMaybeStartTrackingJobLocked_DelayReverseOrder",
+                createJob().setMinimumLatency(30 * MINUTE_IN_MILLIS));
+        JobStatus jobEarliest = createJobStatus("testMaybeStartTrackingJobLocked_DelayReverseOrder",
+                createJob().setMinimumLatency(5 * MINUTE_IN_MILLIS));
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY), any(),
+                        any(), any());
+        mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 30 * MINUTE_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY),
+                        any(), any(), any());
+        mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY),
+                        any(), any(), any());
+    }
+
+    @Test
+    public void testMaybeStartTrackingJobLocked_DelayReverseOrder_WithSkipping_SomeNotReady() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+        mTimeController.onConstantsUpdatedLocked();
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+        JobStatus jobLatest = createJobStatus("testMaybeStartTrackingJobLocked_DelayReverseOrder",
+                createJob().setMinimumLatency(HOUR_IN_MILLIS));
+        JobStatus jobMiddle = createJobStatus("testMaybeStartTrackingJobLocked_DelayReverseOrder",
+                createJob().setMinimumLatency(30 * MINUTE_IN_MILLIS));
+        JobStatus jobEarliest = createJobStatus("testMaybeStartTrackingJobLocked_DelayReverseOrder",
+                createJob().setMinimumLatency(5 * MINUTE_IN_MILLIS));
+
+        doReturn(true).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+        doReturn(false).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobMiddle), anyInt());
+        doReturn(true).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY), any(),
+                        any(), any());
+        mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+        // Middle alarm shouldn't be set since it won't be ready.
+        inOrder.verify(mAlarmManager, never())
+                .set(anyInt(), anyLong(), anyLong(), anyLong(), eq(TAG_DELAY), any(), any(), any());
+        mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY),
+                        any(), any(), any());
+    }
+
+    @Test
+    public void testMaybeStartTrackingJobLocked_DeadlineInOrder_NoSkipping() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = false;
+        mTimeController.onConstantsUpdatedLocked();
+
+        runTestMaybeStartTrackingJobLocked_DeadlineInOrder();
+    }
+
+    @Test
+    public void testMaybeStartTrackingJobLocked_DeadlineInOrder_WithSkipping_AllReady() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+        mTimeController.onConstantsUpdatedLocked();
+
+        doReturn(true).when(mTimeController).wouldBeReadyWithConstraintLocked(any(), anyInt());
+
+        runTestMaybeStartTrackingJobLocked_DeadlineInOrder();
+    }
+
+    private void runTestMaybeStartTrackingJobLocked_DeadlineInOrder() {
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+        JobStatus jobLatest = createJobStatus("testMaybeStartTrackingJobLocked_DeadlineInOrder",
+                createJob().setOverrideDeadline(HOUR_IN_MILLIS));
+        JobStatus jobMiddle = createJobStatus("testMaybeStartTrackingJobLocked_DeadlineInOrder",
+                createJob().setOverrideDeadline(30 * MINUTE_IN_MILLIS));
+        JobStatus jobEarliest = createJobStatus("testMaybeStartTrackingJobLocked_DeadlineInOrder",
+                createJob().setOverrideDeadline(5 * MINUTE_IN_MILLIS));
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+                        eq(TAG_DEADLINE), any(), any(), any());
+        mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+        inOrder.verify(mAlarmManager, never())
+                .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+        mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+        inOrder.verify(mAlarmManager, never())
+                .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+    }
+
+    @Test
+    public void testMaybeStartTrackingJobLocked_DeadlineInOrder_WithSkipping_SomeNotReady() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+        mTimeController.onConstantsUpdatedLocked();
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+        JobStatus jobLatest = createJobStatus("testMaybeStartTrackingJobLocked_DeadlineInOrder",
+                createJob().setOverrideDeadline(HOUR_IN_MILLIS));
+        JobStatus jobMiddle = createJobStatus("testMaybeStartTrackingJobLocked_DeadlineInOrder",
+                createJob().setOverrideDeadline(30 * MINUTE_IN_MILLIS));
+        JobStatus jobEarliest = createJobStatus("testMaybeStartTrackingJobLocked_DeadlineInOrder",
+                createJob().setOverrideDeadline(5 * MINUTE_IN_MILLIS));
+
+        doReturn(true).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+        doReturn(false).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobMiddle), anyInt());
+        doReturn(true).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+                        eq(TAG_DEADLINE), any(), any(), any());
+        mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+        inOrder.verify(mAlarmManager, never())
+                .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+        mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+        inOrder.verify(mAlarmManager, never())
+                .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+    }
+
+    @Test
+    public void testMaybeStartTrackingJobLocked_DeadlineReverseOrder_NoSkipping() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = false;
+        mTimeController.onConstantsUpdatedLocked();
+
+        runTestMaybeStartTrackingJobLocked_DeadlineReverseOrder();
+    }
+
+    @Test
+    public void testMaybeStartTrackingJobLocked_DeadlineReverseOrder_WithSkipping_AllReady() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+        mTimeController.onConstantsUpdatedLocked();
+
+        doReturn(true).when(mTimeController).wouldBeReadyWithConstraintLocked(any(), anyInt());
+
+        runTestMaybeStartTrackingJobLocked_DeadlineReverseOrder();
+    }
+
+    private void runTestMaybeStartTrackingJobLocked_DeadlineReverseOrder() {
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+        JobStatus jobLatest = createJobStatus(
+                "testMaybeStartTrackingJobLocked_DeadlineReverseOrder",
+                createJob().setOverrideDeadline(HOUR_IN_MILLIS));
+        JobStatus jobMiddle = createJobStatus(
+                "testMaybeStartTrackingJobLocked_DeadlineReverseOrder",
+                createJob().setOverrideDeadline(30 * MINUTE_IN_MILLIS));
+        JobStatus jobEarliest = createJobStatus(
+                "testMaybeStartTrackingJobLocked_DeadlineReverseOrder",
+                createJob().setOverrideDeadline(5 * MINUTE_IN_MILLIS));
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DEADLINE),
+                        any(), any(), any());
+        mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 30 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+                        eq(TAG_DEADLINE), any(), any(), any());
+        mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+                        eq(TAG_DEADLINE), any(), any(), any());
+    }
+
+    @Test
+    public void testMaybeStartTrackingJobLocked_DeadlineReverseOrder_WithSkipping_SomeNotReady() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+        mTimeController.onConstantsUpdatedLocked();
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+        JobStatus jobLatest = createJobStatus(
+                "testMaybeStartTrackingJobLocked_DeadlineReverseOrder",
+                createJob().setOverrideDeadline(HOUR_IN_MILLIS));
+        JobStatus jobMiddle = createJobStatus(
+                "testMaybeStartTrackingJobLocked_DeadlineReverseOrder",
+                createJob().setOverrideDeadline(30 * MINUTE_IN_MILLIS));
+        JobStatus jobEarliest = createJobStatus(
+                "testMaybeStartTrackingJobLocked_DeadlineReverseOrder",
+                createJob().setOverrideDeadline(5 * MINUTE_IN_MILLIS));
+
+        doReturn(true).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+        doReturn(false).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobMiddle), anyInt());
+        doReturn(true).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DEADLINE),
+                        any(), any(), any());
+        mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+        // Middle alarm should be skipped since the job wouldn't be ready.
+        inOrder.verify(mAlarmManager, never())
+                .set(anyInt(), anyLong(), anyLong(), anyLong(), eq(TAG_DEADLINE), any(), any(),
+                        any());
+        mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+                        eq(TAG_DEADLINE), any(), any(), any());
+    }
+
+    @Test
+    public void testJobSkipToggling() {
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+        JobStatus jobLatest = createJobStatus(
+                "testMaybeStartTrackingJobLocked_DeadlineReverseOrder",
+                createJob().setOverrideDeadline(HOUR_IN_MILLIS));
+        JobStatus jobEarliest = createJobStatus(
+                "testMaybeStartTrackingJobLocked_DeadlineReverseOrder",
+                createJob().setOverrideDeadline(5 * MINUTE_IN_MILLIS));
+
+        doReturn(true).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+        doReturn(false).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+
+        // Starting off with the skipping off, we should still set an alarm for the earlier job.
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = false;
+        mTimeController.onConstantsUpdatedLocked();
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+        mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+                        eq(TAG_DEADLINE), any(), any(), any());
+
+        // Turn it on, use alarm for later job.
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+        mTimeController.onConstantsUpdatedLocked();
+
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DEADLINE),
+                        any(), any(), any());
+
+        // Back off, use alarm for earlier job.
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = false;
+        mTimeController.onConstantsUpdatedLocked();
+
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+                        eq(TAG_DEADLINE), any(), any(), any());
+    }
+
+    @Test
+    public void testCheckExpiredDelaysAndResetAlarm_NoSkipping() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = false;
+        mTimeController.onConstantsUpdatedLocked();
+
+        runTestCheckExpiredDelaysAndResetAlarm();
+    }
+
+    @Test
+    public void testCheckExpiredDelaysAndResetAlarm_WithSkipping_AllReady() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+        mTimeController.onConstantsUpdatedLocked();
+
+        doReturn(true).when(mTimeController).wouldBeReadyWithConstraintLocked(any(), anyInt());
+
+        runTestCheckExpiredDelaysAndResetAlarm();
+    }
+
+    private void runTestCheckExpiredDelaysAndResetAlarm() {
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+        JobStatus jobLatest = createJobStatus("testCheckExpiredDelaysAndResetAlarm",
+                createJob().setMinimumLatency(HOUR_IN_MILLIS));
+        JobStatus jobMiddle = createJobStatus("testCheckExpiredDelaysAndResetAlarm",
+                createJob().setMinimumLatency(30 * MINUTE_IN_MILLIS));
+        JobStatus jobEarliest = createJobStatus("testCheckExpiredDelaysAndResetAlarm",
+                createJob().setMinimumLatency(5 * MINUTE_IN_MILLIS));
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+        mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+        mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY),
+                        any(), any(), any());
+
+        advanceElapsedClock(10 * MINUTE_IN_MILLIS);
+
+        mTimeController.checkExpiredDelaysAndResetAlarm();
+        assertTrue(jobEarliest.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+        assertFalse(jobMiddle.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+        assertFalse(jobLatest.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 30 * MINUTE_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY),
+                        any(), any(), any());
+
+        advanceElapsedClock(30 * MINUTE_IN_MILLIS);
+
+        mTimeController.checkExpiredDelaysAndResetAlarm();
+        assertTrue(jobEarliest.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+        assertTrue(jobMiddle.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+        assertFalse(jobLatest.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY), any(),
+                        any(), any());
+
+        advanceElapsedClock(30 * MINUTE_IN_MILLIS);
+
+        mTimeController.checkExpiredDelaysAndResetAlarm();
+        assertTrue(jobEarliest.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+        assertTrue(jobMiddle.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+        assertTrue(jobLatest.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+        inOrder.verify(mAlarmManager, never())
+                .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+    }
+
+    @Test
+    public void testCheckExpiredDelaysAndResetAlarm_WithSkipping_SomeNotReady() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+        mTimeController.onConstantsUpdatedLocked();
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+        JobStatus jobLatest = createJobStatus("testCheckExpiredDelaysAndResetAlarm",
+                createJob().setMinimumLatency(HOUR_IN_MILLIS));
+        JobStatus jobMiddle = createJobStatus("testCheckExpiredDelaysAndResetAlarm",
+                createJob().setMinimumLatency(30 * MINUTE_IN_MILLIS));
+        JobStatus jobEarliest = createJobStatus("testCheckExpiredDelaysAndResetAlarm",
+                createJob().setMinimumLatency(5 * MINUTE_IN_MILLIS));
+
+        doReturn(true).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+        doReturn(false).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobMiddle), anyInt());
+        doReturn(true).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+        mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+        mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY),
+                        any(), any(), any());
+
+        advanceElapsedClock(10 * MINUTE_IN_MILLIS);
+
+        mTimeController.checkExpiredDelaysAndResetAlarm();
+        assertTrue(jobEarliest.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+        assertFalse(jobMiddle.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+        assertFalse(jobLatest.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+        // Middle job wouldn't be ready, so its alarm should be skipped.
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY), any(),
+                        any(), any());
+
+        advanceElapsedClock(55 * MINUTE_IN_MILLIS);
+
+        mTimeController.checkExpiredDelaysAndResetAlarm();
+        assertTrue(jobEarliest.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+        assertTrue(jobMiddle.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+        assertTrue(jobLatest.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+        inOrder.verify(mAlarmManager, never())
+                .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+    }
+
+    @Test
+    public void testCheckExpiredDeadlinesAndResetAlarm_NoSkipping() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = false;
+        mTimeController.onConstantsUpdatedLocked();
+
+        runTestCheckExpiredDeadlinesAndResetAlarm();
+    }
+
+    @Test
+    public void testCheckExpiredDeadlinesAndResetAlarm_WithSkipping_AllReady() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+        mTimeController.onConstantsUpdatedLocked();
+
+        doReturn(true).when(mTimeController).wouldBeReadyWithConstraintLocked(any(), anyInt());
+
+        runTestCheckExpiredDeadlinesAndResetAlarm();
+    }
+
+    private void runTestCheckExpiredDeadlinesAndResetAlarm() {
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+        JobStatus jobLatest = createJobStatus("testCheckExpiredDeadlinesAndResetAlarm",
+                createJob().setOverrideDeadline(HOUR_IN_MILLIS));
+        JobStatus jobMiddle = createJobStatus("testCheckExpiredDeadlinesAndResetAlarm",
+                createJob().setOverrideDeadline(30 * MINUTE_IN_MILLIS));
+        JobStatus jobEarliest = createJobStatus("testCheckExpiredDeadlinesAndResetAlarm",
+                createJob().setOverrideDeadline(5 * MINUTE_IN_MILLIS));
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+        mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+        mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+                        eq(TAG_DEADLINE), any(), any(), any());
+
+        advanceElapsedClock(10 * MINUTE_IN_MILLIS);
+
+        mTimeController.checkExpiredDeadlinesAndResetAlarm();
+        assertTrue(jobEarliest.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+        assertFalse(jobMiddle.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+        assertFalse(jobLatest.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 30 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+                        eq(TAG_DEADLINE), any(), any(), any());
+
+        advanceElapsedClock(30 * MINUTE_IN_MILLIS);
+
+        mTimeController.checkExpiredDeadlinesAndResetAlarm();
+        assertTrue(jobEarliest.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+        assertTrue(jobMiddle.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+        assertFalse(jobLatest.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DEADLINE),
+                        any(), any(), any());
+
+        advanceElapsedClock(30 * MINUTE_IN_MILLIS);
+
+        mTimeController.checkExpiredDeadlinesAndResetAlarm();
+        assertTrue(jobEarliest.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+        assertTrue(jobMiddle.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+        assertTrue(jobLatest.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+        inOrder.verify(mAlarmManager, never())
+                .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+    }
+
+    @Test
+    public void testCheckExpiredDeadlinesAndResetAlarm_WithSkipping_SomeNotReady() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+        mTimeController.onConstantsUpdatedLocked();
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+        JobStatus jobLatest = createJobStatus("testCheckExpiredDeadlinesAndResetAlarm",
+                createJob().setOverrideDeadline(HOUR_IN_MILLIS));
+        JobStatus jobMiddle = createJobStatus("testCheckExpiredDeadlinesAndResetAlarm",
+                createJob().setOverrideDeadline(30 * MINUTE_IN_MILLIS));
+        JobStatus jobEarliest = createJobStatus("testCheckExpiredDeadlinesAndResetAlarm",
+                createJob().setOverrideDeadline(5 * MINUTE_IN_MILLIS));
+
+        doReturn(true).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+        doReturn(false).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobMiddle), anyInt());
+        doReturn(true).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+        mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+        mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+                        eq(TAG_DEADLINE), any(), any(), any());
+
+        advanceElapsedClock(10 * MINUTE_IN_MILLIS);
+
+        mTimeController.checkExpiredDeadlinesAndResetAlarm();
+        assertTrue(jobEarliest.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+        assertFalse(jobMiddle.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+        assertFalse(jobLatest.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+        // Middle job wouldn't be ready, so its alarm should be skipped.
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DEADLINE),
+                        any(), any(), any());
+
+        advanceElapsedClock(55 * MINUTE_IN_MILLIS);
+
+        mTimeController.checkExpiredDeadlinesAndResetAlarm();
+        assertTrue(jobEarliest.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+        assertTrue(jobMiddle.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+        assertTrue(jobLatest.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+        inOrder.verify(mAlarmManager, never())
+                .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+    }
+
+    @Test
+    public void testEvaluateStateLocked_SkippingOff() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = false;
+        mTimeController.onConstantsUpdatedLocked();
+        JobStatus job = createJobStatus("testEvaluateStateLocked_SkippingOff",
+                createJob().setOverrideDeadline(HOUR_IN_MILLIS));
+
+        mTimeController.evaluateStateLocked(job);
+        verify(mAlarmManager, never())
+                .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+    }
+
+    @Test
+    public void testEvaluateStateLocked_SkippingOn_Delay() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+        mTimeController.onConstantsUpdatedLocked();
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+        JobStatus jobLatest = createJobStatus("testEvaluateStateLocked_SkippingOn_Delay",
+                createJob().setMinimumLatency(HOUR_IN_MILLIS));
+        JobStatus jobMiddle = createJobStatus("testEvaluateStateLocked_SkippingOn_Delay",
+                createJob().setMinimumLatency(30 * MINUTE_IN_MILLIS));
+        JobStatus jobEarliest = createJobStatus("testEvaluateStateLocked_SkippingOn_Delay",
+                createJob().setMinimumLatency(5 * MINUTE_IN_MILLIS));
+
+        doReturn(false).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+        doReturn(true).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobMiddle), anyInt());
+        doReturn(false).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+        mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+        mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 30 * MINUTE_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY),
+                        any(), any(), any());
+
+        // Test evaluating something after the current deadline.
+        doReturn(true).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+        mTimeController.evaluateStateLocked(jobLatest);
+        inOrder.verify(mAlarmManager, never())
+                .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+
+        // Test evaluating something before the current deadline.
+        doReturn(true).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+        mTimeController.evaluateStateLocked(jobEarliest);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY),
+                        any(), any(), any());
+        // Job goes back to not being ready. Middle is still true, so use that alarm.
+        doReturn(false).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+        mTimeController.evaluateStateLocked(jobEarliest);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 30 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+                        eq(TAG_DELAY), any(), any(), any());
+        // Turn middle off. Latest is true, so use that alarm.
+        doReturn(false).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobMiddle), anyInt());
+        mTimeController.evaluateStateLocked(jobMiddle);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(),
+                        eq(TAG_DELAY), any(), any(), any());
+    }
+
+    @Test
+    public void testEvaluateStateLocked_SkippingOn_Deadline() {
+        mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+        mTimeController.onConstantsUpdatedLocked();
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+        JobStatus jobLatest = createJobStatus("testEvaluateStateLocked_SkippingOn_Deadline",
+                createJob().setOverrideDeadline(HOUR_IN_MILLIS));
+        JobStatus jobMiddle = createJobStatus("testEvaluateStateLocked_SkippingOn_Deadline",
+                createJob().setOverrideDeadline(30 * MINUTE_IN_MILLIS));
+        JobStatus jobEarliest = createJobStatus("testEvaluateStateLocked_SkippingOn_Deadline",
+                createJob().setOverrideDeadline(5 * MINUTE_IN_MILLIS));
+
+        doReturn(false).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+        doReturn(true).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobMiddle), anyInt());
+        doReturn(false).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+        mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+        mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 30 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+                        eq(TAG_DEADLINE), any(), any(), any());
+
+        // Test evaluating something after the current deadline.
+        doReturn(true).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+        mTimeController.evaluateStateLocked(jobLatest);
+        inOrder.verify(mAlarmManager, never())
+                .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+
+        // Test evaluating something before the current deadline.
+        doReturn(true).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+        mTimeController.evaluateStateLocked(jobEarliest);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+                        eq(TAG_DEADLINE), any(), any(), any());
+        // Job goes back to not being ready. Middle is still true, so use that alarm.
+        doReturn(false).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+        mTimeController.evaluateStateLocked(jobEarliest);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + 30 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+                        eq(TAG_DEADLINE), any(), any(), any());
+        // Turn middle off. Latest is true, so use that alarm.
+        doReturn(false).when(mTimeController)
+                .wouldBeReadyWithConstraintLocked(eq(jobMiddle), anyInt());
+        mTimeController.evaluateStateLocked(jobMiddle);
+        inOrder.verify(mAlarmManager, times(1))
+                .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(),
+                        eq(TAG_DEADLINE), any(), any(), any());
+    }
+}
diff --git a/telephony/java/android/telephony/NetworkService.java b/telephony/java/android/telephony/NetworkService.java
index 4354314..4bca404 100644
--- a/telephony/java/android/telephony/NetworkService.java
+++ b/telephony/java/android/telephony/NetworkService.java
@@ -16,7 +16,6 @@
 
 package android.telephony;
 
-import android.annotation.CallSuper;
 import android.annotation.SystemApi;
 import android.app.Service;
 import android.content.Intent;
@@ -53,7 +52,6 @@
     private final String TAG = NetworkService.class.getSimpleName();
 
     public static final String NETWORK_SERVICE_INTERFACE = "android.telephony.NetworkService";
-    public static final String NETWORK_SERVICE_EXTRA_SLOT_ID = "android.telephony.extra.SLOT_ID";
 
     private static final int NETWORK_SERVICE_CREATE_NETWORK_SERVICE_PROVIDER                 = 1;
     private static final int NETWORK_SERVICE_REMOVE_NETWORK_SERVICE_PROVIDER                 = 2;
@@ -81,7 +79,7 @@
      * must extend this class to support network connection. Note that each instance of network
      * service is associated with one physical SIM slot.
      */
-    public class NetworkServiceProvider {
+    public abstract class NetworkServiceProvider implements AutoCloseable {
         private final int mSlotId;
 
         private final List<INetworkServiceCallback>
@@ -137,12 +135,12 @@
         }
 
         /**
-         * Called when the instance of network service is destroyed (e.g. got unbind or binder died).
+         * Called when the instance of network service is destroyed (e.g. got unbind or binder died)
+         * or when the network service provider is removed. The extended class should implement this
+         * method to perform cleanup works.
          */
-        @CallSuper
-        protected void onDestroy() {
-            mNetworkRegistrationStateChangedCallbacks.clear();
-        }
+        @Override
+        public abstract void close();
     }
 
     private class NetworkServiceHandler extends Handler {
@@ -168,7 +166,7 @@
                 case NETWORK_SERVICE_REMOVE_NETWORK_SERVICE_PROVIDER:
                     // If the service provider doesn't exist yet, we try to create it.
                     if (serviceProvider != null) {
-                        serviceProvider.onDestroy();
+                        serviceProvider.close();
                         mServiceMap.remove(slotId);
                     }
                     break;
@@ -176,7 +174,7 @@
                     for (int i = 0; i < mServiceMap.size(); i++) {
                         serviceProvider = mServiceMap.get(i);
                         if (serviceProvider != null) {
-                            serviceProvider.onDestroy();
+                            serviceProvider.close();
                         }
                     }
                     mServiceMap.clear();
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 387453f..8c65eb6 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -572,7 +572,6 @@
      * TelephonyProvider column name for whether a subscription is opportunistic, that is,
      * whether the network it connects to is limited in functionality or coverage.
      * For example, CBRS.
-     * IS_EMBEDDED should always be true.
      * <p>Type: INTEGER (int), 1 for opportunistic or 0 for non-opportunistic.
      * @hide
      */
@@ -2379,18 +2378,32 @@
     }
 
     /**
-     * Set opportunistic by simInfo index
+     * Set whether a subscription is opportunistic, that is, whether the network it connects
+     * to has limited coverage. For example, CBRS. Setting a subscription opportunistic has
+     * following impacts:
+     *  1) Even if it's active, it will be dormant most of the time. The modem will not try
+     *     to scan or camp until it knows an available network is nearby to save power.
+     *  2) Telephony relies on system app or carrier input to notify nearby available networks.
+     *     See {@link TelephonyManager#updateAvailableNetworks(List)} for more information.
+     *  3) In multi-SIM devices, when the network is nearby and camped, system may automatically
+     *     switch internet data between it and default data subscription, based on carrier
+     *     recommendation and its signal strength and metered-ness, etc.
+     *
+     *
+     * Caller will either have {@link android.Manifest.permission#MODIFY_PHONE_STATE} or carrier
+     * privilege permission of the subscription.
      *
      * @param opportunistic whether it’s opportunistic subscription.
      * @param subId the unique SubscriptionInfo index in database
-     * @return the number of records updated
-     * @hide
+     * @return {@code true} if the operation is succeed, {@code false} otherwise.
      */
+    @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
     @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
-    public int setOpportunistic(boolean opportunistic, int subId) {
+    public boolean setOpportunistic(boolean opportunistic, int subId) {
         if (VDBG) logd("[setOpportunistic]+ opportunistic:" + opportunistic + " subId:" + subId);
         return setSubscriptionPropertyHelper(subId, "setOpportunistic",
-                (iSub)-> iSub.setOpportunistic(opportunistic, subId));
+                (iSub)-> iSub.setOpportunistic(
+                        opportunistic, subId, mContext.getOpPackageName())) == 1;
     }
 
     /**
@@ -2510,18 +2523,26 @@
     }
 
     /**
-     * Set metered by simInfo index
+     * Set if a subscription is metered or not. Similar to Wi-Fi, metered means
+     * user may be charged more if more data is used.
+     *
+     * By default all Cellular networks are considered metered. System or carrier privileged apps
+     * can set a subscription un-metered which will be considered when system switches data between
+     * primary subscription and opportunistic subscription.
+     *
+     * Caller will either have {@link android.Manifest.permission#MODIFY_PHONE_STATE} or carrier
+     * privilege permission of the subscription.
      *
      * @param isMetered whether it’s a metered subscription.
      * @param subId the unique SubscriptionInfo index in database
-     * @return the number of records updated
-     * @hide
+     * @return {@code true} if the operation is succeed, {@code false} otherwise.
      */
+    @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
     @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
-    public int setMetered(boolean isMetered, int subId) {
+    public boolean setMetered(boolean isMetered, int subId) {
         if (VDBG) logd("[setIsMetered]+ isMetered:" + isMetered + " subId:" + subId);
         return setSubscriptionPropertyHelper(subId, "setIsMetered",
-                (iSub)-> iSub.setMetered(isMetered, subId));
+                (iSub)-> iSub.setMetered(isMetered, subId, mContext.getOpPackageName())) == 1;
     }
 
     /**
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index e2d03a0..e0632b1 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -6671,8 +6671,8 @@
         try {
             ITelephony telephony = getITelephony();
             if (telephony != null) {
-                return telephony.getCarrierPrivilegeStatus(subId) ==
-                    CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
+                return telephony.getCarrierPrivilegeStatus(subId)
+                        == CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
             }
         } catch (RemoteException ex) {
             Rlog.e(TAG, "hasCarrierPrivileges RemoteException", ex);
diff --git a/telephony/java/android/telephony/data/DataService.java b/telephony/java/android/telephony/data/DataService.java
index 1db5850..74d1e83 100644
--- a/telephony/java/android/telephony/data/DataService.java
+++ b/telephony/java/android/telephony/data/DataService.java
@@ -16,7 +16,6 @@
 
 package android.telephony.data;
 
-import android.annotation.CallSuper;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -60,7 +59,6 @@
     private static final String TAG = DataService.class.getSimpleName();
 
     public static final String DATA_SERVICE_INTERFACE = "android.telephony.data.DataService";
-    public static final String DATA_SERVICE_EXTRA_SLOT_ID = "android.telephony.data.extra.SLOT_ID";
 
     /** {@hide} */
     @IntDef(prefix = "REQUEST_REASON_", value = {
@@ -116,7 +114,7 @@
      * must extend this class to support data connection. Note that each instance of data service
      * provider is associated with one physical SIM slot.
      */
-    public class DataServiceProvider {
+    public abstract class DataServiceProvider implements AutoCloseable {
 
         private final int mSlotId;
 
@@ -250,12 +248,12 @@
         }
 
         /**
-         * Called when the instance of data service is destroyed (e.g. got unbind or binder died).
+         * Called when the instance of data service is destroyed (e.g. got unbind or binder died)
+         * or when the data service provider is removed. The extended class should implement this
+         * method to perform cleanup works.
          */
-        @CallSuper
-        protected void onDestroy() {
-            mDataCallListChangedCallbacks.clear();
-        }
+        @Override
+        public abstract void close();
     }
 
     private static final class SetupDataCallRequest {
@@ -345,7 +343,7 @@
                     break;
                 case DATA_SERVICE_REMOVE_DATA_SERVICE_PROVIDER:
                     if (serviceProvider != null) {
-                        serviceProvider.onDestroy();
+                        serviceProvider.close();
                         mServiceMap.remove(slotId);
                     }
                     break;
@@ -353,7 +351,7 @@
                     for (int i = 0; i < mServiceMap.size(); i++) {
                         serviceProvider = mServiceMap.get(i);
                         if (serviceProvider != null) {
-                            serviceProvider.onDestroy();
+                            serviceProvider.close();
                         }
                     }
                     mServiceMap.clear();
diff --git a/telephony/java/android/telephony/data/QualifiedNetworksService.java b/telephony/java/android/telephony/data/QualifiedNetworksService.java
index 57d9cce..45b4849 100644
--- a/telephony/java/android/telephony/data/QualifiedNetworksService.java
+++ b/telephony/java/android/telephony/data/QualifiedNetworksService.java
@@ -151,7 +151,7 @@
 
         /**
          * Called when the qualified networks updater is removed. The extended class should
-         * implement this method to perform clean up works.
+         * implement this method to perform cleanup works.
          */
         @Override
         public abstract void close();
diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl
index 65d1a920..65eedb8 100755
--- a/telephony/java/com/android/internal/telephony/ISub.aidl
+++ b/telephony/java/com/android/internal/telephony/ISub.aidl
@@ -162,7 +162,7 @@
      * @param subId the unique SubscriptionInfo index in database
      * @return the number of records updated
      */
-    int setOpportunistic(boolean opportunistic, int subId);
+    int setOpportunistic(boolean opportunistic, int subId, String callingPackage);
 
     /**
      * Inform SubscriptionManager that subscriptions in the list are bundled
@@ -190,7 +190,7 @@
      * @param subId the unique SubscriptionInfo index in database
      * @return the number of records updated
      */
-    int setMetered(boolean isMetered, int subId);
+    int setMetered(boolean isMetered, int subId, String callingPackage);
 
     /**
      * Set which subscription is preferred for cellular data. It's
diff --git a/wifi/java/android/net/wifi/aware/DiscoverySession.java b/wifi/java/android/net/wifi/aware/DiscoverySession.java
index 699f54c..a47e70b 100644
--- a/wifi/java/android/net/wifi/aware/DiscoverySession.java
+++ b/wifi/java/android/net/wifi/aware/DiscoverySession.java
@@ -34,11 +34,11 @@
  * {@link PublishDiscoverySession} and {@link SubscribeDiscoverySession}. This
  * class provides functionality common to both publish and subscribe discovery sessions:
  * <ul>
- *     <li>Sending messages: {@link #sendMessage(PeerHandle, int, byte[])} method.
- *     <li>Creating a network-specifier when requesting a Aware connection:
- *     {@link #createNetworkSpecifierOpen(PeerHandle)} or
- *     {@link #createNetworkSpecifierPassphrase(PeerHandle, String)}.
+ *      <li>Sending messages: {@link #sendMessage(PeerHandle, int, byte[])} method.
+ *      <li>Creating a network-specifier when requesting a Aware connection using
+ *      {@link WifiAwareManager.NetworkSpecifierBuilder}.
  * </ul>
+ * <p>
  * The {@link #close()} method must be called to destroy discovery sessions once they are
  * no longer needed.
  */
@@ -270,6 +270,7 @@
      * <p>
      * To set up an encrypted link use the
      * {@link #createNetworkSpecifierPassphrase(PeerHandle, String)} API.
+     * @deprecated Use the replacement {@link WifiAwareManager.NetworkSpecifierBuilder}.
      *
      * @param peerHandle The peer's handle obtained through
      * {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle, byte[], java.util.List)}
@@ -284,6 +285,7 @@
      * android.net.ConnectivityManager.NetworkCallback)}
      * [or other varieties of that API].
      */
+    @Deprecated
     public NetworkSpecifier createNetworkSpecifierOpen(@NonNull PeerHandle peerHandle) {
         if (mTerminated) {
             Log.w(TAG, "createNetworkSpecifierOpen: called on terminated session");
@@ -318,6 +320,7 @@
      * <p>
      * Note: per the Wi-Fi Aware specification the roles are fixed - a Subscriber is an INITIATOR
      * and a Publisher is a RESPONDER.
+     * @deprecated Use the replacement {@link WifiAwareManager.NetworkSpecifierBuilder}.
      *
      * @param peerHandle The peer's handle obtained through
      * {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle,
@@ -336,6 +339,7 @@
      * android.net.ConnectivityManager.NetworkCallback)}
      * [or other varieties of that API].
      */
+    @Deprecated
     public NetworkSpecifier createNetworkSpecifierPassphrase(
             @NonNull PeerHandle peerHandle, @NonNull String passphrase) {
         if (!WifiAwareUtils.validatePassphrase(passphrase)) {
@@ -376,6 +380,7 @@
      * <p>
      * Note: per the Wi-Fi Aware specification the roles are fixed - a Subscriber is an INITIATOR
      * and a Publisher is a RESPONDER.
+     * @deprecated Use the replacement {@link WifiAwareManager.NetworkSpecifierBuilder}.
      *
      * @param peerHandle The peer's handle obtained through
      * {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle,
@@ -397,6 +402,7 @@
      *
      * @hide
      */
+    @Deprecated
     @SystemApi
     public NetworkSpecifier createNetworkSpecifierPmk(@NonNull PeerHandle peerHandle,
             @NonNull byte[] pmk) {
diff --git a/wifi/java/android/net/wifi/aware/WifiAwareManager.java b/wifi/java/android/net/wifi/aware/WifiAwareManager.java
index 8529a89..26a6c08 100644
--- a/wifi/java/android/net/wifi/aware/WifiAwareManager.java
+++ b/wifi/java/android/net/wifi/aware/WifiAwareManager.java
@@ -21,6 +21,7 @@
 import android.annotation.Nullable;
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.content.Context;
 import android.net.ConnectivityManager;
@@ -57,11 +58,7 @@
  * {@link WifiAwareSession#subscribe(SubscribeConfig, DiscoverySessionCallback, Handler)}.
  * <li>Create a Aware network specifier to be used with
  * {@link ConnectivityManager#requestNetwork(NetworkRequest, ConnectivityManager.NetworkCallback)}
- * to set-up a Aware connection with a peer. Refer to
- * {@link DiscoverySession#createNetworkSpecifierOpen(PeerHandle)},
- * {@link DiscoverySession#createNetworkSpecifierPassphrase(PeerHandle, String)},
- * {@link WifiAwareSession#createNetworkSpecifierOpen(int, byte[])}, and
- * {@link WifiAwareSession#createNetworkSpecifierPassphrase(int, byte[], String)}.
+ * to set-up a Aware connection with a peer. Refer to {@link NetworkSpecifierBuilder}.
  * </ul>
  * <p>
  *     Aware may not be usable when Wi-Fi is disabled (and other conditions). To validate that
@@ -110,10 +107,7 @@
  *        <li>{@link NetworkRequest.Builder#addTransportType(int)} of
  *        {@link android.net.NetworkCapabilities#TRANSPORT_WIFI_AWARE}.
  *        <li>{@link NetworkRequest.Builder#setNetworkSpecifier(String)} using
- *        {@link WifiAwareSession#createNetworkSpecifierOpen(int, byte[])},
- *        {@link WifiAwareSession#createNetworkSpecifierPassphrase(int, byte[], String)},
- *        {@link DiscoverySession#createNetworkSpecifierOpen(PeerHandle)}, or
- *        {@link DiscoverySession#createNetworkSpecifierPassphrase(PeerHandle, String)}.
+ *        {@link NetworkSpecifierBuilder}.
  *    </ul>
  */
 @SystemService(Context.WIFI_AWARE_SERVICE)
@@ -145,8 +139,6 @@
      * Connection creation role is that of INITIATOR. Used to create a network specifier string
      * when requesting a Aware network.
      *
-     * @see DiscoverySession#createNetworkSpecifierOpen(PeerHandle)
-     * @see DiscoverySession#createNetworkSpecifierPassphrase(PeerHandle, String)
      * @see WifiAwareSession#createNetworkSpecifierOpen(int, byte[])
      * @see WifiAwareSession#createNetworkSpecifierPassphrase(int, byte[], String)
      */
@@ -156,8 +148,6 @@
      * Connection creation role is that of RESPONDER. Used to create a network specifier string
      * when requesting a Aware network.
      *
-     * @see DiscoverySession#createNetworkSpecifierOpen(PeerHandle)
-     * @see DiscoverySession#createNetworkSpecifierPassphrase(PeerHandle, String)
      * @see WifiAwareSession#createNetworkSpecifierOpen(int, byte[])
      * @see WifiAwareSession#createNetworkSpecifierPassphrase(int, byte[], String)
      */
@@ -415,6 +405,11 @@
                     + ", passphrase=" + ((passphrase == null) ? "null" : "non-null"));
         }
 
+        if (!WifiAwareUtils.isLegacyVersion(mContext, Build.VERSION_CODES.Q)) {
+            throw new UnsupportedOperationException(
+                    "API not deprecated - use WifiAwareManager.NetworkSpecifierBuilder");
+        }
+
         if (role != WIFI_AWARE_DATA_PATH_ROLE_INITIATOR
                 && role != WIFI_AWARE_DATA_PATH_ROLE_RESPONDER) {
             throw new IllegalArgumentException(
@@ -813,4 +808,135 @@
             mOriginalCallback.onSessionTerminated();
         }
     }
+
+    /**
+     * A builder class for a Wi-Fi Aware network specifier to set up an Aware connection with a
+     * peer.
+     * <p>
+     * Note that all Wi-Fi Aware connection specifier objects must call the
+     * {@link NetworkSpecifierBuilder#setDiscoverySession(DiscoverySession)} to specify the context
+     * within which the connection is created, and
+     * {@link NetworkSpecifierBuilder#setPeerHandle(PeerHandle)} to specify the peer to which the
+     * connection is created.
+     */
+    public static class NetworkSpecifierBuilder {
+        private DiscoverySession mDiscoverySession;
+        private PeerHandle mPeerHandle;
+        private String mPskPassphrase;
+        private byte[] mPmk;
+
+        /**
+         * Configure the {@link PublishDiscoverySession} or {@link SubscribeDiscoverySession}
+         * discovery session in whose context the connection is created.
+         * <p>
+         * Note: this method must be called for any connection request!
+         *
+         * @param discoverySession A Wi-Fi Aware discovery session.
+         * @return the current {@link NetworkSpecifierBuilder} builder, enabling chaining of builder
+         *         methods.
+         */
+        public @NonNull NetworkSpecifierBuilder setDiscoverySession(
+                @NonNull DiscoverySession discoverySession) {
+            if (discoverySession == null) {
+                throw new IllegalArgumentException("Non-null discoverySession required");
+            }
+            mDiscoverySession = discoverySession;
+            return this;
+        }
+
+        /**
+         * Configure the {@link PeerHandle} of the peer to which the Wi-Fi Aware connection is
+         * requested. The peer is discovered through Wi-Fi Aware discovery,
+         * <p>
+         * Note: this method must be called for any connection request!
+         *
+         * @param peerHandle The peer's handle obtained through
+         * {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle, byte[], java.util.List)}
+         *                   or
+         *                   {@link DiscoverySessionCallback#onMessageReceived(PeerHandle, byte[])}.
+         * @return the current {@link NetworkSpecifierBuilder} builder, enabling chaining of builder
+         *         methods.
+         */
+        public @NonNull NetworkSpecifierBuilder setPeerHandle(@NonNull PeerHandle peerHandle) {
+            if (peerHandle == null) {
+                throw new IllegalArgumentException("Non-null peerHandle required");
+            }
+            mPeerHandle = peerHandle;
+            return this;
+        }
+
+        /**
+         * Configure the PSK Passphrase for the Wi-Fi Aware connection being requested. This method
+         * is optional - if not called, then an Open (unencrypted) connection will be created.
+         *
+         * @param pskPassphrase The (optional) passphrase to be used to encrypt the link.
+         * @return the current {@link NetworkSpecifierBuilder} builder, enabling chaining of builder
+         *         methods.
+         */
+        public @NonNull NetworkSpecifierBuilder setPskPassphrase(@NonNull String pskPassphrase) {
+            if (!WifiAwareUtils.validatePassphrase(pskPassphrase)) {
+                throw new IllegalArgumentException("Passphrase must meet length requirements");
+            }
+            mPskPassphrase = pskPassphrase;
+            return this;
+        }
+
+        /**
+         * Configure the PMK for the Wi-Fi Aware connection being requested. This method
+         * is optional - if not called, then an Open (unencrypted) connection will be created.
+         *
+         * @param pmk A PMK (pairwise master key, see IEEE 802.11i) specifying the key to use for
+         *            encrypting the data-path. Use the {@link #setPskPassphrase(String)} to
+         *            specify a Passphrase.
+         * @return the current {@link NetworkSpecifierBuilder} builder, enabling chaining of builder
+         *         methods.
+         * @hide
+         */
+        @SystemApi
+        public @NonNull NetworkSpecifierBuilder setPmk(@NonNull byte[] pmk) {
+            if (!WifiAwareUtils.validatePmk(pmk)) {
+                throw new IllegalArgumentException("PMK must 32 bytes");
+            }
+            mPmk = pmk;
+            return this;
+        }
+
+        /**
+         * Create a {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)}
+         * for a WiFi Aware connection (link) to the specified peer. The
+         * {@link android.net.NetworkRequest.Builder#addTransportType(int)} should be set to
+         * {@link android.net.NetworkCapabilities#TRANSPORT_WIFI_AWARE}.
+         * <p> The default builder constructor will initialize a NetworkSpecifier which requests an
+         * open (non-encrypted) link. To request an encrypted link use the
+         * {@link #setPskPassphrase(String)} builder method.
+         *
+         * @return A {@link NetworkSpecifier} to be used to construct
+         * {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)} to pass
+         * to {@link android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest,
+         * android.net.ConnectivityManager.NetworkCallback)}
+         * [or other varieties of that API].
+         */
+        public @NonNull NetworkSpecifier build() {
+            if (mDiscoverySession == null) {
+                throw new IllegalStateException("Null discovery session!?");
+            }
+            if (mPskPassphrase != null & mPmk != null) {
+                throw new IllegalStateException(
+                        "Can only specify a Passphrase or a PMK - not both!");
+            }
+
+            int role = mDiscoverySession instanceof SubscribeDiscoverySession
+                    ? WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_INITIATOR
+                    : WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_RESPONDER;
+
+            if (role == WIFI_AWARE_DATA_PATH_ROLE_INITIATOR && mPeerHandle == null) {
+                throw new IllegalStateException("Null peerHandle!?");
+            }
+
+            return new WifiAwareNetworkSpecifier(
+                    WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_IB, role,
+                    mDiscoverySession.mClientId, mDiscoverySession.mSessionId, mPeerHandle.peerId,
+                    null, mPmk, mPskPassphrase, Process.myUid());
+        }
+    }
 }
diff --git a/wifi/java/android/net/wifi/aware/WifiAwareNetworkInfo.java b/wifi/java/android/net/wifi/aware/WifiAwareNetworkInfo.java
new file mode 100644
index 0000000..0f29e08
--- /dev/null
+++ b/wifi/java/android/net/wifi/aware/WifiAwareNetworkInfo.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.aware;
+
+import android.annotation.Nullable;
+import android.net.NetworkCapabilities;
+import android.net.TransportInfo;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.net.Inet6Address;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/**
+ * Wi-Fi Aware-specific network information. The information can be extracted from the
+ * {@link android.net.NetworkCapabilities} of the network using
+ * {@link NetworkCapabilities#getTransportInfo()}.
+ * The {@link NetworkCapabilities} is provided by the connectivity service to apps, e.g. received
+ * through the
+ * {@link android.net.ConnectivityManager.NetworkCallback#onCapabilitiesChanged(android.net.Network,
+ * android.net.NetworkCapabilities)} callback.
+ * <p>
+ * The Wi-Fi Aware-specific network information include the peer's scoped link-local IPv6 address
+ * for the Wi-Fi Aware link. The scoped link-local IPv6 can then be used to create a
+ * {@link java.net.Socket} connection to the peer.
+ */
+public final class WifiAwareNetworkInfo implements TransportInfo, Parcelable {
+    private Inet6Address mIpv6Addr;
+
+    /** @hide */
+    public WifiAwareNetworkInfo(Inet6Address ipv6Addr) {
+        mIpv6Addr = ipv6Addr;
+    }
+
+    /**
+     * Get the scoped link-local IPv6 address of the Wi-Fi Aware peer (not of the local device!).
+     *
+     * @return An IPv6 address.
+     */
+    @Nullable
+    public Inet6Address getPeerIpv6Addr() {
+        return mIpv6Addr;
+    }
+
+    // parcelable methods
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeByteArray(mIpv6Addr.getAddress());
+        NetworkInterface ni = mIpv6Addr.getScopedInterface();
+        dest.writeString(ni == null ? null : ni.getName());
+    }
+
+    public static final Creator<WifiAwareNetworkInfo> CREATOR =
+            new Creator<WifiAwareNetworkInfo>() {
+                @Override
+                public WifiAwareNetworkInfo createFromParcel(Parcel in) {
+                    Inet6Address ipv6Addr;
+                    try {
+                        byte[] addr = in.createByteArray();
+                        String interfaceName = in.readString();
+                        NetworkInterface ni = null;
+                        if (interfaceName != null) {
+                            try {
+                                ni = NetworkInterface.getByName(interfaceName);
+                            } catch (SocketException e) {
+                                e.printStackTrace();
+                            }
+                        }
+                        ipv6Addr = Inet6Address.getByAddress(null, addr, ni);
+                    } catch (UnknownHostException e) {
+                        e.printStackTrace();
+                        return null;
+                    }
+
+                    return new WifiAwareNetworkInfo(ipv6Addr);
+                }
+
+                @Override
+                public WifiAwareNetworkInfo[] newArray(int size) {
+                    return new WifiAwareNetworkInfo[size];
+                }
+            };
+
+
+    // object methods
+
+    @Override
+    public String toString() {
+        return new StringBuilder("AwareNetworkInfo: IPv6=").append(mIpv6Addr).toString();
+    }
+
+    /** @hide */
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (!(obj instanceof WifiAwareNetworkInfo)) {
+            return false;
+        }
+
+        WifiAwareNetworkInfo lhs = (WifiAwareNetworkInfo) obj;
+        return Objects.equals(mIpv6Addr, lhs.mIpv6Addr);
+    }
+
+    /** @hide */
+    @Override
+    public int hashCode() {
+        return Objects.hash(mIpv6Addr);
+    }
+}
diff --git a/wifi/java/android/net/wifi/aware/WifiAwareSession.java b/wifi/java/android/net/wifi/aware/WifiAwareSession.java
index 3219653..5f8841c 100644
--- a/wifi/java/android/net/wifi/aware/WifiAwareSession.java
+++ b/wifi/java/android/net/wifi/aware/WifiAwareSession.java
@@ -213,7 +213,7 @@
      *     This API is targeted for applications which can obtain the peer MAC address using OOB
      *     (out-of-band) discovery. Aware discovery does not provide the MAC address of the peer -
      *     when using Aware discovery use the alternative network specifier method -
-     *     {@link DiscoverySession#createNetworkSpecifierOpen(PeerHandle)}.
+     *     {@link android.net.wifi.aware.WifiAwareManager.NetworkSpecifierBuilder}.
      * <p>
      * To set up an encrypted link use the
      * {@link #createNetworkSpecifierPassphrase(int, byte[], String)} API.
@@ -254,7 +254,7 @@
      *     This API is targeted for applications which can obtain the peer MAC address using OOB
      *     (out-of-band) discovery. Aware discovery does not provide the MAC address of the peer -
      *     when using Aware discovery use the alternative network specifier method -
-     *     {@link DiscoverySession#createNetworkSpecifierPassphrase(PeerHandle, String)}.
+     *     {@link android.net.wifi.aware.WifiAwareManager.NetworkSpecifierBuilder}.
      *
      * @param role  The role of this device:
      *              {@link WifiAwareManager#WIFI_AWARE_DATA_PATH_ROLE_INITIATOR} or
@@ -300,7 +300,7 @@
      *     This API is targeted for applications which can obtain the peer MAC address using OOB
      *     (out-of-band) discovery. Aware discovery does not provide the MAC address of the peer -
      *     when using Aware discovery use the alternative network specifier method -
-     *     {@link DiscoverySession#createNetworkSpecifierPassphrase(PeerHandle, String)}.
+     *     {@link android.net.wifi.aware.WifiAwareManager.NetworkSpecifierBuilder}.
      *
      * @param role  The role of this device:
      *              {@link WifiAwareManager#WIFI_AWARE_DATA_PATH_ROLE_INITIATOR} or
diff --git a/wifi/tests/src/android/net/wifi/aware/WifiAwareManagerTest.java b/wifi/tests/src/android/net/wifi/aware/WifiAwareManagerTest.java
index 272f727..45e1720 100644
--- a/wifi/tests/src/android/net/wifi/aware/WifiAwareManagerTest.java
+++ b/wifi/tests/src/android/net/wifi/aware/WifiAwareManagerTest.java
@@ -31,6 +31,7 @@
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.net.MacAddress;
 import android.net.wifi.RttManager;
 import android.os.Build;
 import android.os.Handler;
@@ -50,6 +51,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.net.Inet6Address;
+import java.net.UnknownHostException;
 import java.util.List;
 
 /**
@@ -105,7 +108,7 @@
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
 
-        mockApplicationInfo.targetSdkVersion = Build.VERSION_CODES.P;
+        mockApplicationInfo.targetSdkVersion = Build.VERSION_CODES.Q;
         when(mockPackageManager.getApplicationInfo(anyString(), anyInt())).thenReturn(
                 mockApplicationInfo);
         when(mockContext.getOpPackageName()).thenReturn("XXX");
@@ -915,6 +918,8 @@
         final ConfigRequest configRequest = new ConfigRequest.Builder().build();
         final PublishConfig publishConfig = new PublishConfig.Builder().build();
 
+        mockApplicationInfo.targetSdkVersion = Build.VERSION_CODES.P;
+
         ArgumentCaptor<WifiAwareSession> sessionCaptor = ArgumentCaptor.forClass(
                 WifiAwareSession.class);
         ArgumentCaptor<IWifiAwareEventCallback> clientProxyCallback = ArgumentCaptor
@@ -948,6 +953,9 @@
         WifiAwareNetworkSpecifier ns =
                 (WifiAwareNetworkSpecifier) publishSession.getValue().createNetworkSpecifierOpen(
                         peerHandle);
+        WifiAwareNetworkSpecifier nsb = (WifiAwareNetworkSpecifier) new WifiAwareManager
+                .NetworkSpecifierBuilder().setDiscoverySession(publishSession.getValue())
+                .setPeerHandle(peerHandle).build();
 
         // validate format
         collector.checkThat("role", role, equalTo(ns.role));
@@ -955,9 +963,18 @@
         collector.checkThat("session_id", sessionId, equalTo(ns.sessionId));
         collector.checkThat("peer_id", peerHandle.peerId, equalTo(ns.peerId));
 
+        collector.checkThat("role", role, equalTo(nsb.role));
+        collector.checkThat("client_id", clientId, equalTo(nsb.clientId));
+        collector.checkThat("session_id", sessionId, equalTo(nsb.sessionId));
+        collector.checkThat("peer_id", peerHandle.peerId, equalTo(nsb.peerId));
+
         // (4) request an encrypted (PMK) network specifier from the session
         ns = (WifiAwareNetworkSpecifier) publishSession.getValue().createNetworkSpecifierPmk(
                 peerHandle, pmk);
+        nsb =
+                (WifiAwareNetworkSpecifier) new WifiAwareManager.NetworkSpecifierBuilder()
+                        .setDiscoverySession(
+                        publishSession.getValue()).setPeerHandle(peerHandle).setPmk(pmk).build();
 
         // validate format
         collector.checkThat("role", role, equalTo(ns.role));
@@ -966,9 +983,18 @@
         collector.checkThat("peer_id", peerHandle.peerId, equalTo(ns.peerId));
         collector.checkThat("pmk", pmk , equalTo(ns.pmk));
 
+        collector.checkThat("role", role, equalTo(nsb.role));
+        collector.checkThat("client_id", clientId, equalTo(nsb.clientId));
+        collector.checkThat("session_id", sessionId, equalTo(nsb.sessionId));
+        collector.checkThat("peer_id", peerHandle.peerId, equalTo(nsb.peerId));
+        collector.checkThat("pmk", pmk , equalTo(nsb.pmk));
+
         // (5) request an encrypted (Passphrase) network specifier from the session
         ns = (WifiAwareNetworkSpecifier) publishSession.getValue().createNetworkSpecifierPassphrase(
                 peerHandle, passphrase);
+        nsb = (WifiAwareNetworkSpecifier) new WifiAwareManager.NetworkSpecifierBuilder()
+                .setDiscoverySession(publishSession.getValue()).setPeerHandle(peerHandle)
+                .setPskPassphrase(passphrase).build();
 
         // validate format
         collector.checkThat("role", role, equalTo(ns.role));
@@ -977,6 +1003,12 @@
         collector.checkThat("peer_id", peerHandle.peerId, equalTo(ns.peerId));
         collector.checkThat("passphrase", passphrase, equalTo(ns.passphrase));
 
+        collector.checkThat("role", role, equalTo(nsb.role));
+        collector.checkThat("client_id", clientId, equalTo(nsb.clientId));
+        collector.checkThat("session_id", sessionId, equalTo(nsb.sessionId));
+        collector.checkThat("peer_id", peerHandle.peerId, equalTo(nsb.peerId));
+        collector.checkThat("passphrase", passphrase, equalTo(nsb.passphrase));
+
         verifyNoMoreInteractions(mockCallback, mockSessionCallback, mockAwareService,
                 mockPublishSession, mockRttListener);
     }
@@ -1048,7 +1080,7 @@
      */
     @Test(expected = IllegalArgumentException.class)
     public void testNetworkSpecifierWithClientNullPmk() throws Exception {
-        executeNetworkSpecifierWithClient(new PeerHandle(123412), true, null, null);
+        executeNetworkSpecifierWithClient(new PeerHandle(123412), true, null, null, false);
     }
 
     /**
@@ -1056,7 +1088,7 @@
      */
     @Test(expected = IllegalArgumentException.class)
     public void testNetworkSpecifierWithClientIncorrectLengthPmk() throws Exception {
-        executeNetworkSpecifierWithClient(new PeerHandle(123412), true, PMK_INVALID, null);
+        executeNetworkSpecifierWithClient(new PeerHandle(123412), true, PMK_INVALID, null, false);
     }
 
     /**
@@ -1064,7 +1096,7 @@
      */
     @Test(expected = IllegalArgumentException.class)
     public void testNetworkSpecifierWithClientNullPassphrase() throws Exception {
-        executeNetworkSpecifierWithClient(new PeerHandle(123412), false, null, null);
+        executeNetworkSpecifierWithClient(new PeerHandle(123412), false, null, null, false);
     }
 
     /**
@@ -1073,7 +1105,7 @@
     @Test(expected = IllegalArgumentException.class)
     public void testNetworkSpecifierWithClientTooShortPassphrase() throws Exception {
         executeNetworkSpecifierWithClient(new PeerHandle(123412), false, null,
-                PASSPHRASE_TOO_SHORT);
+                PASSPHRASE_TOO_SHORT, false);
     }
 
     /**
@@ -1081,7 +1113,8 @@
      */
     @Test(expected = IllegalArgumentException.class)
     public void testNetworkSpecifierWithClientTooLongPassphrase() throws Exception {
-        executeNetworkSpecifierWithClient(new PeerHandle(123412), false, null, PASSPHRASE_TOO_LONG);
+        executeNetworkSpecifierWithClient(new PeerHandle(123412), false, null, PASSPHRASE_TOO_LONG,
+                false);
     }
 
     /**
@@ -1089,7 +1122,8 @@
      */
     @Test(expected = IllegalArgumentException.class)
     public void testNetworkSpecifierWithClientNullPeer() throws Exception {
-        executeNetworkSpecifierWithClient(null, false, null, PASSPHRASE_VALID);
+        mockApplicationInfo.targetSdkVersion = Build.VERSION_CODES.P;
+        executeNetworkSpecifierWithClient(null, false, null, PASSPHRASE_VALID, false);
     }
 
     /**
@@ -1098,11 +1132,75 @@
     @Test
     public void testNetworkSpecifierWithClientNullPeerLegacyApi() throws Exception {
         mockApplicationInfo.targetSdkVersion = Build.VERSION_CODES.O;
-        executeNetworkSpecifierWithClient(null, false, null, PASSPHRASE_VALID);
+        executeNetworkSpecifierWithClient(null, false, null, PASSPHRASE_VALID, false);
+    }
+
+    /**
+     * Validate that a null PMK triggers an exception.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testNetworkSpecifierWithClientNullPmkBuilder() throws Exception {
+        executeNetworkSpecifierWithClient(new PeerHandle(123412), true, null, null, true);
+    }
+
+    /**
+     * Validate that a non-32-bytes PMK triggers an exception.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testNetworkSpecifierWithClientIncorrectLengthPmkBuilder() throws Exception {
+        executeNetworkSpecifierWithClient(new PeerHandle(123412), true, PMK_INVALID, null, true);
+    }
+
+    /**
+     * Validate that a null Passphrase triggers an exception.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testNetworkSpecifierWithClientNullPassphraseBuilder() throws Exception {
+        executeNetworkSpecifierWithClient(new PeerHandle(123412), false, null, null, true);
+    }
+
+    /**
+     * Validate that a too short Passphrase triggers an exception.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testNetworkSpecifierWithClientTooShortPassphraseBuilder() throws Exception {
+        executeNetworkSpecifierWithClient(new PeerHandle(123412), false, null,
+                PASSPHRASE_TOO_SHORT, true);
+    }
+
+    /**
+     * Validate that a too long Passphrase triggers an exception.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testNetworkSpecifierWithClientTooLongPassphraseBuilder() throws Exception {
+        executeNetworkSpecifierWithClient(new PeerHandle(123412), false, null, PASSPHRASE_TOO_LONG,
+                true);
+    }
+
+    /**
+     * Validate that a null PeerHandle triggers an exception.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testNetworkSpecifierWithClientNullPeerBuilder() throws Exception {
+        executeNetworkSpecifierWithClient(null, false, null, PASSPHRASE_VALID, true);
+    }
+
+    /**
+     * Validate that a null PeerHandle does not trigger an exception for legacy API.
+     */
+    @Test
+    public void testNetworkSpecifierWithClientNullPeerLegacyApiBuilder() throws Exception {
+        mockApplicationInfo.targetSdkVersion = Build.VERSION_CODES.O;
+        executeNetworkSpecifierWithClient(null, false, null, PASSPHRASE_VALID, false);
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void testNetworkSpecifierDeprecatedOnNewApi() throws Exception {
+        executeNetworkSpecifierWithClient(null, false, null, PASSPHRASE_VALID, false);
     }
 
     private void executeNetworkSpecifierWithClient(PeerHandle peerHandle, boolean doPmk, byte[] pmk,
-            String passphrase) throws Exception {
+            String passphrase, boolean useBuilder) throws Exception {
         final int clientId = 4565;
         final int sessionId = 123;
         final ConfigRequest configRequest = new ConfigRequest.Builder().build();
@@ -1139,9 +1237,20 @@
 
         // (3) create network specifier
         if (doPmk) {
-            publishSession.getValue().createNetworkSpecifierPmk(peerHandle, pmk);
+            if (useBuilder) {
+                new WifiAwareManager.NetworkSpecifierBuilder().setDiscoverySession(
+                        publishSession.getValue()).setPeerHandle(peerHandle).setPmk(pmk).build();
+            } else {
+                publishSession.getValue().createNetworkSpecifierPmk(peerHandle, pmk);
+            }
         } else {
-            publishSession.getValue().createNetworkSpecifierPassphrase(peerHandle, passphrase);
+            if (useBuilder) {
+                new WifiAwareManager.NetworkSpecifierBuilder().setDiscoverySession(
+                        publishSession.getValue()).setPeerHandle(peerHandle).setPskPassphrase(
+                        passphrase).build();
+            } else {
+                publishSession.getValue().createNetworkSpecifierPassphrase(peerHandle, passphrase);
+            }
         }
     }
 
@@ -1245,4 +1354,31 @@
             sessionCaptor.getValue().createNetworkSpecifierPassphrase(role, someMac, passphrase);
         }
     }
+
+    // WifiAwareNetworkInfo tests
+
+    @Test
+    public void testWifiAwareNetworkCapabilitiesParcel() throws UnknownHostException {
+        final Inet6Address inet6 = MacAddress.fromString(
+                "11:22:33:44:55:66").getLinkLocalIpv6FromEui48Mac();
+        // note: dummy scope = 5
+        final Inet6Address inet6Scoped = Inet6Address.getByAddress(null, inet6.getAddress(), 5);
+
+        assertEquals(inet6Scoped.toString(), "/fe80::1322:33ff:fe44:5566%5");
+        WifiAwareNetworkInfo cap = new WifiAwareNetworkInfo(inet6Scoped);
+
+        Parcel parcelW = Parcel.obtain();
+        cap.writeToParcel(parcelW, 0);
+        byte[] bytes = parcelW.marshall();
+        parcelW.recycle();
+
+        Parcel parcelR = Parcel.obtain();
+        parcelR.unmarshall(bytes, 0, bytes.length);
+        parcelR.setDataPosition(0);
+        WifiAwareNetworkInfo rereadCap =
+                WifiAwareNetworkInfo.CREATOR.createFromParcel(parcelR);
+
+        assertEquals(cap.getPeerIpv6Addr().toString(), "/fe80::1322:33ff:fe44:5566%5");
+        assertEquals(cap.hashCode(), rereadCap.hashCode());
+    }
 }