Temporarily whitelist an app for network during doze

API to allow an app to be whitelisted for network and wakelock
access for a short period. So even if the device is in idle
mode, such apps can be given a chance to download the payload
related to a high priority cloud-to-device message.

This API is meant for system apps only.

A new permission CHANGE_DEVICE_IDLE_TEMP_WHITELIST is required
to make this call.

Bug: 21525864
Change-Id: Id7a761a664f21af5d7ff55aa56e8df98d15511ca
diff --git a/services/core/java/com/android/server/DeviceIdleController.java b/services/core/java/com/android/server/DeviceIdleController.java
index 9b7b2d3..e9759c3 100644
--- a/services/core/java/com/android/server/DeviceIdleController.java
+++ b/services/core/java/com/android/server/DeviceIdleController.java
@@ -16,14 +16,19 @@
 
 package com.android.server;
 
+import android.Manifest;
+import android.app.ActivityManagerNative;
 import android.app.AlarmManager;
+import android.app.AppGlobals;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
 import android.hardware.TriggerEvent;
@@ -47,7 +52,9 @@
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Slog;
+import android.util.SparseArray;
 import android.util.SparseBooleanArray;
+import android.util.SparseLongArray;
 import android.util.TimeUtils;
 import android.util.Xml;
 import android.view.Display;
@@ -55,6 +62,7 @@
 import com.android.internal.app.IBatteryStats;
 import com.android.internal.os.AtomicFile;
 import com.android.internal.os.BackgroundThread;
+import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.XmlUtils;
 import com.android.server.am.BatteryStatsService;
@@ -72,6 +80,7 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
 
 /**
  * Keeps track of device idleness and drives low power mode based on that.
@@ -79,7 +88,8 @@
 public class DeviceIdleController extends SystemService {
     private static final String TAG = "DeviceIdleController";
 
-    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    private static final boolean DEBUG = false;
+    private static final boolean COMPRESS_TIME = false;
 
     public static final String SERVICE_NAME = "deviceidle";
 
@@ -94,29 +104,31 @@
      * immediately after going inactive just because we don't want to be continually running
      * the significant motion sensor whenever the screen is off.
      */
-    private static final long DEFAULT_INACTIVE_TIMEOUT = !DEBUG ? 30*60*1000L
-            : 2 * 60 * 1000L;
+    private static final long DEFAULT_INACTIVE_TIMEOUT = !COMPRESS_TIME ? 30*60*1000L
+            : 3 * 60 * 1000L;
     /**
      * This is the time, after seeing motion, that we wait after becoming inactive from
      * that until we start looking for motion again.
      */
-    private static final long DEFAULT_MOTION_INACTIVE_TIMEOUT = !DEBUG ? 10*60*1000L
+    private static final long DEFAULT_MOTION_INACTIVE_TIMEOUT = !COMPRESS_TIME ? 10*60*1000L
             : 60 * 1000L;
     /**
      * This is the time, after the inactive timeout elapses, that we will wait looking
      * for significant motion until we truly consider the device to be idle.
      */
-    private static final long DEFAULT_IDLE_AFTER_INACTIVE_TIMEOUT = !DEBUG ? 30*60*1000L
-            : 2 * 60 * 1000L;
+    private static final long DEFAULT_IDLE_AFTER_INACTIVE_TIMEOUT = !COMPRESS_TIME ? 30*60*1000L
+            : 3 * 60 * 1000L;
     /**
      * This is the initial time, after being idle, that we will allow ourself to be back
      * in the IDLE_PENDING state allowing the system to run normally until we return to idle.
      */
-    private static final long DEFAULT_IDLE_PENDING_TIMEOUT = 5*60*1000L;
+    private static final long DEFAULT_IDLE_PENDING_TIMEOUT = !COMPRESS_TIME ? 5*60*1000L
+            : 30 * 1000L;
     /**
      * Maximum pending idle timeout (time spent running) we will be allowed to use.
      */
-    private static final long DEFAULT_MAX_IDLE_PENDING_TIMEOUT = 10*60*1000L;
+    private static final long DEFAULT_MAX_IDLE_PENDING_TIMEOUT = !COMPRESS_TIME ? 10*60*1000L
+            : 60 * 1000L;
     /**
      * Scaling factor to apply to current pending idle timeout each time we cycle through
      * that state.
@@ -126,13 +138,13 @@
      * This is the initial time that we want to sit in the idle state before waking up
      * again to return to pending idle and allowing normal work to run.
      */
-    private static final long DEFAULT_IDLE_TIMEOUT = !DEBUG ? 60*60*1000L
-            : 5 * 60 * 1000L;
+    private static final long DEFAULT_IDLE_TIMEOUT = !COMPRESS_TIME ? 60*60*1000L
+            : 6 * 60 * 1000L;
     /**
      * Maximum idle duration we will be allowed to use.
      */
-    private static final long DEFAULT_MAX_IDLE_TIMEOUT = !DEBUG ? 6*60*60*1000L
-            : 10 * 60 * 1000L;
+    private static final long DEFAULT_MAX_IDLE_TIMEOUT = !COMPRESS_TIME ? 6*60*60*1000L
+            : 30 * 60 * 1000L;
     /**
      * Scaling factor to apply to current idle timeout each time we cycle through that state.
      */
@@ -141,8 +153,13 @@
      * This is the minimum time we will allow until the next upcoming alarm for us to
      * actually go in to idle mode.
      */
-    private static final long DEFAULT_MIN_TIME_TO_ALARM = !DEBUG ? 60*60*1000L
-            : 5 * 60 * 1000L;
+    private static final long DEFAULT_MIN_TIME_TO_ALARM = !COMPRESS_TIME ? 60*60*1000L
+            : 6 * 60 * 1000L;
+
+    /**
+     * Max amount of time to temporarily whitelist an app when it receives a high priority tickle.
+     */
+    private static final long MAX_TEMP_APP_WHITELIST_DURATION = 5 * 60 * 1000L;
 
     private AlarmManager mAlarmManager;
     private IBatteryStats mBatteryStats;
@@ -210,6 +227,17 @@
      */
     private int[] mPowerSaveWhitelistAppIdArray = new int[0];
 
+    /**
+     * List of end times for UIDs that are temporarily marked as being allowed to access
+     * the network and acquire wakelocks. Times are in milliseconds.
+     */
+    private SparseLongArray mTempWhitelistAppIdEndTimes = new SparseLongArray();
+
+    /**
+     * Current app IDs of temporarily whitelist apps for high-priority messages.
+     */
+    private int[] mTempWhitelistAppIdArray = new int[0];
+
     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
         @Override public void onReceive(Context context, Intent intent) {
             if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {
@@ -252,6 +280,7 @@
     static final int MSG_REPORT_IDLE_ON = 2;
     static final int MSG_REPORT_IDLE_OFF = 3;
     static final int MSG_REPORT_ACTIVE = 4;
+    static final int MSG_TEMP_APP_WHITELIST_TIMEOUT = 5;
 
     final class MyHandler extends Handler {
         MyHandler(Looper looper) {
@@ -294,6 +323,10 @@
                         getContext().sendBroadcastAsUser(mIdleIntent, UserHandle.ALL);
                     }
                 } break;
+                case MSG_TEMP_APP_WHITELIST_TIMEOUT: {
+                    int uid = msg.arg1;
+                    checkTempAppWhitelistTimeout(uid);
+                } break;
             }
         }
     }
@@ -325,10 +358,39 @@
             return getAppIdWhitelistInternal();
         }
 
+        @Override public int[] getAppIdTempWhitelist() {
+            return getAppIdTempWhitelistInternal();
+        }
+
         @Override public boolean isPowerSaveWhitelistApp(String name) {
             return isPowerSaveWhitelistAppInternal(name);
         }
 
+        @Override public void addPowerSaveTempWhitelistApp(String packageName, long duration,
+                int userId) throws RemoteException {
+            getContext().enforceCallingPermission(
+                    Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST,
+                    "No permission to change device idle whitelist");
+            userId = ActivityManagerNative.getDefault().handleIncomingUser(
+                    Binder.getCallingPid(),
+                    Binder.getCallingUid(),
+                    userId,
+                    /*allowAll=*/ false,
+                    /*requireFull=*/ false,
+                    "addAppBrieflyToWhitelist", null);
+            final long token = Binder.clearCallingIdentity();
+            try {
+                PackageInfo pi = AppGlobals.getPackageManager()
+                        .getPackageInfo(packageName, 0, userId);
+                if (pi == null) return;
+                DeviceIdleController.this.addPowerSaveTempWhitelistAppInternal(packageName,
+                        duration, userId);
+            } catch (RemoteException re) {
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
         @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
             DeviceIdleController.this.dump(fd, pw, args);
         }
@@ -481,6 +543,70 @@
         }
     }
 
+    public int[] getAppIdTempWhitelistInternal() {
+        synchronized (this) {
+            return mTempWhitelistAppIdArray;
+        }
+    }
+
+    /**
+     * Adds an app to the temporary whitelist and resets the endTime for granting the
+     * app an exemption to access network and acquire wakelocks.
+     */
+    public void addPowerSaveTempWhitelistAppInternal(String packageName, long duration,
+            int userId) {
+        if (duration > MAX_TEMP_APP_WHITELIST_DURATION) {
+            duration = MAX_TEMP_APP_WHITELIST_DURATION;
+        }
+        try {
+            int uid = getContext().getPackageManager().getPackageUid(packageName, userId);
+            int appId = UserHandle.getAppId(uid);
+            final long timeNow = System.currentTimeMillis();
+            synchronized (this) {
+                long currentEndTime = mTempWhitelistAppIdEndTimes.get(appId);
+                // Set the new end time
+                mTempWhitelistAppIdEndTimes.put(appId, timeNow + duration);
+                if (DEBUG) {
+                    Slog.d(TAG, "Adding AppId " + appId + " to temp whitelist");
+                }
+                if (currentEndTime == 0) {
+                    // No pending timeout for the app id, post a delayed message
+                    postTempActiveTimeoutMessage(appId, duration);
+                    updateTempWhitelistAppIdsLocked();
+                    reportTempWhitelistChangedLocked();
+                }
+            }
+        } catch (NameNotFoundException e) {
+        }
+    }
+
+    private void postTempActiveTimeoutMessage(int uid, long delay) {
+        mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TEMP_APP_WHITELIST_TIMEOUT, uid, 0),
+                delay);
+    }
+
+    void checkTempAppWhitelistTimeout(int uid) {
+        final long timeNow = System.currentTimeMillis();
+        synchronized (this) {
+            long endTime = mTempWhitelistAppIdEndTimes.get(uid);
+            if (endTime == 0) {
+                // Nothing to do
+                return;
+            }
+            if (timeNow >= endTime) {
+                mTempWhitelistAppIdEndTimes.delete(uid);
+                if (DEBUG) {
+                    Slog.d(TAG, "Removing UID " + uid + " from temp whitelist");
+                }
+                updateTempWhitelistAppIdsLocked();
+                reportTempWhitelistChangedLocked();
+            } else {
+                // Need more time
+                postTempActiveTimeoutMessage(uid, endTime - timeNow);
+            }
+        }
+    }
+
     void updateDisplayLocked() {
         mCurDisplay = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
         // We consider any situation where the display is showing something to be it on,
@@ -659,14 +785,41 @@
         }
         mPowerSaveWhitelistAppIdArray = appids;
         if (mLocalPowerManager != null) {
+            if (DEBUG) {
+                Slog.d(TAG, "Setting wakelock whitelist to "
+                        + Arrays.toString(mPowerSaveWhitelistAppIdArray));
+            }
             mLocalPowerManager.setDeviceIdleWhitelist(mPowerSaveWhitelistAppIdArray);
         }
     }
 
+    private void updateTempWhitelistAppIdsLocked() {
+        final int size = mTempWhitelistAppIdEndTimes.size();
+        if (mTempWhitelistAppIdArray.length != size) {
+            mTempWhitelistAppIdArray = new int[size];
+        }
+        for (int i = 0; i < size; i++) {
+            mTempWhitelistAppIdArray[i] = mTempWhitelistAppIdEndTimes.keyAt(i);
+        }
+        if (mLocalPowerManager != null) {
+            if (DEBUG) {
+                Slog.d(TAG, "Setting wakelock temp whitelist to "
+                        + Arrays.toString(mTempWhitelistAppIdArray));
+            }
+            mLocalPowerManager.setDeviceIdleTempWhitelist(mTempWhitelistAppIdArray);
+        }
+    }
+
     private void reportPowerSaveWhitelistChangedLocked() {
         Intent intent = new Intent(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
-        getContext().sendBroadcast(intent);
+        getContext().sendBroadcastAsUser(intent, UserHandle.OWNER);
+    }
+
+    private void reportTempWhitelistChangedLocked() {
+        Intent intent = new Intent(PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED);
+        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+        getContext().sendBroadcastAsUser(intent, UserHandle.OWNER);
     }
 
     void readConfigFileLocked() {
@@ -817,11 +970,18 @@
         }
 
         if (args != null) {
+            int userId = UserHandle.USER_OWNER;
             for (int i=0; i<args.length; i++) {
                 String arg = args[i];
                 if ("-h".equals(arg)) {
                     dumpHelp(pw);
                     return;
+                } else if ("-u".equals(arg)) {
+                    i++;
+                    if (i < args.length) {
+                        arg = args[i];
+                        userId = Integer.parseInt(arg);
+                    }
                 } else if ("-a".equals(arg)) {
                     // Ignore, we always dump all.
                 } else if ("step".equals(arg)) {
@@ -873,6 +1033,17 @@
                         }
                     }
                     return;
+                } else if ("tempwhitelist".equals(arg)) {
+                    i++;
+                    if (i >= args.length) {
+                        pw.println("At least one package name must be specified");
+                        return;
+                    }
+                    while (i < args.length) {
+                        arg = args[i];
+                        i++;
+                        addPowerSaveTempWhitelistAppInternal(arg, 10000L, userId);
+                    }
                 } else if (arg.length() > 0 && arg.charAt(0) == '-'){
                     pw.println("Unknown option: " + arg);
                     return;
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index 792d4ba..7673af4 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -283,9 +283,12 @@
     /**
      * UIDs that have been white-listed to always be able to have network access
      * in power save mode.
+     * TODO: An int array might be sufficient
      */
     private final SparseBooleanArray mPowerSaveWhitelistAppIds = new SparseBooleanArray();
 
+    private final SparseBooleanArray mPowerSaveTempWhitelistAppIds = new SparseBooleanArray();
+
     /** Set of ifaces that are metered. */
     private ArraySet<String> mMeteredIfaces = new ArraySet<>();
     /** Set of over-limit templates that have been notified. */
@@ -371,6 +374,19 @@
         }
     }
 
+    void updatePowerSaveTempWhitelistLocked() {
+        try {
+            final int[] whitelist = mDeviceIdleController.getAppIdTempWhitelist();
+            mPowerSaveTempWhitelistAppIds.clear();
+            if (whitelist != null) {
+                for (int uid : whitelist) {
+                    mPowerSaveTempWhitelistAppIds.put(uid, true);
+                }
+            }
+        } catch (RemoteException e) {
+        }
+    }
+
     public void systemReady() {
         if (!isBandwidthControlEnabled()) {
             Slog.w(TAG, "bandwidth controls disabled, unable to enforce policy");
@@ -392,6 +408,7 @@
                         if (mRestrictPower != enabled) {
                             mRestrictPower = enabled;
                             updateRulesForGlobalChangeLocked(true);
+                            updateRulesForTempWhitelistChangeLocked();
                         }
                     }
                 }
@@ -404,6 +421,7 @@
 
             if (mRestrictBackground || mRestrictPower || mDeviceIdleMode) {
                 updateRulesForGlobalChangeLocked(true);
+                updateRulesForTempWhitelistChangeLocked();
                 updateNotificationsLocked();
             }
         }
@@ -428,6 +446,7 @@
         // listen for changes to power save whitelist
         final IntentFilter whitelistFilter = new IntentFilter(
                 PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
+        whitelistFilter.addAction(PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED);
         mContext.registerReceiver(mPowerSaveWhitelistReceiver, whitelistFilter, null, mHandler);
 
         // watch for network interfaces to be claimed
@@ -496,8 +515,13 @@
         public void onReceive(Context context, Intent intent) {
             // on background handler thread, and POWER_SAVE_WHITELIST_CHANGED is protected
             synchronized (mRulesLock) {
-                updatePowerSaveWhitelistLocked();
-                updateRulesForGlobalChangeLocked(false);
+                if (PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED.equals(intent.getAction())) {
+                    updatePowerSaveWhitelistLocked();
+                    updateRulesForGlobalChangeLocked(false);
+                } else {
+                    updatePowerSaveTempWhitelistLocked();
+                    updateRulesForTempWhitelistChangeLocked();
+                }
             }
         }
     };
@@ -2019,6 +2043,17 @@
         }
     }
 
+    void updateRulesForTempWhitelistChangeLocked() {
+        final List<UserInfo> users = mUserManager.getUsers();
+        for (UserInfo user : users) {
+            for (int i = mPowerSaveTempWhitelistAppIds.size() - 1; i >= 0; i--) {
+                int appId = mPowerSaveTempWhitelistAppIds.keyAt(i);
+                int uid = UserHandle.getUid(user.id, appId);
+                updateRulesForUidLocked(uid);
+            }
+        }
+    }
+
     private static boolean isUidValidForRules(int uid) {
         // allow rules on specific system services, and any apps
         if (uid == android.os.Process.MEDIA_UID || uid == android.os.Process.DRM_UID
@@ -2065,8 +2100,10 @@
 
         // derive active rules based on policy and active state
 
+        int appId = UserHandle.getAppId(uid);
         int uidRules = RULE_ALLOW_ALL;
-        if (uidIdle && !mPowerSaveWhitelistAppIds.get(UserHandle.getAppId(uid))) {
+        if (uidIdle && !mPowerSaveWhitelistAppIds.get(appId)
+                && !mPowerSaveTempWhitelistAppIds.get(appId)) {
             uidRules = RULE_REJECT_ALL;
         } else if (!uidForeground && (uidPolicy & POLICY_REJECT_METERED_BACKGROUND) != 0) {
             // uid in background, and policy says to block metered data
@@ -2077,7 +2114,8 @@
                 uidRules = RULE_REJECT_METERED;
             }
         } else if (mRestrictPower || mDeviceIdleMode) {
-            final boolean whitelisted = mPowerSaveWhitelistAppIds.get(UserHandle.getAppId(uid));
+            final boolean whitelisted = mPowerSaveWhitelistAppIds.get(appId)
+                    || mPowerSaveTempWhitelistAppIds.get(appId);
             if (!whitelisted && !uidForeground
                     && (uidPolicy & POLICY_ALLOW_BACKGROUND_BATTERY_SAVE) == 0) {
                 // uid is in background, restrict power use mode is on (so we want to
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index c1fe984..3af97db 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -438,6 +438,9 @@
     // Set of app ids that we will always respect the wake locks for.
     int[] mDeviceIdleWhitelist = new int[0];
 
+    // Set of app ids that are temporarily allowed to acquire wakelocks due to high-pri message
+    int[] mDeviceIdleTempWhitelist = new int[0];
+
     private final SparseIntArray mUidState = new SparseIntArray();
 
     // True if theater mode is enabled
@@ -2320,6 +2323,15 @@
         }
     }
 
+    void setDeviceIdleTempWhitelistInternal(int[] appids) {
+        synchronized (mLock) {
+            mDeviceIdleTempWhitelist = appids;
+            if (mDeviceIdleMode) {
+                updateWakeLockDisabledStatesLocked();
+            }
+        }
+    }
+
     void updateUidProcStateInternal(int uid, int procState) {
         synchronized (mLock) {
             mUidState.put(uid, procState);
@@ -2372,6 +2384,7 @@
                 // for application uids that are not whitelisted.
                 if (appid >= Process.FIRST_APPLICATION_UID &&
                         Arrays.binarySearch(mDeviceIdleWhitelist, appid) < 0 &&
+                        Arrays.binarySearch(mDeviceIdleTempWhitelist, appid) < 0 &&
                         mUidState.get(wakeLock.mOwnerUid,
                                 ActivityManager.PROCESS_STATE_CACHED_EMPTY)
                                 > ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
@@ -2579,6 +2592,7 @@
             pw.println("  mBatteryLevelLow=" + mBatteryLevelLow);
             pw.println("  mDeviceIdleMode=" + mDeviceIdleMode);
             pw.println("  mDeviceIdleWhitelist=" + Arrays.toString(mDeviceIdleWhitelist));
+            pw.println("  mDeviceIdleTempWhitelist=" + Arrays.toString(mDeviceIdleTempWhitelist));
             pw.println("  mLastWakeTime=" + TimeUtils.formatUptime(mLastWakeTime));
             pw.println("  mLastSleepTime=" + TimeUtils.formatUptime(mLastSleepTime));
             pw.println("  mLastUserActivityTime=" + TimeUtils.formatUptime(mLastUserActivityTime));
@@ -3478,6 +3492,11 @@
         }
 
         @Override
+        public void setDeviceIdleTempWhitelist(int[] appids) {
+            setDeviceIdleTempWhitelistInternal(appids);
+        }
+
+        @Override
         public void updateUidProcState(int uid, int procState) {
             updateUidProcStateInternal(uid, procState);
         }