Restricting background apps from running jobs

Introducing a new app op which controls whether apps are allowed to run
jobs in the background. When the app op mode is set to ignored, jobs
will be delayed until the app is in the foreground. The same semantics
as background check for O apps will apply, which means power whitelisted
apps can still run jobs freely and apps will have some settle time after
going to background after which their jobs will be stopped.

Test:
Added AppOpsUpgradeTest for upgrading appops to inherit existing value of
OP_RUN_ANY_IN_BACKGROUND from OP_RUN_IN_BACKGROUND
Added backgroundRestrictionsTest for background jobs. To run the test:
mmm -j32 services/tests/servicestests/
adb install -r \
out/target/product/marlin/data/app/JobTestApp/JobTestApp.apk
adb install -r \
out/target/product/marlin/data/app/FrameworksServicesTests/FrameworksServicesTests.apk
adb  shell am instrument -e class
'com.android.server.job.BackgroundRestrictionsTest' -w
'com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner'

Bug: 63001625
Change-Id: I6eb01adb6cd2c1d0e7be4f7eca960f57ad9581bf
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index b331d84..4bd85ae 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -252,8 +252,10 @@
     public static final int OP_INSTANT_APP_START_FOREGROUND = 68;
     /** @hide Answer incoming phone calls */
     public static final int OP_ANSWER_PHONE_CALLS = 69;
+    /** @hide Run jobs when in background */
+    public static final int OP_RUN_ANY_IN_BACKGROUND = 70;
     /** @hide */
-    public static final int _NUM_OP = 70;
+    public static final int _NUM_OP = 71;
 
     /** Access to coarse location information. */
     public static final String OPSTR_COARSE_LOCATION = "android:coarse_location";
@@ -492,7 +494,8 @@
             OP_REQUEST_INSTALL_PACKAGES,
             OP_PICTURE_IN_PICTURE,
             OP_INSTANT_APP_START_FOREGROUND,
-            OP_ANSWER_PHONE_CALLS
+            OP_ANSWER_PHONE_CALLS,
+            OP_RUN_ANY_IN_BACKGROUND,
     };
 
     /**
@@ -570,6 +573,7 @@
             OPSTR_PICTURE_IN_PICTURE,
             OPSTR_INSTANT_APP_START_FOREGROUND,
             OPSTR_ANSWER_PHONE_CALLS,
+            null, // OP_RUN_ANY_IN_BACKGROUND
     };
 
     /**
@@ -647,6 +651,7 @@
             "PICTURE_IN_PICTURE",
             "INSTANT_APP_START_FOREGROUND",
             "ANSWER_PHONE_CALLS",
+            "RUN_ANY_IN_BACKGROUND",
     };
 
     /**
@@ -724,6 +729,7 @@
             null, // no permission for entering picture-in-picture on hide
             Manifest.permission.INSTANT_APP_FOREGROUND_SERVICE,
             Manifest.permission.ANSWER_PHONE_CALLS,
+            null, // no permission for OP_RUN_ANY_IN_BACKGROUND
     };
 
     /**
@@ -802,6 +808,7 @@
             null, // ENTER_PICTURE_IN_PICTURE_ON_HIDE
             null, // INSTANT_APP_START_FOREGROUND
             null, // ANSWER_PHONE_CALLS
+            null, // OP_RUN_ANY_IN_BACKGROUND
     };
 
     /**
@@ -879,6 +886,7 @@
             false, // ENTER_PICTURE_IN_PICTURE_ON_HIDE
             false, // INSTANT_APP_START_FOREGROUND
             false, // ANSWER_PHONE_CALLS
+            false, // OP_RUN_ANY_IN_BACKGROUND
     };
 
     /**
@@ -955,6 +963,7 @@
             AppOpsManager.MODE_ALLOWED,  // OP_PICTURE_IN_PICTURE
             AppOpsManager.MODE_DEFAULT,  // OP_INSTANT_APP_START_FOREGROUND
             AppOpsManager.MODE_ALLOWED, // ANSWER_PHONE_CALLS
+            AppOpsManager.MODE_ALLOWED,  // OP_RUN_ANY_IN_BACKGROUND
     };
 
     /**
@@ -1035,6 +1044,7 @@
             false, // OP_PICTURE_IN_PICTURE
             false,
             false, // ANSWER_PHONE_CALLS
+            false, // OP_RUN_ANY_IN_BACKGROUND
     };
 
     /**
diff --git a/services/core/java/com/android/server/AppOpsService.java b/services/core/java/com/android/server/AppOpsService.java
index 29f8a11..50b8df2 100644
--- a/services/core/java/com/android/server/AppOpsService.java
+++ b/services/core/java/com/android/server/AppOpsService.java
@@ -52,6 +52,7 @@
 import android.util.TimeUtils;
 import android.util.Xml;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IAppOpsCallback;
 import com.android.internal.app.IAppOpsService;
 import com.android.internal.os.Zygote;
@@ -87,6 +88,11 @@
     static final String TAG = "AppOps";
     static final boolean DEBUG = false;
 
+    private static final int NO_VERSION = -1;
+    /** Increment by one every time and add the corresponding upgrade logic in
+     *  {@link #upgradeLocked(int)} below. The first version was 1 */
+    private static final int CURRENT_VERSION = 1;
+
     // Write at most every 30 minutes.
     static final long WRITE_DELAY = DEBUG ? 1000 : 30*60*1000;
 
@@ -112,14 +118,16 @@
         }
     };
 
-    private final SparseArray<UidState> mUidStates = new SparseArray<>();
+    @VisibleForTesting
+    final SparseArray<UidState> mUidStates = new SparseArray<>();
 
     /*
      * These are app op restrictions imposed per user from various parties.
      */
     private final ArrayMap<IBinder, ClientRestrictionState> mOpUserRestrictions = new ArrayMap<>();
 
-    private static final class UidState {
+    @VisibleForTesting
+    static final class UidState {
         public final int uid;
         public ArrayMap<String, Ops> pkgOps;
         public SparseIntArray opModes;
@@ -1398,6 +1406,7 @@
     }
 
     void readState() {
+        int oldVersion = NO_VERSION;
         synchronized (mFile) {
             synchronized (this) {
                 FileInputStream stream;
@@ -1422,6 +1431,11 @@
                         throw new IllegalStateException("no start tag found");
                     }
 
+                    final String versionString = parser.getAttributeValue(null, "v");
+                    if (versionString != null) {
+                        oldVersion = Integer.parseInt(versionString);
+                    }
+
                     int outerDepth = parser.getDepth();
                     while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                             && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
@@ -1464,6 +1478,55 @@
                 }
             }
         }
+        synchronized (this) {
+            upgradeLocked(oldVersion);
+        }
+    }
+
+    private void upgradeRunAnyInBackgroundLocked() {
+        for (int i = 0; i < mUidStates.size(); i++) {
+            final UidState uidState = mUidStates.valueAt(i);
+            if (uidState == null) {
+                continue;
+            }
+            if (uidState.opModes != null) {
+                final int idx = uidState.opModes.indexOfKey(AppOpsManager.OP_RUN_IN_BACKGROUND);
+                if (idx >= 0) {
+                    uidState.opModes.put(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND,
+                            uidState.opModes.valueAt(idx));
+                }
+            }
+            if (uidState.pkgOps == null) {
+                continue;
+            }
+            for (int j = 0; j < uidState.pkgOps.size(); j++) {
+                Ops ops = uidState.pkgOps.valueAt(j);
+                if (ops != null) {
+                    final Op op = ops.get(AppOpsManager.OP_RUN_IN_BACKGROUND);
+                    if (op != null && op.mode != AppOpsManager.opToDefaultMode(op.op)) {
+                        final Op copy = new Op(op.uid, op.packageName,
+                                AppOpsManager.OP_RUN_ANY_IN_BACKGROUND);
+                        copy.mode = op.mode;
+                        ops.put(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, copy);
+                    }
+                }
+            }
+        }
+    }
+
+    private void upgradeLocked(int oldVersion) {
+        if (oldVersion >= CURRENT_VERSION) {
+            return;
+        }
+        Slog.d(TAG, "Upgrading app-ops xml from version " + oldVersion + " to " + CURRENT_VERSION);
+        switch (oldVersion) {
+            case NO_VERSION:
+                upgradeRunAnyInBackgroundLocked();
+                // fall through
+            case 1:
+                // for future upgrades
+        }
+        scheduleFastWriteLocked();
     }
 
     void readUidOps(XmlPullParser parser) throws NumberFormatException,
@@ -1613,6 +1676,7 @@
                 out.setOutput(stream, StandardCharsets.UTF_8.name());
                 out.startDocument(null, true);
                 out.startTag(null, "app-ops");
+                out.attribute(null, "v", String.valueOf(CURRENT_VERSION));
 
                 final int uidStateCount = mUidStates.size();
                 for (int i = 0; i < uidStateCount; i++) {
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index f885167..98d49d4 100644
--- a/services/core/java/com/android/server/job/JobSchedulerService.java
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -79,6 +79,7 @@
 import com.android.server.LocalServices;
 import com.android.server.job.JobStore.JobStatusFunctor;
 import com.android.server.job.controllers.AppIdleController;
+import com.android.server.job.controllers.BackgroundJobsController;
 import com.android.server.job.controllers.BatteryController;
 import com.android.server.job.controllers.ConnectivityController;
 import com.android.server.job.controllers.ContentObserverController;
@@ -139,6 +140,8 @@
     BatteryController mBatteryController;
     /** Need direct access to this for testing. */
     StorageController mStorageController;
+    /** Need directly for sending uid state changes */
+    private BackgroundJobsController mBackgroundJobsController;
     /**
      * Queue of pending jobs. The JobServiceContext class will receive jobs from this list
      * when ready to execute them.
@@ -224,6 +227,7 @@
         private static final String KEY_MAX_WORK_RESCHEDULE_COUNT = "max_work_reschedule_count";
         private static final String KEY_MIN_LINEAR_BACKOFF_TIME = "min_linear_backoff_time";
         private static final String KEY_MIN_EXP_BACKOFF_TIME = "min_exp_backoff_time";
+        private static final String KEY_BG_JOBS_RESTRICTED = "bg_jobs_restricted";
 
         private static final int DEFAULT_MIN_IDLE_COUNT = 1;
         private static final int DEFAULT_MIN_CHARGING_COUNT = 1;
@@ -239,6 +243,7 @@
         private static final int DEFAULT_BG_MODERATE_JOB_COUNT = 4;
         private static final int DEFAULT_BG_LOW_JOB_COUNT = 1;
         private static final int DEFAULT_BG_CRITICAL_JOB_COUNT = 1;
+        private static final boolean DEFAULT_BG_JOBS_RESTRICTED = false;
         private static final int DEFAULT_MAX_STANDARD_RESCHEDULE_COUNT = Integer.MAX_VALUE;
         private static final int DEFAULT_MAX_WORK_RESCHEDULE_COUNT = Integer.MAX_VALUE;
         private static final long DEFAULT_MIN_LINEAR_BACKOFF_TIME = JobInfo.MIN_BACKOFF_MILLIS;
@@ -332,6 +337,11 @@
          */
         long MIN_EXP_BACKOFF_TIME = DEFAULT_MIN_EXP_BACKOFF_TIME;
 
+        /**
+         * Runtime switch for throttling background jobs
+         */
+        boolean BACKGROUND_JOBS_RESTRICTED = DEFAULT_BG_JOBS_RESTRICTED;
+
         private ContentResolver mResolver;
         private final KeyValueListParser mParser = new KeyValueListParser(',');
 
@@ -410,6 +420,12 @@
                         DEFAULT_MIN_LINEAR_BACKOFF_TIME);
                 MIN_EXP_BACKOFF_TIME = mParser.getLong(KEY_MIN_EXP_BACKOFF_TIME,
                         DEFAULT_MIN_EXP_BACKOFF_TIME);
+                final boolean bgJobsRestricted = mParser.getBoolean(KEY_BG_JOBS_RESTRICTED,
+                        DEFAULT_BG_JOBS_RESTRICTED);
+                if (bgJobsRestricted != BACKGROUND_JOBS_RESTRICTED) {
+                    mBackgroundJobsController.enableRestrictionsLocked(
+                            BACKGROUND_JOBS_RESTRICTED = bgJobsRestricted);
+                }
             }
         }
 
@@ -469,6 +485,9 @@
 
             pw.print("    "); pw.print(KEY_MIN_EXP_BACKOFF_TIME); pw.print("=");
             pw.print(MIN_EXP_BACKOFF_TIME); pw.println();
+
+            pw.print("    "); pw.print(KEY_BG_JOBS_RESTRICTED); pw.print("=");
+            pw.print(BACKGROUND_JOBS_RESTRICTED); pw.println();
         }
     }
 
@@ -612,15 +631,24 @@
             if (disabled) {
                 cancelJobsForUid(uid, "uid gone");
             }
+            synchronized (mLock) {
+                mBackgroundJobsController.setUidActiveLocked(uid, false);
+            }
         }
 
         @Override public void onUidActive(int uid) throws RemoteException {
+            synchronized (mLock) {
+                mBackgroundJobsController.setUidActiveLocked(uid, true);
+            }
         }
 
         @Override public void onUidIdle(int uid, boolean disabled) {
             if (disabled) {
                 cancelJobsForUid(uid, "app uid idle");
             }
+            synchronized (mLock) {
+                mBackgroundJobsController.setUidActiveLocked(uid, false);
+            }
         }
 
         @Override public void onUidCachedChanged(int uid, boolean cached) {
@@ -916,6 +944,8 @@
         mControllers.add(mBatteryController);
         mStorageController = StorageController.get(this);
         mControllers.add(mStorageController);
+        mBackgroundJobsController = BackgroundJobsController.get(this);
+        mControllers.add(mBackgroundJobsController);
         mControllers.add(AppIdleController.get(this));
         mControllers.add(ContentObserverController.get(this));
         mControllers.add(DeviceIdleJobsController.get(this));
@@ -947,8 +977,8 @@
             try {
                 ActivityManager.getService().registerUidObserver(mUidObserver,
                         ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE
-                        | ActivityManager.UID_OBSERVER_IDLE, ActivityManager.PROCESS_STATE_UNKNOWN,
-                        null);
+                        | ActivityManager.UID_OBSERVER_IDLE | ActivityManager.UID_OBSERVER_ACTIVE,
+                        ActivityManager.PROCESS_STATE_UNKNOWN, null);
             } catch (RemoteException e) {
                 // ignored; both services live in system_server
             }
diff --git a/services/core/java/com/android/server/job/controllers/BackgroundJobsController.java b/services/core/java/com/android/server/job/controllers/BackgroundJobsController.java
new file mode 100644
index 0000000..ec50d6d
--- /dev/null
+++ b/services/core/java/com/android/server/job/controllers/BackgroundJobsController.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.job.controllers;
+
+import android.app.AppOpsManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.IDeviceIdleController;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.util.ArraySet;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+
+import com.android.internal.app.IAppOpsCallback;
+import com.android.internal.app.IAppOpsService;
+import com.android.internal.util.ArrayUtils;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.JobStore;
+
+import java.io.PrintWriter;
+
+public final class BackgroundJobsController extends StateController {
+
+    private static final String LOG_TAG = "BackgroundJobsController";
+    private static final boolean DEBUG = JobSchedulerService.DEBUG;
+
+    // Singleton factory
+    private static final Object sCreationLock = new Object();
+    private static volatile BackgroundJobsController sController;
+
+    /* Runtime switch to keep feature under wraps */
+    private boolean mEnableSwitch;
+    private final JobSchedulerService mJobSchedulerService;
+    private final IAppOpsService mAppOpsService;
+    private final IDeviceIdleController mDeviceIdleController;
+
+    private final SparseBooleanArray mForegroundUids;
+    private int[] mPowerWhitelistedAppIds;
+    private int[] mTempWhitelistedAppIds;
+    /**
+     * Only tracks jobs for which source package app op RUN_ANY_IN_BACKGROUND is not ALLOWED.
+     * Maps jobs to the sourceUid unlike the global {@link JobSchedulerService#mJobs JobStore}
+     * which uses callingUid.
+     */
+    private SparseArray<ArraySet<JobStatus>> mTrackedJobs;
+
+    public static BackgroundJobsController get(JobSchedulerService service) {
+        synchronized (sCreationLock) {
+            if (sController == null) {
+                sController = new BackgroundJobsController(service, service.getContext(),
+                        service.getLock());
+            }
+            return sController;
+        }
+    }
+
+    private BroadcastReceiver mDozeWhitelistReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            synchronized (mLock) {
+                try {
+                    switch (intent.getAction()) {
+                        case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED:
+                            mPowerWhitelistedAppIds = mDeviceIdleController.getAppIdWhitelist();
+                            break;
+                        case PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED:
+                            mTempWhitelistedAppIds = mDeviceIdleController.getAppIdTempWhitelist();
+                            break;
+                    }
+                } catch (RemoteException rexc) {
+                    Slog.e(LOG_TAG, "Device idle controller not reachable");
+                }
+                if (checkAllTrackedJobsLocked()) {
+                    mStateChangedListener.onControllerStateChanged();
+                }
+            }
+        }
+    };
+
+    private BackgroundJobsController(JobSchedulerService service, Context context, Object lock) {
+        super(service, context, lock);
+        mJobSchedulerService = service;
+        mAppOpsService = IAppOpsService.Stub.asInterface(
+                ServiceManager.getService(Context.APP_OPS_SERVICE));
+        mDeviceIdleController = IDeviceIdleController.Stub.asInterface(
+                ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
+
+        mForegroundUids = new SparseBooleanArray();
+        mTrackedJobs = new SparseArray<>();
+        try {
+            mAppOpsService.startWatchingMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, null,
+                    new AppOpsWatcher());
+            mPowerWhitelistedAppIds = mDeviceIdleController.getAppIdWhitelist();
+            mTempWhitelistedAppIds = mDeviceIdleController.getAppIdTempWhitelist();
+        } catch (RemoteException rexc) {
+            // Shouldn't happen as they are in the same process.
+            Slog.e(LOG_TAG, "AppOps or DeviceIdle service not reachable", rexc);
+        }
+        IntentFilter powerWhitelistFilter = new IntentFilter();
+        powerWhitelistFilter.addAction(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
+        powerWhitelistFilter.addAction(PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED);
+        context.registerReceiverAsUser(mDozeWhitelistReceiver, UserHandle.ALL, powerWhitelistFilter,
+                null, null);
+
+        mEnableSwitch = false;
+    }
+
+    @Override
+    public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
+        final int uid = jobStatus.getSourceUid();
+        final String packageName = jobStatus.getSourcePackageName();
+        try {
+            final int mode = mAppOpsService.checkOperation(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND,
+                    uid, packageName);
+            if (mode == AppOpsManager.MODE_ALLOWED) {
+                jobStatus.setBackgroundNotRestrictedConstraintSatisfied(true);
+                return;
+            }
+        } catch (RemoteException rexc) {
+            Slog.e(LOG_TAG, "Cannot reach app ops service", rexc);
+        }
+        jobStatus.setBackgroundNotRestrictedConstraintSatisfied(canRunJobLocked(uid));
+        startTrackingJobLocked(jobStatus);
+    }
+
+    @Override
+    public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
+            boolean forUpdate) {
+        stopTrackingJobLocked(jobStatus);
+    }
+
+    /* Called by JobSchedulerService 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 (DEBUG) {
+            Slog.d(LOG_TAG, "uid " + uid + " going to " + (active ? "fg" : "bg"));
+        }
+        if (active) {
+            mForegroundUids.put(uid, true);
+        } else {
+            mForegroundUids.delete(uid);
+        }
+        if (checkTrackedJobsForUidLocked(uid)) {
+            mStateChangedListener.onControllerStateChanged();
+        }
+    }
+
+    @Override
+    public void dumpControllerStateLocked(final PrintWriter pw, final int filterUid) {
+        pw.println("Background restrictions: global switch = " + mEnableSwitch);
+        pw.print("Foreground uids: [");
+        for (int i = 0; i < mForegroundUids.size(); i++) {
+            if (mForegroundUids.valueAt(i)) pw.print(mForegroundUids.keyAt(i) + " ");
+        }
+        pw.println("]");
+        mJobSchedulerService.getJobStore().forEachJob(new JobStore.JobStatusFunctor() {
+            @Override
+            public void process(JobStatus jobStatus) {
+                if (!jobStatus.shouldDump(filterUid)) {
+                    return;
+                }
+                final int uid = jobStatus.getSourceUid();
+                pw.print("  #");
+                jobStatus.printUniqueId(pw);
+                pw.print(" from ");
+                UserHandle.formatUid(pw, uid);
+                pw.print(mForegroundUids.get(uid) ? " foreground" : " background");
+                if (isWhitelistedLocked(uid)) {
+                    pw.print(", whitelisted");
+                }
+                pw.print(": ");
+                pw.print(jobStatus.getSourcePackageName());
+                pw.print(" [background restrictions");
+                final ArraySet<JobStatus> jobsForUid = mTrackedJobs.get(uid);
+                pw.print(jobsForUid != null && jobsForUid.contains(jobStatus) ? " on]" : " off]");
+                if ((jobStatus.satisfiedConstraints
+                        & JobStatus.CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) {
+                    pw.println(" RUNNABLE");
+                } else {
+                    pw.println(" WAITING");
+                }
+            }
+        });
+    }
+
+    public void enableRestrictionsLocked(boolean enable) {
+        mEnableSwitch = enable;
+        Slog.d(LOG_TAG, "Background jobs restrictions switch changed to " + mEnableSwitch);
+        if (checkAllTrackedJobsLocked()) {
+            mStateChangedListener.onControllerStateChanged();
+        }
+    }
+
+    void startTrackingJobLocked(JobStatus jobStatus) {
+        final int uid = jobStatus.getSourceUid();
+        ArraySet<JobStatus> jobsForUid = mTrackedJobs.get(uid);
+        if (jobsForUid == null) {
+            jobsForUid = new ArraySet<>();
+            mTrackedJobs.put(uid, jobsForUid);
+        }
+        jobsForUid.add(jobStatus);
+    }
+
+    void stopTrackingJobLocked(JobStatus jobStatus) {
+        final int uid = jobStatus.getSourceUid();
+        ArraySet<JobStatus> jobsForUid = mTrackedJobs.get(uid);
+        if (jobsForUid != null) {
+            jobsForUid.remove(jobStatus);
+        }
+    }
+
+    boolean checkAllTrackedJobsLocked() {
+        boolean changed = false;
+        for (int i = 0; i < mTrackedJobs.size(); i++) {
+            changed |= checkTrackedJobsForUidLocked(mTrackedJobs.keyAt(i));
+        }
+        return changed;
+    }
+
+    private boolean checkTrackedJobsForUidLocked(int uid) {
+        final ArraySet<JobStatus> jobsForUid = mTrackedJobs.get(uid);
+        boolean changed = false;
+        if (jobsForUid != null) {
+            for (int i = 0; i < jobsForUid.size(); i++) {
+                JobStatus jobStatus = jobsForUid.valueAt(i);
+                changed |= jobStatus.setBackgroundNotRestrictedConstraintSatisfied(
+                        canRunJobLocked(uid));
+            }
+        }
+        return changed;
+    }
+
+    boolean isWhitelistedLocked(int uid) {
+        return ArrayUtils.contains(mTempWhitelistedAppIds, UserHandle.getAppId(uid))
+                || ArrayUtils.contains(mPowerWhitelistedAppIds, UserHandle.getAppId(uid));
+    }
+
+    boolean canRunJobLocked(int uid) {
+        return !mEnableSwitch || mForegroundUids.get(uid) || isWhitelistedLocked(uid);
+    }
+
+    private final class AppOpsWatcher extends IAppOpsCallback.Stub {
+        @Override
+        public void opChanged(int op, int uid, String packageName) throws RemoteException {
+            synchronized (mLock) {
+                final int mode = mAppOpsService.checkOperation(op, uid, packageName);
+                if (DEBUG) {
+                    Slog.d(LOG_TAG,
+                            "Appop changed for " + uid + ", " + packageName + " to " + mode);
+                }
+                final boolean shouldTrack = (mode != AppOpsManager.MODE_ALLOWED);
+                UpdateTrackedJobsFunc updateTrackedJobs = new UpdateTrackedJobsFunc(uid,
+                        packageName, shouldTrack);
+                mJobSchedulerService.getJobStore().forEachJob(updateTrackedJobs);
+                if (updateTrackedJobs.mChanged) {
+                    mStateChangedListener.onControllerStateChanged();
+                }
+            }
+        }
+    }
+
+    private final class UpdateTrackedJobsFunc implements JobStore.JobStatusFunctor {
+        private final String mPackageName;
+        private final int mUid;
+        private final boolean mShouldTrack;
+        private boolean mChanged = false;
+
+        UpdateTrackedJobsFunc(int uid, String packageName, boolean shouldTrack) {
+            mUid = uid;
+            mPackageName = packageName;
+            mShouldTrack = shouldTrack;
+        }
+
+        @Override
+        public void process(JobStatus jobStatus) {
+            final String packageName = jobStatus.getSourcePackageName();
+            final int uid = jobStatus.getSourceUid();
+            if (mUid != uid || !mPackageName.equals(packageName)) {
+                return;
+            }
+            if (mShouldTrack) {
+                mChanged |= jobStatus.setBackgroundNotRestrictedConstraintSatisfied(
+                        canRunJobLocked(uid));
+                startTrackingJobLocked(jobStatus);
+            } else {
+                mChanged |= jobStatus.setBackgroundNotRestrictedConstraintSatisfied(true);
+                stopTrackingJobLocked(jobStatus);
+            }
+        }
+    }
+}
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 9658da7..de112be 100644
--- a/services/core/java/com/android/server/job/controllers/JobStatus.java
+++ b/services/core/java/com/android/server/job/controllers/JobStatus.java
@@ -67,6 +67,7 @@
     static final int CONSTRAINT_DEVICE_NOT_DOZING = 1<<25;
     static final int CONSTRAINT_NOT_ROAMING = 1<<24;
     static final int CONSTRAINT_METERED = 1<<23;
+    static final int CONSTRAINT_BACKGROUND_NOT_RESTRICTED = 1<<22;
 
     static final int CONNECTIVITY_MASK =
             CONSTRAINT_UNMETERED | CONSTRAINT_CONNECTIVITY |
@@ -665,6 +666,10 @@
         return setConstraintSatisfied(CONSTRAINT_DEVICE_NOT_DOZING, state);
     }
 
+    boolean setBackgroundNotRestrictedConstraintSatisfied(boolean state) {
+        return setConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED, state);
+    }
+
     boolean setConstraintSatisfied(int constraint, boolean state) {
         boolean old = (satisfiedConstraints&constraint) != 0;
         if (old == state) {
@@ -716,12 +721,16 @@
         // satisfied).
         // AppNotIdle implicit constraint must be satisfied
         // DeviceNotDozing implicit constraint must be satisfied
+        // NotRestrictedInBackground implicit constraint must be satisfied
         final boolean deadlineSatisfied = (!job.isPeriodic() && hasDeadlineConstraint()
                 && (satisfiedConstraints & CONSTRAINT_DEADLINE) != 0);
         final boolean notIdle = (satisfiedConstraints & CONSTRAINT_APP_NOT_IDLE) != 0;
         final boolean notDozing = (satisfiedConstraints & CONSTRAINT_DEVICE_NOT_DOZING) != 0
                 || (job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0;
-        return (isConstraintsSatisfied() || deadlineSatisfied) && notIdle && notDozing;
+        final boolean notRestrictedInBg =
+                (satisfiedConstraints & CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0;
+        return (isConstraintsSatisfied() || deadlineSatisfied) && notIdle && notDozing
+                && notRestrictedInBg;
     }
 
     static final int CONSTRAINTS_OF_INTEREST =
diff --git a/services/tests/servicestests/Android.mk b/services/tests/servicestests/Android.mk
index 507b483..baec956 100644
--- a/services/tests/servicestests/Android.mk
+++ b/services/tests/servicestests/Android.mk
@@ -30,6 +30,7 @@
 LOCAL_AIDL_INCLUDES := $(LOCAL_PATH)/aidl
 
 LOCAL_SRC_FILES += aidl/com/android/servicestests/aidl/INetworkStateObserver.aidl
+LOCAL_SRC_FILES += $(call all-java-files-under, test-apps/JobTestApp/src)
 
 LOCAL_JAVA_LIBRARIES := android.test.mock legacy-android-test
 
@@ -61,3 +62,5 @@
 LOCAL_STATIC_JAVA_LIBRARIES += ub-uiautomator
 
 include $(BUILD_PACKAGE)
+
+include $(call all-makefiles-under, $(LOCAL_PATH))
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index 59d205ec..4729d06 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -50,6 +50,11 @@
     <uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
     <uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE" />
     <uses-permission android:name="android.permission.DELETE_PACKAGES" />
+    <uses-permission android:name="android.permission.GET_APP_OPS_STATS" />
+    <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
+    <uses-permission android:name="android.permission.DEVICE_POWER" />
+    <uses-permission android:name="android.permission.FORCE_STOP_PACKAGES" />
+    <uses-permission android:name="android.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST" />
 
     <!-- Uses API introduced in O (26) -->
     <uses-sdk android:minSdkVersion="1"
diff --git a/services/tests/servicestests/AndroidTest.xml b/services/tests/servicestests/AndroidTest.xml
index 63f3b756..cea9a8b 100644
--- a/services/tests/servicestests/AndroidTest.xml
+++ b/services/tests/servicestests/AndroidTest.xml
@@ -16,6 +16,7 @@
 <configuration description="Runs Frameworks Services Tests.">
     <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
         <option name="test-file-name" value="FrameworksServicesTests.apk" />
+        <option name="test-file-name" value="JobTestApp.apk" />
     </target_preparer>
 
     <option name="test-suite-tag" value="apct" />
diff --git a/services/tests/servicestests/assets/AppOpsUpgradeTest/appops-unversioned.xml b/services/tests/servicestests/assets/AppOpsUpgradeTest/appops-unversioned.xml
new file mode 100644
index 0000000..a37d84f
--- /dev/null
+++ b/services/tests/servicestests/assets/AppOpsUpgradeTest/appops-unversioned.xml
@@ -0,0 +1,200 @@
+<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
+<app-ops>
+<uid n="1001">
+<op n="15" m="0" />
+</uid>
+<uid n="10052">
+<op n="63" m="1" />
+</uid>
+<pkg n="com.quicinc.cne.CNEService">
+<uid n="1000" p="true">
+<op n="59" t="1501535978641" pu="0" />
+<op n="60" t="1501535978641" pu="0" />
+</uid>
+</pkg>
+<pkg n="android">
+<uid n="1000" p="true">
+<op n="0" />
+<op n="3" t="1501537828283" d="22" />
+<op n="8" t="1501535987988" pu="0" pp="com.android.providers.calendar" />
+<op n="23" r="1501535979451" />
+<op n="40" t="1501621469584" d="1" />
+<op n="41" t="1501535980608" d="85615033" />
+<op n="61" t="1501557904487" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.android.server.telecom">
+<uid n="1000" p="true">
+<op n="6" t="1501535984350" pu="0" pp="com.android.providers.contacts" />
+</uid>
+</pkg>
+<pkg n="com.android.settings">
+<uid n="1000" p="true">
+<op n="59" t="1501536001265" pu="0" />
+<op n="60" t="1501536001265" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.android.providers.telephony">
+<uid n="1001" p="true">
+<op n="15" m="0" />
+</uid>
+</pkg>
+<pkg n="com.qualcomm.qti.rcsbootstraputil">
+<uid n="1001" p="false">
+<op n="59" t="1501535981233" pu="0" />
+<op n="60" t="1501535981233" pu="0" />
+<op n="63" t="1501536015379" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.android.phone">
+<uid n="1001" p="true">
+<op n="14" t="1501547602479" />
+<op n="15" m="0" t="1501535981903" pu="0" />
+<op n="40" t="1501621220685" d="4" />
+<op n="59" t="1501535978675" pu="0" />
+<op n="60" t="1501535978675" pu="0" />
+<op n="63" m="1" t="1501277487395" pu="0" />
+</uid>
+</pkg>
+<pkg n="audioserver">
+<uid n="1041" p="false">
+<op n="40" t="1501542152888" d="4" />
+</uid>
+</pkg>
+<pkg n="com.android.shell">
+<uid n="2000" p="true">
+<op n="59" t="1501535997600" pu="0" />
+<op n="60" t="1501535997600" pu="0" />
+<op n="63" t="1501535997600" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.google.android.apps.turbo">
+<uid n="10024" p="true">
+<op n="59" t="1501621079685" pu="0" />
+<op n="60" t="1501621079685" pu="0" />
+<op n="63" t="1501621079682" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.android.providers.downloads">
+<uid n="10027" p="true">
+<op n="59" t="1501601386341" />
+<op n="60" t="1501601375992" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.google.android.carriersetup">
+<uid n="10029" p="true">
+<op n="59" t="1501536001405" pu="0" />
+<op n="60" t="1501536001405" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.android.systemui">
+<uid n="10031" p="true">
+<op n="3" t="1501537825972" d="21" />
+<op n="40" t="1501619729317" d="7297" />
+<op n="59" t="1501535979651" pu="0" />
+<op n="60" t="1501535978058" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.android.chrome">
+<uid n="10096" p="false">
+<op n="23" r="1501537723291" />
+<op n="59" t="1501537615416" pu="0" />
+<op n="60" t="1501537615416" pu="0" />
+<op n="63" m="1" />
+</uid>
+</pkg>
+<pkg n="com.google.android.apps.maps">
+<uid n="10102" p="false">
+<op n="0" />
+<op n="1" t="1501620392477" pu="0" />
+<op n="59" t="1501620392609" pu="0" />
+<op n="60" t="1501620392609" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.google.android.syncadapters.contacts">
+<uid n="10109" p="false">
+<op n="4" t="1501535997715" pu="0" pp="com.android.providers.contacts" />
+<op n="59" t="1501535997265" pu="0" />
+<op n="60" t="1501535997265" pu="0" />
+<op n="63" t="1501535997589" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.google.android.youtube">
+<uid n="10111" p="false">
+<op n="59" t="1501620380957" pu="0" />
+<op n="60" t="1501620380957" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.google.android.deskclock">
+<uid n="10114" p="false">
+<op n="40" t="1501537682746" d="379" />
+<op n="59" t="1501537682098" pu="0" />
+<op n="60" t="1501537682098" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.google.android.apps.internal.betterbug">
+<uid n="10117" p="false">
+<op n="59" t="1501535989133" pu="0" />
+<op n="60" t="1501535989133" pu="0" />
+<op n="63" t="1501535989132" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.google.android.tts">
+<uid n="10118" p="false">
+<op n="59" t="1501193966186" pu="0" />
+<op n="60" t="1501193966186" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.google.android.apps.enterprise.dmagent">
+<uid n="10119" p="false">
+<op n="59" t="1501193986104" pu="0" />
+<op n="60" t="1501193986104" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.qualcomm.embms">
+<uid n="10122" p="false">
+<op n="59" t="1501535999723" pu="0" />
+<op n="60" t="1501535999723" pu="0" />
+<op n="63" t="1501535999550" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.qualcomm.qti.telephonyservice">
+<uid n="10123" p="false">
+<op n="59" t="1501535978649" pu="0" />
+<op n="60" t="1501535978649" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.qualcomm.ltebc_vzw">
+<uid n="10124" p="false">
+<op n="59" t="1501536001390" pu="0" />
+<op n="60" t="1501536001390" pu="0" />
+<op n="63" t="1501536000356" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.android.ramdump">
+<uid n="10125" p="false">
+<op n="59" t="1501536047490" pu="0" />
+<op n="60" t="1501536047490" pu="0" />
+<op n="63" m="1" />
+</uid>
+</pkg>
+<pkg n="com.android.nexuslogger">
+<uid n="10127" p="false">
+<op n="59" t="1501535985248" pu="0" />
+<op n="60" t="1501535985248" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.google.android.apps.multidevice.client">
+<uid n="10131" p="false">
+<op n="59" t="1501535991782" pu="0" />
+<op n="60" t="1501535991782" pu="0" />
+<op n="63" t="1501535991781" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.android.frameworks.servicestests">
+<uid n="10132" p="false">
+<op n="59" t="1501551739953" pu="0" />
+<op n="60" t="1501551739953" pu="0" />
+</uid>
+</pkg>
+</app-ops>
diff --git a/services/tests/servicestests/src/com/android/server/AppOpsUpgradeTest.java b/services/tests/servicestests/src/com/android/server/AppOpsUpgradeTest.java
new file mode 100644
index 0000000..cc89ae8
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/AppOpsUpgradeTest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.Xml;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Tests app ops version upgrades
+ */
+@RunWith(AndroidJUnit4.class)
+public class AppOpsUpgradeTest {
+    private static final String TAG = AppOpsUpgradeTest.class.getSimpleName();
+    private static final String APP_OPS_UNVERSIONED_ASSET_PATH =
+            "AppOpsUpgradeTest/appops-unversioned.xml";
+    private static final String APP_OPS_FILENAME = "appops-test.xml";
+    private static final int NON_DEFAULT_OPS_IN_FILE = 4;
+    private static final int CURRENT_VERSION = 1;
+
+    private File mAppOpsFile;
+    private Context mContext;
+    private Handler mHandler;
+
+    private void extractAppOpsFile() {
+        mAppOpsFile.getParentFile().mkdirs();
+        if (mAppOpsFile.exists()) {
+            mAppOpsFile.delete();
+        }
+        try (FileOutputStream out = new FileOutputStream(mAppOpsFile);
+             InputStream in = mContext.getAssets().open(APP_OPS_UNVERSIONED_ASSET_PATH,
+                     AssetManager.ACCESS_BUFFER)) {
+            byte[] buffer = new byte[4096];
+            int bytesRead;
+            while ((bytesRead = in.read(buffer)) >= 0) {
+                out.write(buffer, 0, bytesRead);
+            }
+            out.flush();
+            Log.d(TAG, "Successfully copied xml to " + mAppOpsFile.getAbsolutePath());
+        } catch (IOException exc) {
+            Log.e(TAG, "Exception while copying appops xml", exc);
+            fail();
+        }
+    }
+
+    private void assertSameModes(SparseArray<AppOpsService.UidState> uidStates, int op1, int op2) {
+        int numberOfNonDefaultOps = 0;
+        final int defaultModeOp1 = AppOpsManager.opToDefaultMode(op1);
+        final int defaultModeOp2 = AppOpsManager.opToDefaultMode(op2);
+        for(int i = 0; i < uidStates.size(); i++) {
+            final AppOpsService.UidState uidState = uidStates.valueAt(i);
+            if (uidState.opModes != null) {
+                final int uidMode1 = uidState.opModes.get(op1, defaultModeOp1);
+                final int uidMode2 = uidState.opModes.get(op2, defaultModeOp2);
+                assertEquals(uidMode1, uidMode2);
+                if (uidMode1 != defaultModeOp1) {
+                    numberOfNonDefaultOps++;
+                }
+            }
+            if (uidState.pkgOps == null) {
+                continue;
+            }
+            for (int j = 0; j < uidState.pkgOps.size(); j++) {
+                final AppOpsService.Ops ops = uidState.pkgOps.valueAt(j);
+                if (ops == null) {
+                    continue;
+                }
+                final AppOpsService.Op _op1 = ops.get(op1);
+                final AppOpsService.Op _op2 = ops.get(op2);
+                final int mode1 = (_op1 == null) ? defaultModeOp1 : _op1.mode;
+                final int mode2 = (_op2 == null) ? defaultModeOp2 : _op2.mode;
+                assertEquals(mode1, mode2);
+                if (mode1 != defaultModeOp1) {
+                    numberOfNonDefaultOps++;
+                }
+            }
+        }
+        assertEquals(numberOfNonDefaultOps, NON_DEFAULT_OPS_IN_FILE);
+    }
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mAppOpsFile = new File(mContext.getFilesDir(), APP_OPS_FILENAME);
+        extractAppOpsFile();
+        HandlerThread handlerThread = new HandlerThread(TAG);
+        handlerThread.start();
+        mHandler = new Handler(handlerThread.getLooper());
+    }
+
+    @Test
+    public void testUpgradeFromNoVersion() throws Exception {
+        AppOpsDataParser parser = new AppOpsDataParser(mAppOpsFile);
+        assertTrue(parser.parse());
+        assertEquals(AppOpsDataParser.NO_VERSION, parser.mVersion);
+        AppOpsService testService = new AppOpsService(mAppOpsFile, mHandler); // trigger upgrade
+        assertSameModes(testService.mUidStates, AppOpsManager.OP_RUN_IN_BACKGROUND,
+                AppOpsManager.OP_RUN_ANY_IN_BACKGROUND);
+        testService.mContext = mContext;
+        mHandler.removeCallbacks(testService.mWriteRunner);
+        testService.writeState();
+        assertTrue(parser.parse());
+        assertEquals(CURRENT_VERSION, parser.mVersion);
+    }
+
+    /**
+     * Class to parse data from the appops xml. Currently only parses and holds the version number.
+     * Other fields may be added as and when required for testing.
+     */
+    private static final class AppOpsDataParser {
+        static final int NO_VERSION = -1;
+        int mVersion;
+        private File mFile;
+
+        AppOpsDataParser(File file) {
+            mFile = file;
+            mVersion = NO_VERSION;
+        }
+
+        boolean parse() {
+            try (FileInputStream stream = new FileInputStream(mFile)) {
+                XmlPullParser parser = Xml.newPullParser();
+                parser.setInput(stream, StandardCharsets.UTF_8.name());
+                int type;
+                while ((type = parser.next()) != XmlPullParser.START_TAG
+                        && type != XmlPullParser.END_DOCUMENT) {
+                    ;
+                }
+                if (type != XmlPullParser.START_TAG) {
+                    throw new IllegalStateException("no start tag found");
+                }
+                final String versionString = parser.getAttributeValue(null, "v");
+                if (versionString != null) {
+                    mVersion = Integer.parseInt(versionString);
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "Failed while parsing test appops xml", e);
+                return false;
+            }
+            return true;
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java b/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java
new file mode 100644
index 0000000..70d2274
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.job;
+
+import static com.android.servicestests.apps.jobtestapp.TestJobService.ACTION_JOB_STARTED;
+import static com.android.servicestests.apps.jobtestapp.TestJobService.ACTION_JOB_STOPPED;
+import static com.android.servicestests.apps.jobtestapp.TestJobService.JOB_PARAMS_EXTRA_KEY;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.app.ActivityManager;
+import android.app.AppOpsManager;
+import android.app.IActivityManager;
+import android.app.job.JobParameters;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.os.IDeviceIdleController;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Log;
+
+import com.android.servicestests.apps.jobtestapp.TestJobActivity;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * TODO: Also add a test for temp power whitelist
+ * Tests that background restrictions on jobs work as expected.
+ * This test requires test-apps/JobTestApp to be installed on the device.
+ * To run this test from root of checkout:
+ * <pre>
+ *  mmm -j32 frameworks/base/services/tests/servicestests/
+ *  adb install out/target/product/marlin/data/app/JobTestApp/JobTestApp.apk
+ *  adb install out/target/product/marlin/data/app/FrameworksServicesTests/FrameworksServicesTests.apk
+ *  adb  shell am instrument -e class 'com.android.server.job.BackgroundRestrictionsTest' -w \
+ *  'com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner'
+ * </pre>
+ */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class BackgroundRestrictionsTest {
+    private static final String TAG = BackgroundRestrictionsTest.class.getSimpleName();
+    private static final String TEST_APP_PACKAGE = "com.android.servicestests.apps.jobtestapp";
+    private static final String TEST_APP_ACTIVITY = TEST_APP_PACKAGE + ".TestJobActivity";
+    private static final long POLL_INTERVAL = 2000;
+    private static final long DEFAULT_WAIT_TIMEOUT = 5000;
+
+    private Context mContext;
+    private AppOpsManager mAppOpsManager;
+    private IDeviceIdleController mDeviceIdleController;
+    private IActivityManager mIActivityManager;
+    private int mTestJobId;
+    private int mTestPackageUid;
+    /* accesses must be synchronized on itself */
+    private final TestJobStatus mTestJobStatus = new TestJobStatus();
+    private final BroadcastReceiver mJobStateChangeReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final JobParameters params = intent.getParcelableExtra(JOB_PARAMS_EXTRA_KEY);
+            Log.d(TAG, "Received action " + intent.getAction());
+            synchronized (mTestJobStatus) {
+                switch (intent.getAction()) {
+                    case ACTION_JOB_STARTED:
+                        mTestJobStatus.running = true;
+                        mTestJobStatus.jobId = params.getJobId();
+                        mTestJobStatus.stopReason = JobParameters.REASON_CANCELED;
+                        break;
+                    case ACTION_JOB_STOPPED:
+                        mTestJobStatus.running = false;
+                        mTestJobStatus.jobId = params.getJobId();
+                        mTestJobStatus.stopReason = params.getStopReason();
+                        break;
+                }
+            }
+        }
+    };
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
+        mDeviceIdleController = IDeviceIdleController.Stub.asInterface(
+                ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
+        mIActivityManager = ActivityManager.getService();
+        mTestPackageUid = mContext.getPackageManager().getPackageUid(TEST_APP_PACKAGE, 0);
+        mTestJobId = (int) (SystemClock.uptimeMillis() / 1000);
+        mTestJobStatus.reset();
+        final IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(ACTION_JOB_STARTED);
+        intentFilter.addAction(ACTION_JOB_STOPPED);
+        mContext.registerReceiver(mJobStateChangeReceiver, intentFilter);
+        setGlobalSwitch(true);
+        setAppOpsModeAllowed(true);
+        setPowerWhiteListed(false);
+    }
+
+    private void scheduleAndAssertJobStarted() throws Exception {
+        final Intent scheduleJobIntent = new Intent(TestJobActivity.ACTION_START_JOB);
+        scheduleJobIntent.putExtra(TestJobActivity.EXTRA_JOB_ID_KEY, mTestJobId);
+        scheduleJobIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        scheduleJobIntent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_ACTIVITY));
+        mContext.startActivity(scheduleJobIntent);
+        Thread.sleep(TestJobActivity.JOB_MINIMUM_LATENCY);
+        assertTrue("Job did not start after scheduling", awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+    }
+
+    @Test
+    public void testGlobalSwitch() throws Exception {
+        setGlobalSwitch(false); // Job should not stop now.
+        scheduleAndAssertJobStarted();
+        setAppOpsModeAllowed(false);
+        mIActivityManager.makePackageIdle(TEST_APP_PACKAGE, UserHandle.USER_CURRENT);
+        assertFalse("Job stopped even when feature switch is off",
+                awaitJobStop(DEFAULT_WAIT_TIMEOUT));
+    }
+
+    @Test
+    public void testPowerWhiteList() throws Exception {
+        scheduleAndAssertJobStarted();
+        setAppOpsModeAllowed(false);
+        mIActivityManager.makePackageIdle(TEST_APP_PACKAGE, UserHandle.USER_CURRENT);
+        assertTrue("Job did not stop after making idle", awaitJobStop(DEFAULT_WAIT_TIMEOUT));
+        setPowerWhiteListed(true);
+        Thread.sleep(TestJobActivity.JOB_INITIAL_BACKOFF);
+        assertTrue("Job did not start after adding to power whitelist",
+                awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+        setPowerWhiteListed(false);
+        assertTrue("Job did not stop after removing from power whitelist",
+                awaitJobStop(DEFAULT_WAIT_TIMEOUT));
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        Intent cancelJobsIntent = new Intent(TestJobActivity.ACTION_CANCEL_JOBS);
+        cancelJobsIntent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_ACTIVITY));
+        cancelJobsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        mContext.startActivity(cancelJobsIntent);
+        mContext.unregisterReceiver(mJobStateChangeReceiver);
+        setGlobalSwitch(false);
+        setAppOpsModeAllowed(true);
+        setPowerWhiteListed(false);
+    }
+
+    private void setGlobalSwitch(boolean enabled) {
+        Settings.Global.putString(mContext.getContentResolver(),
+                Settings.Global.JOB_SCHEDULER_CONSTANTS, "bg_jobs_restricted=" + enabled);
+    }
+
+    private void setPowerWhiteListed(boolean whitelist) throws RemoteException {
+        if (whitelist) {
+            mDeviceIdleController.addPowerSaveWhitelistApp(TEST_APP_PACKAGE);
+        } else {
+            mDeviceIdleController.removePowerSaveWhitelistApp(TEST_APP_PACKAGE);
+        }
+    }
+
+    private void setAppOpsModeAllowed(boolean allow) throws PackageManager.NameNotFoundException {
+        mAppOpsManager.setMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, mTestPackageUid,
+                TEST_APP_PACKAGE, allow ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED);
+    }
+
+    private boolean awaitJobStart(long timeout) throws InterruptedException {
+        return waitUntilTrue(timeout, () -> {
+            synchronized (mTestJobStatus) {
+                return (mTestJobStatus.jobId == mTestJobId) && mTestJobStatus.running;
+            }
+        });
+    }
+
+    private boolean awaitJobStop(long timeout) throws InterruptedException {
+        return waitUntilTrue(timeout, () -> {
+            synchronized (mTestJobStatus) {
+                return (mTestJobStatus.jobId == mTestJobId) && !mTestJobStatus.running &&
+                        mTestJobStatus.stopReason == JobParameters.REASON_CONSTRAINTS_NOT_SATISFIED;
+            }
+        });
+    }
+
+    private boolean waitUntilTrue(long timeout, Condition condition) throws InterruptedException {
+        final long deadLine = SystemClock.uptimeMillis() + timeout;
+        do {
+            Thread.sleep(POLL_INTERVAL);
+        } while (!condition.isTrue() && SystemClock.uptimeMillis() < deadLine);
+        return condition.isTrue();
+    }
+
+    private static final class TestJobStatus {
+        int jobId;
+        int stopReason;
+        boolean running;
+        private void reset() {
+            running = false;
+            stopReason = jobId = 0;
+        }
+    }
+
+    private interface Condition {
+        boolean isTrue();
+    }
+}
diff --git a/services/tests/servicestests/test-apps/Android.mk b/services/tests/servicestests/test-apps/Android.mk
new file mode 100644
index 0000000..5053e7d
--- /dev/null
+++ b/services/tests/servicestests/test-apps/Android.mk
@@ -0,0 +1 @@
+include $(call all-subdir-makefiles)
diff --git a/services/tests/servicestests/test-apps/JobTestApp/Android.mk b/services/tests/servicestests/test-apps/JobTestApp/Android.mk
new file mode 100644
index 0000000..7893c91
--- /dev/null
+++ b/services/tests/servicestests/test-apps/JobTestApp/Android.mk
@@ -0,0 +1,30 @@
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+LOCAL_SDK_VERSION := current
+
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_PACKAGE_NAME := JobTestApp
+LOCAL_DEX_PREOPT := false
+LOCAL_PROGUARD_ENABLED := disabled
+
+include $(BUILD_PACKAGE)
\ No newline at end of file
diff --git a/services/tests/servicestests/test-apps/JobTestApp/AndroidManifest.xml b/services/tests/servicestests/test-apps/JobTestApp/AndroidManifest.xml
new file mode 100644
index 0000000..ac35805
--- /dev/null
+++ b/services/tests/servicestests/test-apps/JobTestApp/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.servicestests.apps.jobtestapp">
+
+    <application>
+        <service android:name=".TestJobService"
+                 android:permission="android.permission.BIND_JOB_SERVICE" />
+        <activity android:name=".TestJobActivity"
+                  android:exported="true" />
+    </application>
+
+</manifest>
\ 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
new file mode 100644
index 0000000..94a85ee
--- /dev/null
+++ b/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobActivity.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.servicestests.apps.jobtestapp;
+
+import android.app.Activity;
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+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.extra.START_JOB";
+    public static final String ACTION_CANCEL_JOBS =
+            "com.android.servicestests.apps.jobtestapp.extra.CANCEL_JOBS";
+    public static final int JOB_INITIAL_BACKOFF = 10_000;
+    public static final int JOB_MINIMUM_LATENCY = 5_000;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        ComponentName jobServiceComponent = new ComponentName(this, TestJobService.class);
+        JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
+        final Intent intent = getIntent();
+        switch (intent.getAction()) {
+            case ACTION_CANCEL_JOBS:
+                jobScheduler.cancelAll();
+                Log.d(TAG, "Cancelled all jobs for " + getPackageName());
+                break;
+            case ACTION_START_JOB:
+                final int jobId = intent.getIntExtra(EXTRA_JOB_ID_KEY, hashCode());
+                JobInfo.Builder jobBuilder = new JobInfo.Builder(jobId, jobServiceComponent)
+                        .setBackoffCriteria(JOB_INITIAL_BACKOFF, JobInfo.BACKOFF_POLICY_LINEAR)
+                        .setMinimumLatency(JOB_MINIMUM_LATENCY);
+                final int result = jobScheduler.schedule(jobBuilder.build());
+                if (result != JobScheduler.RESULT_SUCCESS) {
+                    Log.e(TAG, "Could not schedule job " + jobId);
+                } else {
+                    Log.d(TAG, "Successfully scheduled job with id " + jobId);
+                }
+                break;
+        }
+        finish();
+    }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java b/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java
new file mode 100644
index 0000000..6bebb32
--- /dev/null
+++ b/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.servicestests.apps.jobtestapp;
+
+import android.annotation.TargetApi;
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.content.Intent;
+import android.util.Log;
+
+@TargetApi(24)
+public class TestJobService extends JobService {
+    private static final String TAG = TestJobService.class.getSimpleName();
+    private static final String PACKAGE_NAME = "com.android.servicestests.apps.jobtestapp";
+    public static final String ACTION_JOB_STARTED = PACKAGE_NAME + ".action.JOB_STARTED";
+    public static final String ACTION_JOB_STOPPED = PACKAGE_NAME + ".action.JOB_STOPPED";
+    public static final String JOB_PARAMS_EXTRA_KEY = PACKAGE_NAME + ".extra.JOB_PARAMETERS";
+
+    @Override
+    public boolean onStartJob(JobParameters params) {
+        Log.i(TAG, "Test job executing: " + params.getJobId());
+        Intent reportJobStartIntent = new Intent(ACTION_JOB_STARTED);
+        reportJobStartIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params);
+        sendBroadcast(reportJobStartIntent);
+        return true;
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        Log.i(TAG, "Test job stopped executing: " + params.getJobId());
+        Intent reportJobStopIntent = new Intent(ACTION_JOB_STOPPED);
+        reportJobStopIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params);
+        sendBroadcast(reportJobStopIntent);
+        return true;
+    }
+}