Merge "Add new job scheduler constraints for "battery not low"."
diff --git a/api/current.txt b/api/current.txt
index df31f2c..22dbe7b 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -6718,6 +6718,7 @@
     method public android.app.job.JobInfo.TriggerContentUri[] getTriggerContentUris();
     method public boolean isPeriodic();
     method public boolean isPersisted();
+    method public boolean isRequireBatteryNotLow();
     method public boolean isRequireCharging();
     method public boolean isRequireDeviceIdle();
     method public void writeToParcel(android.os.Parcel, int);
@@ -6744,6 +6745,7 @@
     method public android.app.job.JobInfo.Builder setPeriodic(long, long);
     method public android.app.job.JobInfo.Builder setPersisted(boolean);
     method public android.app.job.JobInfo.Builder setRequiredNetworkType(int);
+    method public android.app.job.JobInfo.Builder setRequiresBatteryNotLow(boolean);
     method public android.app.job.JobInfo.Builder setRequiresCharging(boolean);
     method public android.app.job.JobInfo.Builder setRequiresDeviceIdle(boolean);
     method public android.app.job.JobInfo.Builder setTransientExtras(android.os.Bundle);
diff --git a/api/system-current.txt b/api/system-current.txt
index dcf62cd..daf8cef 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -7110,6 +7110,7 @@
     method public android.app.job.JobInfo.TriggerContentUri[] getTriggerContentUris();
     method public boolean isPeriodic();
     method public boolean isPersisted();
+    method public boolean isRequireBatteryNotLow();
     method public boolean isRequireCharging();
     method public boolean isRequireDeviceIdle();
     method public void writeToParcel(android.os.Parcel, int);
@@ -7136,6 +7137,7 @@
     method public android.app.job.JobInfo.Builder setPeriodic(long, long);
     method public android.app.job.JobInfo.Builder setPersisted(boolean);
     method public android.app.job.JobInfo.Builder setRequiredNetworkType(int);
+    method public android.app.job.JobInfo.Builder setRequiresBatteryNotLow(boolean);
     method public android.app.job.JobInfo.Builder setRequiresCharging(boolean);
     method public android.app.job.JobInfo.Builder setRequiresDeviceIdle(boolean);
     method public android.app.job.JobInfo.Builder setTransientExtras(android.os.Bundle);
diff --git a/api/test-current.txt b/api/test-current.txt
index d90b5f0..e9dddc3 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -6744,6 +6744,7 @@
     method public android.app.job.JobInfo.TriggerContentUri[] getTriggerContentUris();
     method public boolean isPeriodic();
     method public boolean isPersisted();
+    method public boolean isRequireBatteryNotLow();
     method public boolean isRequireCharging();
     method public boolean isRequireDeviceIdle();
     method public void writeToParcel(android.os.Parcel, int);
@@ -6770,6 +6771,7 @@
     method public android.app.job.JobInfo.Builder setPeriodic(long, long);
     method public android.app.job.JobInfo.Builder setPersisted(boolean);
     method public android.app.job.JobInfo.Builder setRequiredNetworkType(int);
+    method public android.app.job.JobInfo.Builder setRequiresBatteryNotLow(boolean);
     method public android.app.job.JobInfo.Builder setRequiresCharging(boolean);
     method public android.app.job.JobInfo.Builder setRequiresDeviceIdle(boolean);
     method public android.app.job.JobInfo.Builder setTransientExtras(android.os.Bundle);
diff --git a/core/java/android/app/job/JobInfo.java b/core/java/android/app/job/JobInfo.java
index 2d6b45d..3887556 100644
--- a/core/java/android/app/job/JobInfo.java
+++ b/core/java/android/app/job/JobInfo.java
@@ -174,12 +174,26 @@
      */
     public static final int FLAG_WILL_BE_FOREGROUND = 1 << 0;
 
+    /**
+     * @hide
+     */
+    public static final int CONSTRAINT_FLAG_CHARGING = 1 << 0;
+
+    /**
+     * @hide
+     */
+    public static final int CONSTRAINT_FLAG_BATTERY_NOT_LOW = 1 << 1;
+
+    /**
+     * @hide
+     */
+    public static final int CONSTRAINT_FLAG_DEVICE_IDLE = 1 << 2;
+
     private final int jobId;
     private final PersistableBundle extras;
     private final Bundle transientExtras;
     private final ComponentName service;
-    private final boolean requireCharging;
-    private final boolean requireDeviceIdle;
+    private final int constraintFlags;
     private final TriggerContentUri[] triggerContentUris;
     private final long triggerContentUpdateDelay;
     private final long triggerContentMaxDelay;
@@ -241,14 +255,28 @@
      * Whether this job needs the device to be plugged in.
      */
     public boolean isRequireCharging() {
-        return requireCharging;
+        return (constraintFlags & CONSTRAINT_FLAG_CHARGING) != 0;
+    }
+
+    /**
+     * Whether this job needs the device's battery level to not be at below the critical threshold.
+     */
+    public boolean isRequireBatteryNotLow() {
+        return (constraintFlags & CONSTRAINT_FLAG_BATTERY_NOT_LOW) != 0;
     }
 
     /**
      * Whether this job needs the device to be in an Idle maintenance window.
      */
     public boolean isRequireDeviceIdle() {
-        return requireDeviceIdle;
+        return (constraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0;
+    }
+
+    /**
+     * @hide
+     */
+    public int getConstraintFlags() {
+        return constraintFlags;
     }
 
     /**
@@ -376,8 +404,7 @@
         extras = in.readPersistableBundle();
         transientExtras = in.readBundle();
         service = in.readParcelable(null);
-        requireCharging = in.readInt() == 1;
-        requireDeviceIdle = in.readInt() == 1;
+        constraintFlags = in.readInt();
         triggerContentUris = in.createTypedArray(TriggerContentUri.CREATOR);
         triggerContentUpdateDelay = in.readLong();
         triggerContentMaxDelay = in.readLong();
@@ -401,8 +428,7 @@
         extras = b.mExtras.deepcopy();
         transientExtras = b.mTransientExtras.deepcopy();
         service = b.mJobService;
-        requireCharging = b.mRequiresCharging;
-        requireDeviceIdle = b.mRequiresDeviceIdle;
+        constraintFlags = b.mConstraintFlags;
         triggerContentUris = b.mTriggerContentUris != null
                 ? b.mTriggerContentUris.toArray(new TriggerContentUri[b.mTriggerContentUris.size()])
                 : null;
@@ -434,8 +460,7 @@
         out.writePersistableBundle(extras);
         out.writeBundle(transientExtras);
         out.writeParcelable(service, flags);
-        out.writeInt(requireCharging ? 1 : 0);
-        out.writeInt(requireDeviceIdle ? 1 : 0);
+        out.writeInt(constraintFlags);
         out.writeTypedArray(triggerContentUris, flags);
         out.writeLong(triggerContentUpdateDelay);
         out.writeLong(triggerContentMaxDelay);
@@ -563,8 +588,7 @@
         private int mPriority = PRIORITY_DEFAULT;
         private int mFlags;
         // Requirements.
-        private boolean mRequiresCharging;
-        private boolean mRequiresDeviceIdle;
+        private int mConstraintFlags;
         private int mNetworkType;
         private ArrayList<TriggerContentUri> mTriggerContentUris;
         private long mTriggerContentUpdateDelay = -1;
@@ -651,7 +675,21 @@
          * @param requiresCharging Whether or not the device is plugged in.
          */
         public Builder setRequiresCharging(boolean requiresCharging) {
-            mRequiresCharging = requiresCharging;
+            mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_CHARGING)
+                    | (requiresCharging ? CONSTRAINT_FLAG_CHARGING : 0);
+            return this;
+        }
+
+        /**
+         * Specify that to run this job, the device's battery level must not be low.
+         * This defaults to false.  If true, the job will only run when the battery level
+         * is not low, which is generally the point where the user is given a "low battery"
+         * warning.
+         * @param batteryNotLow Whether or not the device's battery level must not be low.
+         */
+        public Builder setRequiresBatteryNotLow(boolean batteryNotLow) {
+            mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_BATTERY_NOT_LOW)
+                    | (batteryNotLow ? CONSTRAINT_FLAG_BATTERY_NOT_LOW : 0);
             return this;
         }
 
@@ -666,7 +704,8 @@
          *                           window.
          */
         public Builder setRequiresDeviceIdle(boolean requiresDeviceIdle) {
-            mRequiresDeviceIdle = requiresDeviceIdle;
+            mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_DEVICE_IDLE)
+                    | (requiresDeviceIdle ? CONSTRAINT_FLAG_DEVICE_IDLE : 0);
             return this;
         }
 
@@ -816,8 +855,8 @@
          */
         public JobInfo build() {
             // Allow jobs with no constraints - What am I, a database?
-            if (!mHasEarlyConstraint && !mHasLateConstraint && !mRequiresCharging &&
-                    !mRequiresDeviceIdle && mNetworkType == NETWORK_TYPE_NONE &&
+            if (!mHasEarlyConstraint && !mHasLateConstraint && mConstraintFlags == 0 &&
+                    mNetworkType == NETWORK_TYPE_NONE &&
                     mTriggerContentUris == null) {
                 throw new IllegalArgumentException("You're trying to build a job with no " +
                         "constraints, this is not allowed.");
@@ -843,7 +882,7 @@
                 throw new IllegalArgumentException("Can't call setTransientExtras() on a " +
                         "persisted job");
             }
-            if (mBackoffPolicySet && mRequiresDeviceIdle) {
+            if (mBackoffPolicySet && (mConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) {
                 throw new IllegalArgumentException("An idle mode job will not respect any" +
                         " back-off policy, so calling setBackoffCriteria with" +
                         " setRequiresDeviceIdle is an error.");
diff --git a/core/java/android/os/BatteryManager.java b/core/java/android/os/BatteryManager.java
index 3a441c7..9ffe2fe 100644
--- a/core/java/android/os/BatteryManager.java
+++ b/core/java/android/os/BatteryManager.java
@@ -118,6 +118,13 @@
      */
      public static final String EXTRA_CHARGE_COUNTER = "charge_counter";
 
+    /**
+     * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}:
+     * Current int sequence number of the update.
+     * {@hide}
+     */
+    public static final String EXTRA_SEQUENCE = "seq";
+
     // values for "status" field in the ACTION_BATTERY_CHANGED Intent
     public static final int BATTERY_STATUS_UNKNOWN = Constants.BATTERY_STATUS_UNKNOWN;
     public static final int BATTERY_STATUS_CHARGING = Constants.BATTERY_STATUS_CHARGING;
diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java
index 6248cab..81f137e 100644
--- a/services/core/java/com/android/server/BatteryService.java
+++ b/services/core/java/com/android/server/BatteryService.java
@@ -130,6 +130,8 @@
     private int mLastMaxChargingVoltage;
     private int mLastChargeCounter;
 
+    private int mSequence = 1;
+
     private int mInvalidCharger;
     private int mLastInvalidCharger;
 
@@ -448,27 +450,29 @@
                 }
             }
 
-            sendIntentLocked();
+            mSequence++;
 
             // Separate broadcast is sent for power connected / not connected
             // since the standard intent will not wake any applications and some
             // applications may want to have smart behavior based on this.
             if (mPlugType != 0 && mLastPlugType == 0) {
+                final Intent statusIntent = new Intent(Intent.ACTION_POWER_CONNECTED);
+                statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+                statusIntent.putExtra(BatteryManager.EXTRA_SEQUENCE, mSequence);
                 mHandler.post(new Runnable() {
                     @Override
                     public void run() {
-                        Intent statusIntent = new Intent(Intent.ACTION_POWER_CONNECTED);
-                        statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
                         mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL);
                     }
                 });
             }
             else if (mPlugType == 0 && mLastPlugType != 0) {
+                final Intent statusIntent = new Intent(Intent.ACTION_POWER_DISCONNECTED);
+                statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+                statusIntent.putExtra(BatteryManager.EXTRA_SEQUENCE, mSequence);
                 mHandler.post(new Runnable() {
                     @Override
                     public void run() {
-                        Intent statusIntent = new Intent(Intent.ACTION_POWER_DISCONNECTED);
-                        statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
                         mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL);
                     }
                 });
@@ -476,26 +480,33 @@
 
             if (shouldSendBatteryLowLocked()) {
                 mSentLowBatteryBroadcast = true;
+                final Intent statusIntent = new Intent(Intent.ACTION_BATTERY_LOW);
+                statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+                statusIntent.putExtra(BatteryManager.EXTRA_SEQUENCE, mSequence);
                 mHandler.post(new Runnable() {
                     @Override
                     public void run() {
-                        Intent statusIntent = new Intent(Intent.ACTION_BATTERY_LOW);
-                        statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
                         mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL);
                     }
                 });
             } else if (mSentLowBatteryBroadcast && mLastBatteryLevel >= mLowBatteryCloseWarningLevel) {
                 mSentLowBatteryBroadcast = false;
+                final Intent statusIntent = new Intent(Intent.ACTION_BATTERY_OKAY);
+                statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+                statusIntent.putExtra(BatteryManager.EXTRA_SEQUENCE, mSequence);
                 mHandler.post(new Runnable() {
                     @Override
                     public void run() {
-                        Intent statusIntent = new Intent(Intent.ACTION_BATTERY_OKAY);
-                        statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
                         mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL);
                     }
                 });
             }
 
+            // We are doing this after sending the above broadcasts, so anything processing
+            // them will get the new sequence number at that point.  (See for example how testing
+            // of JobScheduler's BatteryController works.)
+            sendIntentLocked();
+
             // Update the battery LED
             mLed.updateLightsLocked();
 
@@ -527,6 +538,7 @@
 
         int icon = getIconLocked(mBatteryProps.batteryLevel);
 
+        intent.putExtra(BatteryManager.EXTRA_SEQUENCE, mSequence);
         intent.putExtra(BatteryManager.EXTRA_STATUS, mBatteryProps.batteryStatus);
         intent.putExtra(BatteryManager.EXTRA_HEALTH, mBatteryProps.batteryHealth);
         intent.putExtra(BatteryManager.EXTRA_PRESENT, mBatteryProps.batteryPresent);
@@ -666,12 +678,28 @@
         pw.println("Battery service (battery) commands:");
         pw.println("  help");
         pw.println("    Print this help text.");
-        pw.println("  set [ac|usb|wireless|status|level|invalid] <value>");
+        pw.println("  set [-f] [ac|usb|wireless|status|level|invalid] <value>");
         pw.println("    Force a battery property value, freezing battery state.");
-        pw.println("  unplug");
+        pw.println("    -f: force a battery change broadcast be sent, prints new sequence.");
+        pw.println("  unplug [-f]");
         pw.println("    Force battery unplugged, freezing battery state.");
-        pw.println("  reset");
+        pw.println("    -f: force a battery change broadcast be sent, prints new sequence.");
+        pw.println("  reset [-f]");
         pw.println("    Unfreeze battery state, returning to current hardware values.");
+        pw.println("    -f: force a battery change broadcast be sent, prints new sequence.");
+    }
+
+    static final int OPTION_FORCE_UPDATE = 1<<0;
+
+    int parseOptions(Shell shell) {
+        String opt;
+        int opts = 0;
+        while ((opt = shell.getNextOption()) != null) {
+            if ("-f".equals(opt)) {
+                opts |= OPTION_FORCE_UPDATE;
+            }
+        }
+        return opts;
     }
 
     int onShellCommand(Shell shell, String cmd) {
@@ -681,6 +709,7 @@
         PrintWriter pw = shell.getOutPrintWriter();
         switch (cmd) {
             case "unplug": {
+                int opts = parseOptions(shell);
                 getContext().enforceCallingOrSelfPermission(
                         android.Manifest.permission.DEVICE_POWER, null);
                 if (!mUpdatesStopped) {
@@ -692,12 +721,13 @@
                 long ident = Binder.clearCallingIdentity();
                 try {
                     mUpdatesStopped = true;
-                    processValuesLocked(false);
+                    processValuesFromShellLocked(pw, opts);
                 } finally {
                     Binder.restoreCallingIdentity(ident);
                 }
             } break;
             case "set": {
+                int opts = parseOptions(shell);
                 getContext().enforceCallingOrSelfPermission(
                         android.Manifest.permission.DEVICE_POWER, null);
                 final String key = shell.getNextArg();
@@ -745,7 +775,7 @@
                         long ident = Binder.clearCallingIdentity();
                         try {
                             mUpdatesStopped = true;
-                            processValuesLocked(false);
+                            processValuesFromShellLocked(pw, opts);
                         } finally {
                             Binder.restoreCallingIdentity(ident);
                         }
@@ -756,6 +786,7 @@
                 }
             } break;
             case "reset": {
+                int opts = parseOptions(shell);
                 getContext().enforceCallingOrSelfPermission(
                         android.Manifest.permission.DEVICE_POWER, null);
                 long ident = Binder.clearCallingIdentity();
@@ -763,7 +794,7 @@
                     if (mUpdatesStopped) {
                         mUpdatesStopped = false;
                         mBatteryProps.set(mLastBatteryProps);
-                        processValuesLocked(false);
+                        processValuesFromShellLocked(pw, opts);
                     }
                 } finally {
                     Binder.restoreCallingIdentity(ident);
@@ -775,6 +806,13 @@
         return 0;
     }
 
+    private void processValuesFromShellLocked(PrintWriter pw, int opts) {
+        processValuesLocked((opts & OPTION_FORCE_UPDATE) != 0);
+        if ((opts & OPTION_FORCE_UPDATE) != 0) {
+            pw.println(mSequence);
+        }
+    }
+
     private void dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args) {
         synchronized (mLock) {
             if (args == null || args.length == 0 || "-a".equals(args[0])) {
diff --git a/services/core/java/com/android/server/job/JobPackageTracker.java b/services/core/java/com/android/server/job/JobPackageTracker.java
index 82e2eb4..0a6d8a4 100644
--- a/services/core/java/com/android/server/job/JobPackageTracker.java
+++ b/services/core/java/com/android/server/job/JobPackageTracker.java
@@ -437,7 +437,7 @@
         for (int i=0; i<size; i++) {
             final int index = mEventIndices.indexOf(i);
             final int uid = mEventUids[index];
-            if (filterUid != -1 && filterUid != UserHandle.getAppId(filterUid)) {
+            if (filterUid != -1 && filterUid != UserHandle.getAppId(uid)) {
                 continue;
             }
             final int cmd = mEventCmds[index];
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index a748013..c973911 100644
--- a/services/core/java/com/android/server/job/JobSchedulerService.java
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -131,6 +131,8 @@
     final List<JobServiceContext> mActiveServices = new ArrayList<>();
     /** List of controllers that will notify this service of updates to jobs. */
     List<StateController> mControllers;
+    /** Need direct access to this for testing. */
+    BatteryController mBatteryController;
     /**
      * Queue of pending jobs. The JobServiceContext class will receive jobs from this list
      * when ready to execute them.
@@ -194,6 +196,7 @@
         // Key names stored in the settings value.
         private static final String KEY_MIN_IDLE_COUNT = "min_idle_count";
         private static final String KEY_MIN_CHARGING_COUNT = "min_charging_count";
+        private static final String KEY_MIN_BATTERY_NOT_LOW_COUNT = "min_battery_not_low_count";
         private static final String KEY_MIN_CONNECTIVITY_COUNT = "min_connectivity_count";
         private static final String KEY_MIN_CONTENT_COUNT = "min_content_count";
         private static final String KEY_MIN_READY_JOBS_COUNT = "min_ready_jobs_count";
@@ -207,6 +210,7 @@
 
         private static final int DEFAULT_MIN_IDLE_COUNT = 1;
         private static final int DEFAULT_MIN_CHARGING_COUNT = 1;
+        private static final int DEFAULT_MIN_BATTERY_NOT_LOW_COUNT = 1;
         private static final int DEFAULT_MIN_CONNECTIVITY_COUNT = 1;
         private static final int DEFAULT_MIN_CONTENT_COUNT = 1;
         private static final int DEFAULT_MIN_READY_JOBS_COUNT = 1;
@@ -229,6 +233,11 @@
          */
         int MIN_CHARGING_COUNT = DEFAULT_MIN_CHARGING_COUNT;
         /**
+         * Minimum # of "battery not low" jobs that must be ready in order to force the JMS to
+         * schedule things early.
+         */
+        int MIN_BATTERY_NOT_LOW_COUNT = DEFAULT_MIN_BATTERY_NOT_LOW_COUNT;
+        /**
          * Minimum # of connectivity jobs that must be ready in order to force the JMS to schedule
          * things early.  1 == Run connectivity jobs as soon as ready.
          */
@@ -312,6 +321,8 @@
                         DEFAULT_MIN_IDLE_COUNT);
                 MIN_CHARGING_COUNT = mParser.getInt(KEY_MIN_CHARGING_COUNT,
                         DEFAULT_MIN_CHARGING_COUNT);
+                MIN_BATTERY_NOT_LOW_COUNT = mParser.getInt(KEY_MIN_BATTERY_NOT_LOW_COUNT,
+                        DEFAULT_MIN_BATTERY_NOT_LOW_COUNT);
                 MIN_CONNECTIVITY_COUNT = mParser.getInt(KEY_MIN_CONNECTIVITY_COUNT,
                         DEFAULT_MIN_CONNECTIVITY_COUNT);
                 MIN_CONTENT_COUNT = mParser.getInt(KEY_MIN_CONTENT_COUNT,
@@ -356,6 +367,9 @@
             pw.print("    "); pw.print(KEY_MIN_CHARGING_COUNT); pw.print("=");
             pw.print(MIN_CHARGING_COUNT); pw.println();
 
+            pw.print("    "); pw.print(KEY_MIN_BATTERY_NOT_LOW_COUNT); pw.print("=");
+            pw.print(MIN_BATTERY_NOT_LOW_COUNT); pw.println();
+
             pw.print("    "); pw.print(KEY_MIN_CONNECTIVITY_COUNT); pw.print("=");
             pw.print(MIN_CONNECTIVITY_COUNT); pw.println();
 
@@ -785,7 +799,8 @@
         mControllers.add(ConnectivityController.get(this));
         mControllers.add(TimeController.get(this));
         mControllers.add(IdleController.get(this));
-        mControllers.add(BatteryController.get(this));
+        mBatteryController = BatteryController.get(this);
+        mControllers.add(mBatteryController);
         mControllers.add(AppIdleController.get(this));
         mControllers.add(ContentObserverController.get(this));
         mControllers.add(DeviceIdleJobsController.get(this));
@@ -1186,6 +1201,7 @@
          */
         class MaybeReadyJobQueueFunctor implements JobStatusFunctor {
             int chargingCount;
+            int batteryNotLowCount;
             int idleCount;
             int backoffCount;
             int connectivityCount;
@@ -1223,6 +1239,9 @@
                     if (job.hasChargingConstraint()) {
                         chargingCount++;
                     }
+                    if (job.hasBatteryNotLowConstraint()) {
+                        batteryNotLowCount++;
+                    }
                     if (job.hasContentTriggerConstraint()) {
                         contentCount++;
                     }
@@ -1241,6 +1260,7 @@
                         idleCount >= mConstants.MIN_IDLE_COUNT ||
                         connectivityCount >= mConstants.MIN_CONNECTIVITY_COUNT ||
                         chargingCount >= mConstants.MIN_CHARGING_COUNT ||
+                        batteryNotLowCount >= mConstants.MIN_BATTERY_NOT_LOW_COUNT ||
                         contentCount >= mConstants.MIN_CONTENT_COUNT ||
                         (runnableJobs != null
                                 && runnableJobs.size() >= mConstants.MIN_READY_JOBS_COUNT)) {
@@ -1264,6 +1284,7 @@
                 idleCount =  0;
                 backoffCount = 0;
                 connectivityCount = 0;
+                batteryNotLowCount = 0;
                 contentCount = 0;
                 runnableJobs = null;
             }
@@ -1764,6 +1785,34 @@
         return 0;
     }
 
+    void setMonitorBattery(boolean enabled) {
+        synchronized (mLock) {
+            if (mBatteryController != null) {
+                mBatteryController.getTracker().setMonitorBatteryLocked(enabled);
+            }
+        }
+    }
+
+    int getBatterySeq() {
+        synchronized (mLock) {
+            return mBatteryController != null ? mBatteryController.getTracker().getSeq() : -1;
+        }
+    }
+
+    boolean getBatteryCharging() {
+        synchronized (mLock) {
+            return mBatteryController != null
+                    ? mBatteryController.getTracker().isOnStablePower() : false;
+        }
+    }
+
+    boolean getBatteryNotLow() {
+        synchronized (mLock) {
+            return mBatteryController != null
+                    ? mBatteryController.getTracker().isBatteryNotLow() : false;
+        }
+    }
+
     private String printContextIdToJobMap(JobStatus[] map, String initial) {
         StringBuilder s = new StringBuilder(initial + ": ");
         for (int i=0; i<map.length; i++) {
diff --git a/services/core/java/com/android/server/job/JobSchedulerShellCommand.java b/services/core/java/com/android/server/job/JobSchedulerShellCommand.java
index 2d62c1c..ec23407 100644
--- a/services/core/java/com/android/server/job/JobSchedulerShellCommand.java
+++ b/services/core/java/com/android/server/job/JobSchedulerShellCommand.java
@@ -43,10 +43,19 @@
     public int onCommand(String cmd) {
         final PrintWriter pw = getOutPrintWriter();
         try {
-            if ("run".equals(cmd)) {
-                return runJob();
-            } else {
-                return handleDefaultCommands(cmd);
+            switch (cmd != null ? cmd : "") {
+                case "run":
+                    return runJob(pw);
+                case "monitor-battery":
+                    return runMonitorBattery(pw);
+                case "get-battery-seq":
+                    return runGetBatterySeq(pw);
+                case "get-battery-charging":
+                    return runGetBatteryCharging(pw);
+                case "get-battery-not-low":
+                    return runGetBatteryNotLow(pw);
+                default:
+                    return handleDefaultCommands(cmd);
             }
         } catch (Exception e) {
             pw.println("Exception: " + e);
@@ -54,20 +63,23 @@
         return -1;
     }
 
-    private int runJob() {
-        try {
-            final int uid = Binder.getCallingUid();
-            final int perm = mPM.checkUidPermission(
-                    "android.permission.CHANGE_APP_IDLE_STATE", uid);
-            if (perm != PackageManager.PERMISSION_GRANTED) {
-                throw new SecurityException("Uid " + uid
-                        + " not permitted to force scheduled jobs");
-            }
-        } catch (RemoteException e) {
-            // Can't happen
+    private void checkPermission(String operation) throws Exception {
+        final int uid = Binder.getCallingUid();
+        if (uid == 0) {
+            // Root can do anything.
+            return;
         }
+        final int perm = mPM.checkUidPermission(
+                "android.permission.CHANGE_APP_IDLE_STATE", uid);
+        if (perm != PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException("Uid " + uid
+                    + " not permitted to " + operation);
+        }
+    }
 
-        final PrintWriter pw = getOutPrintWriter();
+    private int runJob(PrintWriter pw) throws Exception {
+        checkPermission("force scheduled jobs");
+
         boolean force = false;
         int userId = UserHandle.USER_SYSTEM;
 
@@ -133,6 +145,42 @@
         return ret;
     }
 
+    private int runMonitorBattery(PrintWriter pw) throws Exception {
+        checkPermission("change battery monitoring");
+        String opt = getNextArgRequired();
+        boolean enabled;
+        if ("on".equals(opt)) {
+            enabled = true;
+        } else if ("off".equals(opt)) {
+            enabled = false;
+        } else {
+            getErrPrintWriter().println("Error: unknown option " + opt);
+            return 1;
+        }
+        mInternal.setMonitorBattery(enabled);
+        if (enabled) pw.println("Battery monitoring enabled");
+        else pw.println("Battery monitoring disabled");
+        return 0;
+    }
+
+    private int runGetBatterySeq(PrintWriter pw) {
+        int seq = mInternal.getBatterySeq();
+        pw.println(seq);
+        return 0;
+    }
+
+    private int runGetBatteryCharging(PrintWriter pw) {
+        boolean val = mInternal.getBatteryCharging();
+        pw.println(val);
+        return 0;
+    }
+
+    private int runGetBatteryNotLow(PrintWriter pw) {
+        boolean val = mInternal.getBatteryNotLow();
+        pw.println(val);
+        return 0;
+    }
+
     @Override
     public void onHelp() {
         final PrintWriter pw = getOutPrintWriter();
@@ -140,7 +188,6 @@
         pw.println("Job scheduler (jobscheduler) commands:");
         pw.println("  help");
         pw.println("    Print this help text.");
-        pw.println();
         pw.println("  run [-f | --force] [-u | --user USER_ID] PACKAGE JOB_ID");
         pw.println("    Trigger immediate execution of a specific scheduled job.");
         pw.println("    Options:");
@@ -148,6 +195,15 @@
         pw.println("         connectivity are not currently met");
         pw.println("      -u or --user: specify which user's job is to be run; the default is");
         pw.println("         the primary or system user");
+        pw.println("  monitor-battery [on|off]");
+        pw.println("    Control monitoring of all battery changes.  Off by default.  Turning");
+        pw.println("    on makes get-battery-seq useful.");
+        pw.println("  get-battery-seq");
+        pw.println("    Return the last battery update sequence number that was received.");
+        pw.println("  get-battery-charging");
+        pw.println("    Return whether the battery is currently considered to be charging.");
+        pw.println("  get-battery-not-low");
+        pw.println("    Return whether the battery is currently considered to not be low.");
         pw.println();
     }
 
diff --git a/services/core/java/com/android/server/job/JobStore.java b/services/core/java/com/android/server/job/JobStore.java
index dd3f85f..ccfc287 100644
--- a/services/core/java/com/android/server/job/JobStore.java
+++ b/services/core/java/com/android/server/job/JobStore.java
@@ -381,6 +381,9 @@
             if (jobStatus.hasChargingConstraint()) {
                 out.attribute(null, "charging", Boolean.toString(true));
             }
+            if (jobStatus.hasBatteryNotLowConstraint()) {
+                out.attribute(null, "battery-not-low", Boolean.toString(true));
+            }
             out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS);
         }
 
diff --git a/services/core/java/com/android/server/job/controllers/BatteryController.java b/services/core/java/com/android/server/job/controllers/BatteryController.java
index f6b8ef4..05527be 100644
--- a/services/core/java/com/android/server/job/controllers/BatteryController.java
+++ b/services/core/java/com/android/server/job/controllers/BatteryController.java
@@ -80,32 +80,38 @@
 
     @Override
     public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) {
-        final boolean isOnStablePower = mChargeTracker.isOnStablePower();
-        if (taskStatus.hasChargingConstraint()) {
+        if (taskStatus.hasPowerConstraint()) {
             mTrackedTasks.add(taskStatus);
-            taskStatus.setChargingConstraintSatisfied(isOnStablePower);
+            taskStatus.setChargingConstraintSatisfied(mChargeTracker.isOnStablePower());
+            taskStatus.setBatteryNotLowConstraintSatisfied(mChargeTracker.isBatteryNotLow());
         }
     }
 
     @Override
     public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) {
-        if (taskStatus.hasChargingConstraint()) {
+        if (taskStatus.hasPowerConstraint()) {
             mTrackedTasks.remove(taskStatus);
         }
     }
 
     private void maybeReportNewChargingState() {
         final boolean stablePower = mChargeTracker.isOnStablePower();
+        final boolean batteryNotLow = mChargeTracker.isBatteryNotLow();
         if (DEBUG) {
             Slog.d(TAG, "maybeReportNewChargingState: " + stablePower);
         }
         boolean reportChange = false;
         synchronized (mLock) {
-            for (JobStatus ts : mTrackedTasks) {
+            for (int i = mTrackedTasks.size() - 1; i >= 0; i--) {
+                final JobStatus ts = mTrackedTasks.get(i);
                 boolean previous = ts.setChargingConstraintSatisfied(stablePower);
                 if (previous != stablePower) {
                     reportChange = true;
                 }
+                previous = ts.setBatteryNotLowConstraintSatisfied(batteryNotLow);
+                if (previous != batteryNotLow) {
+                    reportChange = true;
+                }
             }
         }
         // Let the scheduler know that state has changed. This may or may not result in an
@@ -127,6 +133,10 @@
         private boolean mCharging;
         /** Keep track of whether the battery is charged enough that we want to do work. */
         private boolean mBatteryHealthy;
+        /** Sequence number of last broadcast. */
+        private int mLastBatterySeq = -1;
+
+        private BroadcastReceiver mMonitor;
 
         public ChargingTracker() {
         }
@@ -149,10 +159,42 @@
             mCharging = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY);
         }
 
-        boolean isOnStablePower() {
+        public void setMonitorBatteryLocked(boolean enabled) {
+            if (enabled) {
+                if (mMonitor == null) {
+                    mMonitor = new BroadcastReceiver() {
+                        @Override public void onReceive(Context context, Intent intent) {
+                            ChargingTracker.this.onReceive(context, intent);
+                        }
+                    };
+                    IntentFilter filter = new IntentFilter();
+                    filter.addAction(Intent.ACTION_BATTERY_CHANGED);
+                    mContext.registerReceiver(mMonitor, filter);
+                }
+            } else {
+                if (mMonitor != null) {
+                    mContext.unregisterReceiver(mMonitor);
+                    mMonitor = null;
+                }
+            }
+        }
+
+        public boolean isOnStablePower() {
             return mCharging && mBatteryHealthy;
         }
 
+        public boolean isBatteryNotLow() {
+            return mBatteryHealthy;
+        }
+
+        public boolean isMonitoring() {
+            return mMonitor != null;
+        }
+
+        public int getSeq() {
+            return mLastBatterySeq;
+        }
+
         @Override
         public void onReceive(Context context, Intent intent) {
             onReceiveInternal(intent);
@@ -191,13 +233,20 @@
                 mCharging = false;
                 maybeReportNewChargingState();
             }
+            mLastBatterySeq = intent.getIntExtra(BatteryManager.EXTRA_SEQUENCE, mLastBatterySeq);
         }
     }
 
     @Override
     public void dumpControllerStateLocked(PrintWriter pw, int filterUid) {
         pw.print("Battery: stable power = ");
-        pw.println(mChargeTracker.isOnStablePower());
+        pw.print(mChargeTracker.isOnStablePower());
+        pw.print(", not low = ");
+        pw.println(mChargeTracker.isBatteryNotLow());
+        if (mChargeTracker.isMonitoring()) {
+            pw.print("MONITORING: seq=");
+            pw.println(mChargeTracker.getSeq());
+        }
         pw.print("Tracking ");
         pw.print(mTrackedTasks.size());
         pw.println(":");
diff --git a/services/core/java/com/android/server/job/controllers/JobStatus.java b/services/core/java/com/android/server/job/controllers/JobStatus.java
index 4b39bf9..9a55fed 100644
--- a/services/core/java/com/android/server/job/controllers/JobStatus.java
+++ b/services/core/java/com/android/server/job/controllers/JobStatus.java
@@ -46,16 +46,17 @@
     public static final long NO_LATEST_RUNTIME = Long.MAX_VALUE;
     public static final long NO_EARLIEST_RUNTIME = 0L;
 
-    static final int CONSTRAINT_CHARGING = 1<<0;
-    static final int CONSTRAINT_TIMING_DELAY = 1<<1;
-    static final int CONSTRAINT_DEADLINE = 1<<2;
-    static final int CONSTRAINT_IDLE = 1<<3;
-    static final int CONSTRAINT_UNMETERED = 1<<4;
-    static final int CONSTRAINT_CONNECTIVITY = 1<<5;
-    static final int CONSTRAINT_APP_NOT_IDLE = 1<<6;
-    static final int CONSTRAINT_CONTENT_TRIGGER = 1<<7;
-    static final int CONSTRAINT_DEVICE_NOT_DOZING = 1<<8;
-    static final int CONSTRAINT_NOT_ROAMING = 1<<9;
+    static final int CONSTRAINT_CHARGING = JobInfo.CONSTRAINT_FLAG_CHARGING;
+    static final int CONSTRAINT_IDLE = JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE;
+    static final int CONSTRAINT_BATTERY_NOT_LOW = JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW;
+    static final int CONSTRAINT_TIMING_DELAY = 1<<31;
+    static final int CONSTRAINT_DEADLINE = 1<<30;
+    static final int CONSTRAINT_UNMETERED = 1<<29;
+    static final int CONSTRAINT_CONNECTIVITY = 1<<28;
+    static final int CONSTRAINT_APP_NOT_IDLE = 1<<27;
+    static final int CONSTRAINT_CONTENT_TRIGGER = 1<<26;
+    static final int CONSTRAINT_DEVICE_NOT_DOZING = 1<<25;
+    static final int CONSTRAINT_NOT_ROAMING = 1<<24;
 
     // Soft override: ignore constraints like time that don't affect API availability
     public static final int OVERRIDE_SOFT = 1;
@@ -163,7 +164,7 @@
         this.latestRunTimeElapsedMillis = latestRunTimeElapsedMillis;
         this.numFailures = numFailures;
 
-        int requiredConstraints = 0;
+        int requiredConstraints = job.getConstraintFlags();
         if (job.getNetworkType() == JobInfo.NETWORK_TYPE_ANY) {
             requiredConstraints |= CONSTRAINT_CONNECTIVITY;
         }
@@ -173,18 +174,12 @@
         if (job.getNetworkType() == JobInfo.NETWORK_TYPE_NOT_ROAMING) {
             requiredConstraints |= CONSTRAINT_NOT_ROAMING;
         }
-        if (job.isRequireCharging()) {
-            requiredConstraints |= CONSTRAINT_CHARGING;
-        }
         if (earliestRunTimeElapsedMillis != NO_EARLIEST_RUNTIME) {
             requiredConstraints |= CONSTRAINT_TIMING_DELAY;
         }
         if (latestRunTimeElapsedMillis != NO_LATEST_RUNTIME) {
             requiredConstraints |= CONSTRAINT_DEADLINE;
         }
-        if (job.isRequireDeviceIdle()) {
-            requiredConstraints |= CONSTRAINT_IDLE;
-        }
         if (job.getTriggerContentUris() != null) {
             requiredConstraints |= CONSTRAINT_CONTENT_TRIGGER;
         }
@@ -331,6 +326,14 @@
         return (requiredConstraints&CONSTRAINT_CHARGING) != 0;
     }
 
+    public boolean hasBatteryNotLowConstraint() {
+        return (requiredConstraints&CONSTRAINT_BATTERY_NOT_LOW) != 0;
+    }
+
+    public boolean hasPowerConstraint() {
+        return (requiredConstraints&(CONSTRAINT_CHARGING|CONSTRAINT_BATTERY_NOT_LOW)) != 0;
+    }
+
     public boolean hasTimingDelayConstraint() {
         return (requiredConstraints&CONSTRAINT_TIMING_DELAY) != 0;
     }
@@ -379,6 +382,10 @@
         return setConstraintSatisfied(CONSTRAINT_CHARGING, state);
     }
 
+    boolean setBatteryNotLowConstraintSatisfied(boolean state) {
+        return setConstraintSatisfied(CONSTRAINT_BATTERY_NOT_LOW, state);
+    }
+
     boolean setTimingDelayConstraintSatisfied(boolean state) {
         return setConstraintSatisfied(CONSTRAINT_TIMING_DELAY, state);
     }
@@ -453,13 +460,14 @@
     }
 
     static final int CONSTRAINTS_OF_INTEREST =
-            CONSTRAINT_CHARGING | CONSTRAINT_TIMING_DELAY |
+            CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW | CONSTRAINT_TIMING_DELAY |
             CONSTRAINT_CONNECTIVITY | CONSTRAINT_UNMETERED | CONSTRAINT_NOT_ROAMING |
             CONSTRAINT_IDLE | CONSTRAINT_CONTENT_TRIGGER;
 
     // Soft override covers all non-"functional" constraints
     static final int SOFT_OVERRIDE_CONSTRAINTS =
-            CONSTRAINT_CHARGING | CONSTRAINT_TIMING_DELAY | CONSTRAINT_IDLE;
+            CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW
+                    | CONSTRAINT_TIMING_DELAY | CONSTRAINT_IDLE;
 
     /**
      * @return Whether the constraints set on this job are satisfied.
@@ -495,6 +503,7 @@
                 + ",R=(" + formatRunTime(earliestRunTimeElapsedMillis, NO_EARLIEST_RUNTIME)
                 + "," + formatRunTime(latestRunTimeElapsedMillis, NO_LATEST_RUNTIME) + ")"
                 + ",N=" + job.getNetworkType() + ",C=" + job.isRequireCharging()
+                + ",BL=" + job.isRequireBatteryNotLow()
                 + ",I=" + job.isRequireDeviceIdle()
                 + ",U=" + (job.getTriggerContentUris() != null)
                 + ",F=" + numFailures + ",P=" + job.isPersisted()
@@ -550,6 +559,9 @@
         if ((constraints&CONSTRAINT_CHARGING) != 0) {
             pw.print(" CHARGING");
         }
+        if ((constraints& CONSTRAINT_BATTERY_NOT_LOW) != 0) {
+            pw.print(" BATTERY_NOT_LOW");
+        }
         if ((constraints&CONSTRAINT_TIMING_DELAY) != 0) {
             pw.print(" TIMING_DELAY");
         }
@@ -607,7 +619,8 @@
                 pw.println(Integer.toHexString(job.getFlags()));
             }
             pw.print(prefix); pw.print("  Requires: charging=");
-            pw.print(job.isRequireCharging()); pw.print(" deviceIdle=");
+            pw.print(job.isRequireCharging()); pw.print(" batteryNotLow=");
+            pw.print(job.isRequireBatteryNotLow()); pw.print(" deviceIdle=");
             pw.println(job.isRequireDeviceIdle());
             if (job.getTriggerContentUris() != null) {
                 pw.print(prefix); pw.println("  Trigger content URIs:");
diff --git a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
index f615bf3..33e1a16 100644
--- a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
@@ -10,7 +10,6 @@
 import android.test.AndroidTestCase;
 import android.test.RenamingDelegatingContext;
 import android.util.Log;
-import android.util.ArraySet;
 
 import com.android.server.job.JobStore.JobSet;
 import com.android.server.job.controllers.JobStatus;
@@ -278,6 +277,8 @@
 
         assertEquals("Invalid charging constraint.", first.isRequireCharging(),
                 second.isRequireCharging());
+        assertEquals("Invalid battery not low constraint.", first.isRequireBatteryNotLow(),
+                second.isRequireBatteryNotLow());
         assertEquals("Invalid idle constraint.", first.isRequireDeviceIdle(),
                 second.isRequireDeviceIdle());
         assertEquals("Invalid unmetered constraint.",