Delaying jobs while coming out of doze

Foreground uids can run their jobs immediately given other constraints
are met. Other jobs will be delayed by 3 seconds when coming out of
doze to ensure imminent user tasks like screen-on can use resources.
Also added an API to allow apps to indicate their job is important to
the user enough that it is allowed to run when the app is in the
foreground or on the temp whitelist regardless of the dozing state of
the device.

Test: cts-tradefed run singleCommand cts-dev -m JobScheduler -t \
android.jobscheduler.cts.DeviceIdleJobsTest

Bug: 64291952
Bug: 64071030

Change-Id: Id52cb4386e683d4f8297e873b3a68c573e5be743
diff --git a/api/current.txt b/api/current.txt
index 06163cc..2ee590e 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -6884,6 +6884,7 @@
     method public android.app.job.JobInfo.Builder setClipData(android.content.ClipData, int);
     method public android.app.job.JobInfo.Builder setEstimatedNetworkBytes(long);
     method public android.app.job.JobInfo.Builder setExtras(android.os.PersistableBundle);
+    method public android.app.job.JobInfo.Builder setImportantWhileForeground(boolean);
     method public android.app.job.JobInfo.Builder setMinimumLatency(long);
     method public android.app.job.JobInfo.Builder setOverrideDeadline(long);
     method public android.app.job.JobInfo.Builder setPeriodic(long);
diff --git a/api/system-current.txt b/api/system-current.txt
index 808f222..026fd81 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -7327,6 +7327,7 @@
     method public android.app.job.JobInfo.Builder setClipData(android.content.ClipData, int);
     method public android.app.job.JobInfo.Builder setEstimatedNetworkBytes(long);
     method public android.app.job.JobInfo.Builder setExtras(android.os.PersistableBundle);
+    method public android.app.job.JobInfo.Builder setImportantWhileForeground(boolean);
     method public android.app.job.JobInfo.Builder setMinimumLatency(long);
     method public android.app.job.JobInfo.Builder setOverrideDeadline(long);
     method public android.app.job.JobInfo.Builder setPeriodic(long);
diff --git a/api/test-current.txt b/api/test-current.txt
index da59056..b3ad75d 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -6958,6 +6958,7 @@
     method public android.app.job.JobInfo.Builder setClipData(android.content.ClipData, int);
     method public android.app.job.JobInfo.Builder setEstimatedNetworkBytes(long);
     method public android.app.job.JobInfo.Builder setExtras(android.os.PersistableBundle);
+    method public android.app.job.JobInfo.Builder setImportantWhileForeground(boolean);
     method public android.app.job.JobInfo.Builder setMinimumLatency(long);
     method public android.app.job.JobInfo.Builder setOverrideDeadline(long);
     method public android.app.job.JobInfo.Builder setPeriodic(long);
diff --git a/core/java/android/app/job/JobInfo.java b/core/java/android/app/job/JobInfo.java
index 530d84b..7c40b4e 100644
--- a/core/java/android/app/job/JobInfo.java
+++ b/core/java/android/app/job/JobInfo.java
@@ -244,6 +244,13 @@
     public static final int FLAG_WILL_BE_FOREGROUND = 1 << 0;
 
     /**
+     * Allows this job to run despite doze restrictions as long as the app is in the foreground
+     * or on the temporary whitelist
+     * @hide
+     */
+    public static final int FLAG_IMPORTANT_WHILE_FOREGROUND = 1 << 1;
+
+    /**
      * @hide
      */
     public static final int CONSTRAINT_FLAG_CHARGING = 1 << 0;
@@ -1333,6 +1340,30 @@
         }
 
         /**
+         * Setting this to true indicates that this job is important while the scheduling app
+         * is in the foreground or on the temporary whitelist for background restrictions.
+         * This means that the system will relax doze restrictions on this job during this time.
+         *
+         * Apps should use this flag only for short jobs that are essential for the app to function
+         * properly in the foreground.
+         *
+         * Note that once the scheduling app is no longer whitelisted from background restrictions
+         * and in the background, or the job failed due to unsatisfied constraints,
+         * this job should be expected to behave like other jobs without this flag.
+         *
+         * @param importantWhileForeground whether to relax doze restrictions for this job when the
+         *                                 app is in the foreground. False by default.
+         */
+        public Builder setImportantWhileForeground(boolean importantWhileForeground) {
+            if (importantWhileForeground) {
+                mFlags |= FLAG_IMPORTANT_WHILE_FOREGROUND;
+            } else {
+                mFlags &= (~FLAG_IMPORTANT_WHILE_FOREGROUND);
+            }
+            return this;
+        }
+
+        /**
          * Set whether or not to persist this job across device reboots.
          *
          * @param isPersisted True to indicate that the job will be written to
@@ -1395,6 +1426,10 @@
                             "persisted job");
                 }
             }
+            if ((mFlags & FLAG_IMPORTANT_WHILE_FOREGROUND) != 0 && mHasEarlyConstraint) {
+                throw new IllegalArgumentException("An important while foreground job cannot "
+                        + "have a time delay");
+            }
             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" +
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index b9777ec..4a3becb 100644
--- a/services/core/java/com/android/server/job/JobSchedulerService.java
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -151,6 +151,7 @@
     StorageController mStorageController;
     /** Need directly for sending uid state changes */
     private BackgroundJobsController mBackgroundJobsController;
+    private DeviceIdleJobsController mDeviceIdleJobsController;
     /**
      * Queue of pending jobs. The JobServiceContext class will receive jobs from this list
      * when ready to execute them.
@@ -622,15 +623,24 @@
             if (disabled) {
                 cancelJobsForUid(uid, "uid gone");
             }
+            synchronized (mLock) {
+                mDeviceIdleJobsController.setUidActiveLocked(uid, false);
+            }
         }
 
         @Override public void onUidActive(int uid) throws RemoteException {
+            synchronized (mLock) {
+                mDeviceIdleJobsController.setUidActiveLocked(uid, true);
+            }
         }
 
         @Override public void onUidIdle(int uid, boolean disabled) {
             if (disabled) {
                 cancelJobsForUid(uid, "app uid idle");
             }
+            synchronized (mLock) {
+                mDeviceIdleJobsController.setUidActiveLocked(uid, false);
+            }
         }
 
         @Override public void onUidCachedChanged(int uid, boolean cached) {
@@ -939,11 +949,11 @@
         mControllers.add(mBatteryController);
         mStorageController = StorageController.get(this);
         mControllers.add(mStorageController);
-        mBackgroundJobsController = BackgroundJobsController.get(this);
-        mControllers.add(mBackgroundJobsController);
+        mControllers.add(BackgroundJobsController.get(this));
         mControllers.add(AppIdleController.get(this));
         mControllers.add(ContentObserverController.get(this));
-        mControllers.add(DeviceIdleJobsController.get(this));
+        mDeviceIdleJobsController = DeviceIdleJobsController.get(this);
+        mControllers.add(mDeviceIdleJobsController);
 
         // If the job store determined that it can't yet reschedule persisted jobs,
         // we need to start watching the clock.
diff --git a/services/core/java/com/android/server/job/JobStore.java b/services/core/java/com/android/server/job/JobStore.java
index 1af3b39..28b60e3 100644
--- a/services/core/java/com/android/server/job/JobStore.java
+++ b/services/core/java/com/android/server/job/JobStore.java
@@ -250,7 +250,7 @@
 
     /**
      * @param userHandle User for whom we are querying the list of jobs.
-     * @return A list of all the jobs scheduled by the provided user. Never null.
+     * @return A list of all the jobs scheduled for the provided user. Never null.
      */
     public List<JobStatus> getJobsByUser(int userHandle) {
         return mJobSet.getJobsByUser(userHandle);
@@ -287,6 +287,10 @@
         mJobSet.forEachJob(uid, functor);
     }
 
+    public void forEachJobForSourceUid(int sourceUid, JobStatusFunctor functor) {
+        mJobSet.forEachJobForSourceUid(sourceUid, functor);
+    }
+
     public interface JobStatusFunctor {
         public void process(JobStatus jobStatus);
     }
@@ -979,9 +983,12 @@
     static final class JobSet {
         // Key is the getUid() originator of the jobs in each sheaf
         private SparseArray<ArraySet<JobStatus>> mJobs;
+        // Same data but with the key as getSourceUid() of the jobs in each sheaf
+        private SparseArray<ArraySet<JobStatus>> mJobsPerSourceUid;
 
         public JobSet() {
             mJobs = new SparseArray<ArraySet<JobStatus>>();
+            mJobsPerSourceUid = new SparseArray<>();
         }
 
         public List<JobStatus> getJobsByUid(int uid) {
@@ -995,10 +1002,10 @@
 
         // By user, not by uid, so we need to traverse by key and check
         public List<JobStatus> getJobsByUser(int userId) {
-            ArrayList<JobStatus> result = new ArrayList<JobStatus>();
-            for (int i = mJobs.size() - 1; i >= 0; i--) {
-                if (UserHandle.getUserId(mJobs.keyAt(i)) == userId) {
-                    ArraySet<JobStatus> jobs = mJobs.valueAt(i);
+            final ArrayList<JobStatus> result = new ArrayList<JobStatus>();
+            for (int i = mJobsPerSourceUid.size() - 1; i >= 0; i--) {
+                if (UserHandle.getUserId(mJobsPerSourceUid.keyAt(i)) == userId) {
+                    final ArraySet<JobStatus> jobs = mJobsPerSourceUid.valueAt(i);
                     if (jobs != null) {
                         result.addAll(jobs);
                     }
@@ -1009,32 +1016,60 @@
 
         public boolean add(JobStatus job) {
             final int uid = job.getUid();
+            final int sourceUid = job.getSourceUid();
             ArraySet<JobStatus> jobs = mJobs.get(uid);
             if (jobs == null) {
                 jobs = new ArraySet<JobStatus>();
                 mJobs.put(uid, jobs);
             }
-            return jobs.add(job);
+            ArraySet<JobStatus> jobsForSourceUid = mJobsPerSourceUid.get(sourceUid);
+            if (jobsForSourceUid == null) {
+                jobsForSourceUid = new ArraySet<>();
+                mJobsPerSourceUid.put(sourceUid, jobsForSourceUid);
+            }
+            return jobs.add(job) && jobsForSourceUid.add(job);
         }
 
         public boolean remove(JobStatus job) {
             final int uid = job.getUid();
-            ArraySet<JobStatus> jobs = mJobs.get(uid);
-            boolean didRemove = (jobs != null) ? jobs.remove(job) : false;
-            if (didRemove && jobs.size() == 0) {
-                // no more jobs for this uid; let the now-empty set object be GC'd.
-                mJobs.remove(uid);
+            final ArraySet<JobStatus> jobs = mJobs.get(uid);
+            final int sourceUid = job.getSourceUid();
+            final ArraySet<JobStatus> jobsForSourceUid = mJobsPerSourceUid.get(sourceUid);
+            boolean didRemove = jobs != null && jobs.remove(job) && jobsForSourceUid.remove(job);
+            if (didRemove) {
+                if (jobs.size() == 0) {
+                    // no more jobs for this uid; let the now-empty set object be GC'd.
+                    mJobs.remove(uid);
+                }
+                if (jobsForSourceUid.size() == 0) {
+                    mJobsPerSourceUid.remove(sourceUid);
+                }
+                return true;
             }
-            return didRemove;
+            return false;
         }
 
-        // Remove the jobs all users not specified by the whitelist of user ids
+        /**
+         * Removes the jobs of all users not specified by the whitelist of user ids.
+         * The jobs scheduled by non existent users will not be removed if they were
+         */
         public void removeJobsOfNonUsers(int[] whitelist) {
-            for (int jobIndex = mJobs.size() - 1; jobIndex >= 0; jobIndex--) {
-                int jobUserId = UserHandle.getUserId(mJobs.keyAt(jobIndex));
-                // check if job's user id is not in the whitelist
+            for (int jobSetIndex = mJobsPerSourceUid.size() - 1; jobSetIndex >= 0; jobSetIndex--) {
+                final int jobUserId = UserHandle.getUserId(mJobsPerSourceUid.keyAt(jobSetIndex));
                 if (!ArrayUtils.contains(whitelist, jobUserId)) {
-                    mJobs.removeAt(jobIndex);
+                    mJobsPerSourceUid.removeAt(jobSetIndex);
+                }
+            }
+            for (int jobSetIndex = mJobs.size() - 1; jobSetIndex >= 0; jobSetIndex--) {
+                final ArraySet<JobStatus> jobsForUid = mJobs.valueAt(jobSetIndex);
+                for (int jobIndex = jobsForUid.size() - 1; jobIndex >= 0; jobIndex--) {
+                    final int jobUserId = jobsForUid.valueAt(jobIndex).getUserId();
+                    if (!ArrayUtils.contains(whitelist, jobUserId)) {
+                        jobsForUid.removeAt(jobIndex);
+                    }
+                }
+                if (jobsForUid.size() == 0) {
+                    mJobs.removeAt(jobSetIndex);
                 }
             }
         }
@@ -1077,6 +1112,7 @@
 
         public void clear() {
             mJobs.clear();
+            mJobsPerSourceUid.clear();
         }
 
         public int size() {
@@ -1112,8 +1148,17 @@
             }
         }
 
-        public void forEachJob(int uid, JobStatusFunctor functor) {
-            ArraySet<JobStatus> jobs = mJobs.get(uid);
+        public void forEachJob(int callingUid, JobStatusFunctor functor) {
+            ArraySet<JobStatus> jobs = mJobs.get(callingUid);
+            if (jobs != null) {
+                for (int i = jobs.size() - 1; i >= 0; i--) {
+                    functor.process(jobs.valueAt(i));
+                }
+            }
+        }
+
+        public void forEachJobForSourceUid(int sourceUid, JobStatusFunctor functor) {
+            final ArraySet<JobStatus> jobs = mJobsPerSourceUid.get(sourceUid);
             if (jobs != null) {
                 for (int i = jobs.size() - 1; i >= 0; i--) {
                     functor.process(jobs.valueAt(i));
diff --git a/services/core/java/com/android/server/job/controllers/DeviceIdleJobsController.java b/services/core/java/com/android/server/job/controllers/DeviceIdleJobsController.java
index 374ab43..b7eb9e0 100644
--- a/services/core/java/com/android/server/job/controllers/DeviceIdleJobsController.java
+++ b/services/core/java/com/android/server/job/controllers/DeviceIdleJobsController.java
@@ -16,14 +16,19 @@
 
 package com.android.server.job.controllers;
 
+import android.app.job.JobInfo;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
 import android.os.PowerManager;
 import android.os.UserHandle;
 import android.util.ArraySet;
 import android.util.Slog;
+import android.util.SparseBooleanArray;
 
 import com.android.internal.util.ArrayUtils;
 import com.android.server.DeviceIdleController;
@@ -42,11 +47,22 @@
 
     private static final String LOG_TAG = "DeviceIdleJobsController";
     private static final boolean LOG_DEBUG = false;
+    private static final long BACKGROUND_JOBS_DELAY = 3000;
+
+    static final int PROCESS_BACKGROUND_JOBS = 1;
 
     // Singleton factory
     private static Object sCreationLock = new Object();
     private static DeviceIdleJobsController sController;
 
+    /**
+     * These are jobs added with a special flag to indicate that they should be exempted from doze
+     * when the app is temp whitelisted or in the foreground.
+     */
+    private final ArraySet<JobStatus> mAllowInIdleJobs;
+    private final SparseBooleanArray mForegroundUids;
+    private final DeviceIdleUpdateFunctor mDeviceIdleUpdateFunctor;
+    private final DeviceIdleJobsDelayHandler mHandler;
     private final JobSchedulerService mJobSchedulerService;
     private final PowerManager mPowerManager;
     private final DeviceIdleController.LocalService mLocalDeviceIdleController;
@@ -57,14 +73,6 @@
     private boolean mDeviceIdleMode;
     private int[] mDeviceIdleWhitelistAppIds;
     private int[] mPowerSaveTempWhitelistAppIds;
-    // These jobs were added when the app was in temp whitelist, these should be exempted from doze
-    private final ArraySet<JobStatus> mTempWhitelistedJobs;
-
-    final JobStore.JobStatusFunctor mUpdateFunctor = new JobStore.JobStatusFunctor() {
-        @Override public void process(JobStatus jobStatus) {
-            updateTaskStateLocked(jobStatus);
-        }
-    };
 
     /**
      * Returns a singleton for the DeviceIdleJobsController
@@ -108,8 +116,8 @@
                                     + Arrays.toString(mPowerSaveTempWhitelistAppIds));
                         }
                         boolean changed = false;
-                        for (int i = 0; i < mTempWhitelistedJobs.size(); i ++) {
-                            changed |= updateTaskStateLocked(mTempWhitelistedJobs.valueAt(i));
+                        for (int i = 0; i < mAllowInIdleJobs.size(); i++) {
+                            changed |= updateTaskStateLocked(mAllowInIdleJobs.valueAt(i));
                         }
                         if (changed) {
                             mStateChangedListener.onControllerStateChanged();
@@ -125,6 +133,7 @@
         super(jobSchedulerService, context, lock);
 
         mJobSchedulerService = jobSchedulerService;
+        mHandler = new DeviceIdleJobsDelayHandler(context.getMainLooper());
         // Register for device idle mode changes
         mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
         mLocalDeviceIdleController =
@@ -132,7 +141,9 @@
         mDeviceIdleWhitelistAppIds = mLocalDeviceIdleController.getPowerSaveWhitelistUserAppIds();
         mPowerSaveTempWhitelistAppIds =
                 mLocalDeviceIdleController.getPowerSaveTempWhitelistAppIds();
-        mTempWhitelistedJobs = new ArraySet<>();
+        mDeviceIdleUpdateFunctor = new DeviceIdleUpdateFunctor();
+        mAllowInIdleJobs = new ArraySet<>();
+        mForegroundUids = new SparseBooleanArray();
         final IntentFilter filter = new IntentFilter();
         filter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
         filter.addAction(PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED);
@@ -150,7 +161,20 @@
             }
             mDeviceIdleMode = enabled;
             if (LOG_DEBUG) Slog.d(LOG_TAG, "mDeviceIdleMode=" + mDeviceIdleMode);
-            mJobSchedulerService.getJobStore().forEachJob(mUpdateFunctor);
+            if (enabled) {
+                mHandler.removeMessages(PROCESS_BACKGROUND_JOBS);
+                mJobSchedulerService.getJobStore().forEachJob(mDeviceIdleUpdateFunctor);
+            } else {
+                // When coming out of doze, process all foreground uids immediately, while others
+                // will be processed after a delay of 3 seconds.
+                for (int i = 0; i < mForegroundUids.size(); i++) {
+                    if (mForegroundUids.valueAt(i)) {
+                        mJobSchedulerService.getJobStore().forEachJobForSourceUid(
+                                mForegroundUids.keyAt(i), mDeviceIdleUpdateFunctor);
+                    }
+                }
+                mHandler.sendEmptyMessageDelayed(PROCESS_BACKGROUND_JOBS, BACKGROUND_JOBS_DELAY);
+            }
         }
         // Inform the job scheduler service about idle mode changes
         if (changed) {
@@ -159,11 +183,30 @@
     }
 
     /**
+     *  Called by jobscheduler service to report uid state changes between active and idle
+     */
+    public void setUidActiveLocked(int uid, boolean active) {
+        final boolean changed = (active != mForegroundUids.get(uid));
+        if (!changed) {
+            return;
+        }
+        if (LOG_DEBUG) {
+            Slog.d(LOG_TAG, "uid " + uid + " going " + (active ? "active" : "inactive"));
+        }
+        mForegroundUids.put(uid, active);
+        mDeviceIdleUpdateFunctor.mChanged = false;
+        mJobSchedulerService.getJobStore().forEachJobForSourceUid(uid, mDeviceIdleUpdateFunctor);
+        if (mDeviceIdleUpdateFunctor.mChanged) {
+            mStateChangedListener.onControllerStateChanged();
+        }
+    }
+
+    /**
      * Checks if the given job's scheduling app id exists in the device idle user whitelist.
      */
     boolean isWhitelistedLocked(JobStatus job) {
-        return ArrayUtils.contains(mDeviceIdleWhitelistAppIds,
-                UserHandle.getAppId(job.getSourceUid()));
+        return Arrays.binarySearch(mDeviceIdleWhitelistAppIds,
+                UserHandle.getAppId(job.getSourceUid())) >= 0;
     }
 
     /**
@@ -175,31 +218,33 @@
     }
 
     private boolean updateTaskStateLocked(JobStatus task) {
-        final boolean whitelisted = isWhitelistedLocked(task)
-                || (mTempWhitelistedJobs.contains(task) && isTempWhitelistedLocked(task));
-        final boolean enableTask = !mDeviceIdleMode || whitelisted;
+        final boolean allowInIdle = ((task.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0)
+                && (mForegroundUids.get(task.getSourceUid()) || isTempWhitelistedLocked(task));
+        final boolean whitelisted = isWhitelistedLocked(task);
+        final boolean enableTask = !mDeviceIdleMode || whitelisted || allowInIdle;
         return task.setDeviceNotDozingConstraintSatisfied(enableTask, whitelisted);
     }
 
     @Override
     public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
-        if (isTempWhitelistedLocked(jobStatus)) {
-            mTempWhitelistedJobs.add(jobStatus);
-            jobStatus.setDeviceNotDozingConstraintSatisfied(true, true);
-        } else {
-            updateTaskStateLocked(jobStatus);
+        if ((jobStatus.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) {
+            mAllowInIdleJobs.add(jobStatus);
         }
+        updateTaskStateLocked(jobStatus);
     }
 
     @Override
     public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
             boolean forUpdate) {
-        mTempWhitelistedJobs.remove(jobStatus);
+        if ((jobStatus.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) {
+            mAllowInIdleJobs.remove(jobStatus);
+        }
     }
 
     @Override
     public void dumpControllerStateLocked(final PrintWriter pw, final int filterUid) {
         pw.println("DeviceIdleJobsController");
+        pw.println("mDeviceIdleMode=" + mDeviceIdleMode);
         mJobSchedulerService.getJobStore().forEachJob(new JobStore.JobStatusFunctor() {
             @Override public void process(JobStatus jobStatus) {
                 if (!jobStatus.shouldDump(filterUid)) {
@@ -217,8 +262,42 @@
                 if (jobStatus.dozeWhitelisted) {
                     pw.print(" WHITELISTED");
                 }
+                if (mAllowInIdleJobs.contains(jobStatus)) {
+                    pw.print(" ALLOWED_IN_DOZE");
+                }
                 pw.println();
             }
         });
     }
+
+    final class DeviceIdleUpdateFunctor implements JobStore.JobStatusFunctor {
+        boolean mChanged;
+
+        @Override
+        public void process(JobStatus jobStatus) {
+            mChanged |= updateTaskStateLocked(jobStatus);
+        }
+    }
+
+    final class DeviceIdleJobsDelayHandler extends Handler {
+        public DeviceIdleJobsDelayHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case PROCESS_BACKGROUND_JOBS:
+                    // Just process all the jobs, the ones in foreground should already be running.
+                    synchronized (mLock) {
+                        mDeviceIdleUpdateFunctor.mChanged = false;
+                        mJobSchedulerService.getJobStore().forEachJob(mDeviceIdleUpdateFunctor);
+                        if (mDeviceIdleUpdateFunctor.mChanged) {
+                            mStateChangedListener.onControllerStateChanged();
+                        }
+                    }
+                    break;
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobActivity.java b/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobActivity.java
index 011817e..884ba70 100644
--- a/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobActivity.java
+++ b/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobActivity.java
@@ -27,12 +27,11 @@
 
 public class TestJobActivity extends Activity {
     private static final String TAG = TestJobActivity.class.getSimpleName();
-    public static final String EXTRA_JOB_ID_KEY =
-            "com.android.servicestests.apps.jobtestapp.extra.JOB_ID";
-    public static final String ACTION_START_JOB =
-            "com.android.servicestests.apps.jobtestapp.action.START_JOB";
-    public static final String ACTION_CANCEL_JOBS =
-            "com.android.servicestests.apps.jobtestapp.action.CANCEL_JOBS";
+    private static final String PACKAGE_NAME = "com.android.servicestests.apps.jobtestapp";
+
+    public static final String EXTRA_JOB_ID_KEY = PACKAGE_NAME + ".extra.JOB_ID";
+    public static final String ACTION_START_JOB = PACKAGE_NAME + ".action.START_JOB";
+    public static final String ACTION_CANCEL_JOBS = PACKAGE_NAME + ".action.CANCEL_JOBS";
     public static final int JOB_INITIAL_BACKOFF = 10_000;
     public static final int JOB_MINIMUM_LATENCY = 5_000;
 
@@ -59,6 +58,8 @@
                     Log.d(TAG, "Successfully scheduled job with id " + jobId);
                 }
                 break;
+            default:
+                Log.e(TAG, "Unknown action " + intent.getAction());
         }
         finish();
     }