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/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++) {