Adding user forced app standby to alarms

The app op RUN_ANY_IN_BACKGROUND can be set to ignored to indicate that
any alarms for the receiving app will be delayed until the app op is set
to allowed or the app comes into the foreground. Power whitelisted apps
are exempted.

Test: cts-tradefed run cts-dev -m AlarmManagerTestCases

Fixes: 64446607
Change-Id: Ic48b52383ad5ccd726c62769cdca25202d1e3814
diff --git a/services/core/java/com/android/server/AlarmManagerService.java b/services/core/java/com/android/server/AlarmManagerService.java
index d1cf025..cab6ecf 100644
--- a/services/core/java/com/android/server/AlarmManagerService.java
+++ b/services/core/java/com/android/server/AlarmManagerService.java
@@ -46,6 +46,7 @@
 import android.os.PowerManager;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
@@ -54,6 +55,7 @@
 import android.text.TextUtils;
 import android.text.format.DateFormat;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.KeyValueListParser;
 import android.util.Log;
 import android.util.Slog;
@@ -85,6 +87,8 @@
 import static android.app.AlarmManager.ELAPSED_REALTIME;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.app.IAppOpsCallback;
+import com.android.internal.app.IAppOpsService;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.LocalLog;
 
@@ -106,6 +110,7 @@
     static final boolean DEBUG_ALARM_CLOCK = localLOGV || false;
     static final boolean DEBUG_LISTENER_CALLBACK = localLOGV || false;
     static final boolean DEBUG_WAKELOCK = localLOGV || false;
+    static final boolean DEBUG_BG_LIMIT = localLOGV || false;
     static final boolean RECORD_ALARMS_IN_HISTORY = true;
     static final boolean RECORD_DEVICE_IDLE_ALARMS = false;
     static final int ALARM_EVENT = 1;
@@ -114,7 +119,7 @@
     private final Intent mBackgroundIntent
             = new Intent().addFlags(Intent.FLAG_FROM_BACKGROUND);
     static final IncreasingTimeOrder sIncreasingTimeOrder = new IncreasingTimeOrder();
-    
+
     static final boolean WAKEUP_STATS = false;
 
     private static final Intent NEXT_ALARM_CLOCK_CHANGED_INTENT =
@@ -125,10 +130,15 @@
     final LocalLog mLog = new LocalLog(TAG);
 
     AppOpsManager mAppOps;
+    IAppOpsService mAppOpsService;
     DeviceIdleController.LocalService mLocalDeviceIdleController;
 
     final Object mLock = new Object();
 
+    ArraySet<String> mForcedAppStandbyPackages = new ArraySet<>();
+    SparseBooleanArray mForegroundUids = new SparseBooleanArray();
+    // List of alarms per uid deferred due to user applied background restrictions on the source app
+    SparseArray<ArrayList<Alarm>> mPendingBackgroundAlarms = new SparseArray<>();
     long mNativeData;
     private long mNextWakeup;
     private long mNextNonWakeup;
@@ -225,6 +235,7 @@
         private static final String KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION
                 = "allow_while_idle_whitelist_duration";
         private static final String KEY_LISTENER_TIMEOUT = "listener_timeout";
+        private static final String KEY_BG_RESTRICTIONS_ENABLED = "limit_bg_alarms_enabled";
 
         private static final long DEFAULT_MIN_FUTURITY = 5 * 1000;
         private static final long DEFAULT_MIN_INTERVAL = 60 * 1000;
@@ -234,6 +245,8 @@
 
         private static final long DEFAULT_LISTENER_TIMEOUT = 5 * 1000;
 
+        private static final boolean DEFAULT_BACKGROUND_RESTRICTIONS_ENABLED = false;
+
         // Minimum futurity of a new alarm
         public long MIN_FUTURITY = DEFAULT_MIN_FUTURITY;
 
@@ -253,6 +266,8 @@
         // Direct alarm listener callback timeout
         public long LISTENER_TIMEOUT = DEFAULT_LISTENER_TIMEOUT;
 
+        public boolean BACKGROUND_ALARMS_BLOCKED = DEFAULT_BACKGROUND_RESTRICTIONS_ENABLED;
+
         private ContentResolver mResolver;
         private final KeyValueListParser mParser = new KeyValueListParser(',');
         private long mLastAllowWhileIdleWhitelistDuration = -1;
@@ -311,6 +326,21 @@
                         DEFAULT_ALLOW_WHILE_IDLE_WHITELIST_DURATION);
                 LISTENER_TIMEOUT = mParser.getLong(KEY_LISTENER_TIMEOUT,
                         DEFAULT_LISTENER_TIMEOUT);
+                BACKGROUND_ALARMS_BLOCKED = mParser.getBoolean(KEY_BG_RESTRICTIONS_ENABLED,
+                        DEFAULT_BACKGROUND_RESTRICTIONS_ENABLED);
+                if (!BACKGROUND_ALARMS_BLOCKED) {
+                    // TODO: remove this code and constant when feature is turned on
+                    // deliver all blocked alarms
+                    final ArrayList<Alarm> allBlockedAlarms = new ArrayList<>();
+                    for (int i = mPendingBackgroundAlarms.size() - 1; i >=0; i--) {
+                        allBlockedAlarms.addAll(mPendingBackgroundAlarms.valueAt(i));
+                    }
+                    mPendingBackgroundAlarms = new SparseArray<>();
+                    deliverPendingBackgroundAlarmsLocked(allBlockedAlarms,
+                            SystemClock.elapsedRealtime());
+                } else if (DEBUG_BG_LIMIT) {
+                    Slog.d(TAG, "Background limiting enabled");
+                }
 
                 updateAllowWhileIdleMinTimeLocked();
                 updateAllowWhileIdleWhitelistDurationLocked();
@@ -705,7 +735,7 @@
     static final BatchTimeOrder sBatchOrder = new BatchTimeOrder();
     final ArrayList<Batch> mAlarmBatches = new ArrayList<>();
 
-    // set to null if in idle mode; while in this mode, any alarms we don't want
+    // set to non-null if in idle mode; while in this mode, any alarms we don't want
     // to run during this time are placed in mPendingWhileIdleAlarms
     Alarm mPendingIdleUntil = null;
     Alarm mNextWakeFromIdle = null;
@@ -814,6 +844,108 @@
         setImplLocked(a, true, doValidate);
     }
 
+    /**
+     * Sends alarms that were blocked due to user applied background restrictions - either because
+     * the user lifted those or the uid came to foreground.
+     *
+     * @param uid uid to filter on
+     * @param packageName package to filter on, or null for all packages in uid
+     */
+    void sendPendingBackgroundAlarmsLocked(int uid, String packageName) {
+        final ArrayList<Alarm> alarmsForUid = mPendingBackgroundAlarms.get(uid);
+        if (alarmsForUid == null || alarmsForUid.size() == 0) {
+            return;
+        }
+        final ArrayList<Alarm> alarmsToDeliver;
+        if (packageName != null) {
+            if (DEBUG_BG_LIMIT) {
+                Slog.d(TAG, "Sending blocked alarms for uid " + uid + ", package " + packageName);
+            }
+            alarmsToDeliver = new ArrayList<>();
+            for (int i = alarmsForUid.size() - 1; i >= 0; i--) {
+                final Alarm a = alarmsForUid.get(i);
+                if (a.matches(packageName)) {
+                    alarmsToDeliver.add(alarmsForUid.remove(i));
+                }
+            }
+            if (alarmsForUid.size() == 0) {
+                mPendingBackgroundAlarms.remove(uid);
+            }
+        } else {
+            if (DEBUG_BG_LIMIT) {
+                Slog.d(TAG, "Sending blocked alarms for uid " + uid);
+            }
+            alarmsToDeliver = alarmsForUid;
+            mPendingBackgroundAlarms.remove(uid);
+        }
+        deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, SystemClock.elapsedRealtime());
+    }
+
+    void sendPendingBackgroundAlarmsForAppIdLocked(int appId) {
+        final ArrayList<Alarm> alarmsToDeliver = new ArrayList<>();
+        for (int i = mPendingBackgroundAlarms.size() - 1; i >= 0; i--) {
+            final int uid = mPendingBackgroundAlarms.keyAt(i);
+            final ArrayList<Alarm> alarmsForUid = mPendingBackgroundAlarms.valueAt(i);
+            if (UserHandle.getAppId(uid) == appId) {
+                alarmsToDeliver.addAll(alarmsForUid);
+                mPendingBackgroundAlarms.removeAt(i);
+            }
+        }
+        deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, SystemClock.elapsedRealtime());
+    }
+
+    private void deliverPendingBackgroundAlarmsLocked(ArrayList<Alarm> alarms, long nowELAPSED) {
+        final int N = alarms.size();
+        boolean hasWakeup = false;
+        for (int i = 0; i < N; i++) {
+            final Alarm alarm = alarms.get(i);
+            if (alarm.wakeup) {
+                hasWakeup = true;
+            }
+            alarm.count = 1;
+            // Recurring alarms may have passed several alarm intervals while the
+            // alarm was kept pending. Send the appropriate trigger count.
+            if (alarm.repeatInterval > 0) {
+                alarm.count += (nowELAPSED - alarm.whenElapsed) / alarm.repeatInterval;
+                // Also schedule its next recurrence
+                final long delta = alarm.count * alarm.repeatInterval;
+                final long nextElapsed = alarm.whenElapsed + delta;
+                setImplLocked(alarm.type, alarm.when + delta, nextElapsed, alarm.windowLength,
+                        maxTriggerTime(nowELAPSED, nextElapsed, alarm.repeatInterval),
+                        alarm.repeatInterval, alarm.operation, null, null, alarm.flags, true,
+                        alarm.workSource, alarm.alarmClock, alarm.uid, alarm.packageName);
+                // Kernel alarms will be rescheduled as needed in setImplLocked
+            }
+        }
+        if (!hasWakeup && checkAllowNonWakeupDelayLocked(nowELAPSED)) {
+            // No need to wakeup for non wakeup alarms
+            if (mPendingNonWakeupAlarms.size() == 0) {
+                mStartCurrentDelayTime = nowELAPSED;
+                mNextNonWakeupDeliveryTime = nowELAPSED
+                        + ((currentNonWakeupFuzzLocked(nowELAPSED)*3)/2);
+            }
+            mPendingNonWakeupAlarms.addAll(alarms);
+            mNumDelayedAlarms += alarms.size();
+        } else {
+            if (DEBUG_BG_LIMIT) {
+                Slog.d(TAG, "Waking up to deliver pending blocked alarms");
+            }
+            // Since we are waking up, also deliver any pending non wakeup alarms we have.
+            if (mPendingNonWakeupAlarms.size() > 0) {
+                alarms.addAll(mPendingNonWakeupAlarms);
+                final long thisDelayTime = nowELAPSED - mStartCurrentDelayTime;
+                mTotalDelayTime += thisDelayTime;
+                if (mMaxDelayTime < thisDelayTime) {
+                    mMaxDelayTime = thisDelayTime;
+                }
+                mPendingNonWakeupAlarms.clear();
+            }
+            calculateDeliveryPriorities(alarms);
+            Collections.sort(alarms, mAlarmDispatchComparator);
+            deliverAlarmsLocked(alarms, nowELAPSED);
+        }
+    }
+
     void restorePendingWhileIdleAlarmsLocked() {
         if (RECORD_DEVICE_IDLE_ALARMS) {
             IdleDispatchEntry ent = new IdleDispatchEntry();
@@ -1032,11 +1164,14 @@
 
         try {
             ActivityManager.getService().registerUidObserver(new UidObserver(),
-                    ActivityManager.UID_OBSERVER_IDLE, ActivityManager.PROCESS_STATE_UNKNOWN, null);
+                    ActivityManager.UID_OBSERVER_GONE | ActivityManager.UID_OBSERVER_IDLE
+                            | ActivityManager.UID_OBSERVER_ACTIVE,
+                    ActivityManager.PROCESS_STATE_UNKNOWN, null);
         } catch (RemoteException e) {
             // ignored; both services live in system_server
         }
-
+        mAppOpsService = IAppOpsService.Stub.asInterface(
+                ServiceManager.getService(Context.APP_OPS_SERVICE));
         publishBinderService(Context.ALARM_SERVICE, mService);
         publishLocalService(LocalService.class, new LocalService());
     }
@@ -1048,6 +1183,13 @@
             mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE);
             mLocalDeviceIdleController
                     = LocalServices.getService(DeviceIdleController.LocalService.class);
+            try {
+                mAppOpsService.startWatchingMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, null,
+                        new AppOpsWatcher());
+            } catch (RemoteException rexc) {
+                // Shouldn't happen as they are in the same process.
+                Slog.e(TAG, "AppOps service not reachable", rexc);
+            }
         }
     }
 
@@ -1461,6 +1603,12 @@
             mConstants.dump(pw);
             pw.println();
 
+            pw.print("  Foreground uids: [");
+            for (int i = 0; i < mForegroundUids.size(); i++) {
+                if (mForegroundUids.valueAt(i)) pw.print(mForegroundUids.keyAt(i) + " ");
+            }
+            pw.println("]");
+            pw.println("  Forced app standby packages: " + mForcedAppStandbyPackages);
             final long nowRTC = System.currentTimeMillis();
             final long nowELAPSED = SystemClock.elapsedRealtime();
             SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@@ -1533,6 +1681,19 @@
                     dumpAlarmList(pw, b.alarms, "    ", nowELAPSED, nowRTC, sdf);
                 }
             }
+            pw.println();
+            pw.println("  Pending user blocked background alarms: ");
+            boolean blocked = false;
+            for (int i = 0; i < mPendingBackgroundAlarms.size(); i++) {
+                final ArrayList<Alarm> blockedAlarms = mPendingBackgroundAlarms.valueAt(i);
+                if (blockedAlarms != null && blockedAlarms.size() > 0) {
+                    blocked = true;
+                    dumpAlarmList(pw, blockedAlarms, "    ", nowELAPSED, nowRTC, sdf);
+                }
+            }
+            if (!blocked) {
+                pw.println("    none");
+            }
             if (mPendingIdleUntil != null || mPendingWhileIdleAlarms.size() > 0) {
                 pw.println();
                 pw.println("    Idle mode state:");
@@ -1799,6 +1960,22 @@
 
     void setDeviceIdleUserWhitelistImpl(int[] appids) {
         synchronized (mLock) {
+            // appids are sorted, just send pending alarms for any new appids added to the whitelist
+            int i = 0, j = 0;
+            while (i < appids.length) {
+                while (j < mDeviceIdleUserWhitelist.length
+                        && mDeviceIdleUserWhitelist[j] < appids[i]) {
+                    j++;
+                }
+                if (j < mDeviceIdleUserWhitelist.length
+                        && appids[i] != mDeviceIdleUserWhitelist[j]) {
+                    if (DEBUG_BG_LIMIT) {
+                        Slog.d(TAG, "Sending blocked alarms for whitelisted appid " + appids[j]);
+                    }
+                    sendPendingBackgroundAlarmsForAppIdLocked(appids[j]);
+                }
+                i++;
+            }
             mDeviceIdleUserWhitelist = appids;
         }
     }
@@ -1979,7 +2156,18 @@
                 mPendingWhileIdleAlarms.remove(i);
             }
         }
-
+        for (int i = mPendingBackgroundAlarms.size() - 1; i >= 0; i--) {
+            final ArrayList<Alarm> alarmsForUid = mPendingBackgroundAlarms.valueAt(i);
+            for (int j = alarmsForUid.size() - 1; j >= 0; j--) {
+                if (alarmsForUid.get(j).matches(operation, directReceiver)) {
+                    // Don't set didRemove, since this doesn't impact the scheduled alarms.
+                    alarmsForUid.remove(j);
+                }
+            }
+            if (alarmsForUid.size() == 0) {
+                mPendingBackgroundAlarms.removeAt(i);
+            }
+        }
         if (didRemove) {
             if (DEBUG_BATCH) {
                 Slog.v(TAG, "remove(operation) changed bounds; rebatching");
@@ -2016,7 +2204,17 @@
                 mPendingWhileIdleAlarms.remove(i);
             }
         }
-
+        for (int i = mPendingBackgroundAlarms.size() - 1; i >= 0; i --) {
+            final ArrayList<Alarm> alarmsForUid = mPendingBackgroundAlarms.valueAt(i);
+            for (int j = alarmsForUid.size() - 1; j >= 0; j--) {
+                if (alarmsForUid.get(j).matches(packageName)) {
+                    alarmsForUid.remove(j);
+                }
+            }
+            if (alarmsForUid.size() == 0) {
+                mPendingBackgroundAlarms.removeAt(i);
+            }
+        }
         if (didRemove) {
             if (DEBUG_BATCH) {
                 Slog.v(TAG, "remove(package) changed bounds; rebatching");
@@ -2043,7 +2241,11 @@
                 mPendingWhileIdleAlarms.remove(i);
             }
         }
-
+        for (int i = mPendingBackgroundAlarms.size() - 1; i >= 0; i--) {
+            if (mPendingBackgroundAlarms.keyAt(i) == uid) {
+                mPendingBackgroundAlarms.removeAt(i);
+            }
+        }
         if (didRemove) {
             if (DEBUG_BATCH) {
                 Slog.v(TAG, "remove(package) changed bounds; rebatching");
@@ -2070,6 +2272,11 @@
                 mPendingWhileIdleAlarms.remove(i);
             }
         }
+        for (int i = mPendingBackgroundAlarms.size() - 1; i >= 0; i--) {
+            if (UserHandle.getUserId(mPendingBackgroundAlarms.keyAt(i)) == userHandle) {
+                mPendingBackgroundAlarms.removeAt(i);
+            }
+        }
         for (int i = mLastAllowWhileIdleDispatch.size() - 1; i >= 0; i--) {
             if (UserHandle.getUserId(mLastAllowWhileIdleDispatch.keyAt(i)) == userHandle) {
                 mLastAllowWhileIdleDispatch.removeAt(i);
@@ -2184,6 +2391,27 @@
         }
     }
 
+    private boolean isBackgroundRestricted(Alarm alarm) {
+        if (!mConstants.BACKGROUND_ALARMS_BLOCKED) {
+            return false;
+        }
+        if (alarm.alarmClock != null) {
+            // Don't block alarm clocks
+            return false;
+        }
+        if (alarm.operation != null
+                && (alarm.operation.isActivity() || alarm.operation.isForegroundService())) {
+            // Don't block starting foreground components
+            return false;
+        }
+        final String sourcePackage =
+                (alarm.operation != null) ? alarm.operation.getCreatorPackage() : alarm.packageName;
+        final int sourceUid = alarm.creatorUid;
+        return mForcedAppStandbyPackages.contains(sourcePackage) && !mForegroundUids.get(sourceUid)
+                && Arrays.binarySearch(mDeviceIdleUserWhitelist, UserHandle.getAppId(sourceUid))
+                < 0;
+    }
+
     private native long init();
     private native void close(long nativeData);
     private native void set(long nativeData, int type, long seconds, long nanoseconds);
@@ -2239,6 +2467,19 @@
                         continue;
                     }
                 }
+                if (isBackgroundRestricted(alarm)) {
+                    // Alarms with FLAG_WAKE_FROM_IDLE or mPendingIdleUntil alarm are not deferred
+                    if (DEBUG_BG_LIMIT) {
+                        Slog.d(TAG, "Deferring alarm " + alarm + " due to user forced app standby");
+                    }
+                    ArrayList<Alarm> alarmsForUid = mPendingBackgroundAlarms.get(alarm.creatorUid);
+                    if (alarmsForUid == null) {
+                        alarmsForUid = new ArrayList<>();
+                        mPendingBackgroundAlarms.put(alarm.creatorUid, alarmsForUid);
+                    }
+                    alarmsForUid.add(alarm);
+                    continue;
+                }
 
                 alarm.count = 1;
                 triggerList.add(alarm);
@@ -2904,21 +3145,29 @@
         }
 
         @Override public void onUidGone(int uid, boolean disabled) {
-            if (disabled) {
-                synchronized (mLock) {
+            synchronized (mLock) {
+                if (disabled) {
                     removeForStoppedLocked(uid);
                 }
+                mForegroundUids.delete(uid);
             }
         }
 
         @Override public void onUidActive(int uid) {
+            synchronized (mLock) {
+                if (!mForegroundUids.get(uid)) {
+                    mForegroundUids.put(uid, true);
+                    sendPendingBackgroundAlarmsLocked(uid, null);
+                }
+            }
         }
 
         @Override public void onUidIdle(int uid, boolean disabled) {
-            if (disabled) {
-                synchronized (mLock) {
+            synchronized (mLock) {
+                if (disabled) {
                     removeForStoppedLocked(uid);
                 }
+                mForegroundUids.delete(uid);
             }
         }
 
@@ -2926,6 +3175,28 @@
         }
     };
 
+    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_BG_LIMIT) {
+                    Slog.d(TAG,
+                            "Appop changed for " + uid + ", " + packageName + " to " + mode);
+                }
+                final boolean changed;
+                if (mode != AppOpsManager.MODE_ALLOWED) {
+                    changed = mForcedAppStandbyPackages.add(packageName);
+                } else {
+                    changed = mForcedAppStandbyPackages.remove(packageName);
+                }
+                if (changed && mode == AppOpsManager.MODE_ALLOWED) {
+                    sendPendingBackgroundAlarmsLocked(uid, packageName);
+                }
+            }
+        }
+    }
+
     private final BroadcastStats getStatsLocked(PendingIntent pi) {
         String pkg = pi.getCreatorPackage();
         int uid = pi.getCreatorUid();