system_server: optimize app idle parole state change

Currently when app idle parole state changes, all idle apps' states
are updated one by one including firewall modifications which are
very expensive.  This optimization gets rid of individual firewall
rule changes and makes sure we only modify the firewall once at child
chain level.

BUG: 21446713
Change-Id: Iafc415fe0bc127826fe17894d4fedcf1755cb17d
diff --git a/core/java/android/app/usage/UsageStatsManagerInternal.java b/core/java/android/app/usage/UsageStatsManagerInternal.java
index 8a31390..9113426 100644
--- a/core/java/android/app/usage/UsageStatsManagerInternal.java
+++ b/core/java/android/app/usage/UsageStatsManagerInternal.java
@@ -77,6 +77,12 @@
     public abstract boolean isAppIdle(String packageName, int userId);
 
     /**
+     * @return True if currently app idle parole mode is on.  This means all idle apps are allow to
+     * run for a short period of time.
+     */
+    public abstract boolean isAppIdleParoleOn();
+
+    /**
      * Sets up a listener for changes to packages being accessed.
      * @param listener A listener within the system process.
      */
@@ -90,8 +96,9 @@
     public abstract void removeAppIdleStateChangeListener(
             AppIdleStateChangeListener listener);
 
-    public interface AppIdleStateChangeListener {
-        void onAppIdleStateChanged(String packageName, int userId, boolean idle);
+    public static abstract class AppIdleStateChangeListener {
+        public abstract void onAppIdleStateChanged(String packageName, int userId, boolean idle);
+        public abstract void onParoleStateChanged(boolean isParoleOn);
     }
 
 }
diff --git a/services/core/java/com/android/server/NetworkManagementService.java b/services/core/java/com/android/server/NetworkManagementService.java
index baa55e7..7afb192 100644
--- a/services/core/java/com/android/server/NetworkManagementService.java
+++ b/services/core/java/com/android/server/NetworkManagementService.java
@@ -214,9 +214,9 @@
      */
     @GuardedBy("mQuotaLock")
     private SparseIntArray mUidFirewallDozableRules = new SparseIntArray();
-
-    private boolean mStandbyChainEnabled = false;
-    private boolean mDozableChainEnabled = false;
+    /** Set of states for the child firewall chains. True if the chain is active. */
+    @GuardedBy("mQuotaLock")
+    final SparseBooleanArray mFirewallChainStates = new SparseBooleanArray();
 
     private Object mIdleTimerLock = new Object();
     /** Set of interfaces with active idle timers. */
@@ -307,9 +307,6 @@
     }
 
     public void systemReady() {
-        // init firewall states
-        mDozableChainEnabled = false;
-        mStandbyChainEnabled = true;
         prepareNativeDaemon();
         if (DBG) Slog.d(TAG, "Prepared");
     }
@@ -611,7 +608,7 @@
                             uidFirewallRules.valueAt(i));
                 }
             }
-            if (mStandbyChainEnabled) {
+            if (mFirewallChainStates.get(FIREWALL_CHAIN_STANDBY)) {
                 setFirewallChainEnabled(FIREWALL_CHAIN_STANDBY, true);
             }
 
@@ -625,7 +622,7 @@
                             uidFirewallRules.valueAt(i));
                 }
             }
-            if (mDozableChainEnabled) {
+            if (mFirewallChainStates.get(FIREWALL_CHAIN_DOZABLE)) {
                 setFirewallChainEnabled(FIREWALL_CHAIN_DOZABLE, true);
             }
         }
@@ -2013,24 +2010,31 @@
     @Override
     public void setFirewallChainEnabled(int chain, boolean enable) {
         enforceSystemUid();
-        final String operation = enable ? "enable_chain" : "disable_chain";
-        try {
-            String chainName;
-            switch(chain) {
-                case FIREWALL_CHAIN_STANDBY:
-                    chainName = FIREWALL_CHAIN_NAME_STANDBY;
-                    mStandbyChainEnabled = enable;
-                    break;
-                case FIREWALL_CHAIN_DOZABLE:
-                    chainName = FIREWALL_CHAIN_NAME_DOZABLE;
-                    mDozableChainEnabled = enable;
-                    break;
-                default:
-                    throw new IllegalArgumentException("Bad child chain: " + chain);
+        synchronized (mQuotaLock) {
+            if (mFirewallChainStates.indexOfKey(chain) >= 0 &&
+                    mFirewallChainStates.get(chain) == enable) {
+                // All is the same, nothing to do.
+                return;
             }
-            mConnector.execute("firewall", operation, chainName);
-        } catch (NativeDaemonConnectorException e) {
-            throw e.rethrowAsParcelableException();
+            mFirewallChainStates.put(chain, enable);
+
+            final String operation = enable ? "enable_chain" : "disable_chain";
+            try {
+                String chainName;
+                switch(chain) {
+                    case FIREWALL_CHAIN_STANDBY:
+                        chainName = FIREWALL_CHAIN_NAME_STANDBY;
+                        break;
+                    case FIREWALL_CHAIN_DOZABLE:
+                        chainName = FIREWALL_CHAIN_NAME_DOZABLE;
+                        break;
+                    default:
+                        throw new IllegalArgumentException("Bad child chain: " + chain);
+                }
+                mConnector.execute("firewall", operation, chainName);
+            } catch (NativeDaemonConnectorException e) {
+                throw e.rethrowAsParcelableException();
+            }
         }
     }
 
@@ -2048,27 +2052,29 @@
     @Override
     public void setFirewallUidRules(int chain, int[] uids, int[] rules) {
         enforceSystemUid();
-        SparseIntArray uidFirewallRules = getUidFirewallRules(chain);
-        SparseIntArray newRules = new SparseIntArray();
-        // apply new set of rules
-        for (int index = uids.length - 1; index >= 0; --index) {
-            int uid = uids[index];
-            int rule = rules[index];
-            setFirewallUidRule(chain, uid, rule);
-            newRules.put(uid, rule);
-        }
-        // collect the rules to remove.
-        SparseIntArray rulesToRemove = new SparseIntArray();
-        for (int index = uidFirewallRules.size() - 1; index >= 0; --index) {
-            int uid = uidFirewallRules.keyAt(index);
-            if (newRules.indexOfKey(uid) < 0) {
-                rulesToRemove.put(uid, FIREWALL_RULE_DEFAULT);
+        synchronized (mQuotaLock) {
+            SparseIntArray uidFirewallRules = getUidFirewallRules(chain);
+            SparseIntArray newRules = new SparseIntArray();
+            // apply new set of rules
+            for (int index = uids.length - 1; index >= 0; --index) {
+                int uid = uids[index];
+                int rule = rules[index];
+                setFirewallUidRule(chain, uid, rule);
+                newRules.put(uid, rule);
             }
-        }
-        // remove dead rules
-        for (int index = rulesToRemove.size() - 1; index >= 0; --index) {
-            int uid = rulesToRemove.keyAt(index);
-            setFirewallUidRuleInternal(chain, uid, FIREWALL_RULE_DEFAULT);
+            // collect the rules to remove.
+            SparseIntArray rulesToRemove = new SparseIntArray();
+            for (int index = uidFirewallRules.size() - 1; index >= 0; --index) {
+                int uid = uidFirewallRules.keyAt(index);
+                if (newRules.indexOfKey(uid) < 0) {
+                    rulesToRemove.put(uid, FIREWALL_RULE_DEFAULT);
+                }
+            }
+            // remove dead rules
+            for (int index = rulesToRemove.size() - 1; index >= 0; --index) {
+                int uid = rulesToRemove.keyAt(index);
+                setFirewallUidRuleInternal(chain, uid, FIREWALL_RULE_DEFAULT);
+            }
         }
     }
 
@@ -2094,34 +2100,43 @@
             }
 
             try {
-                String ruleName;
-                if (getFirewallType(chain) == FIREWALL_TYPE_WHITELIST) {
-                    if (rule == NetworkPolicyManager.FIREWALL_RULE_ALLOW) {
-                        ruleName = "allow";
-                    } else {
-                        ruleName = "deny";
-                    }
-                } else { // Blacklist mode
-                    if (rule == NetworkPolicyManager.FIREWALL_RULE_DENY) {
-                        ruleName = "deny";
-                    } else {
-                        ruleName = "allow";
-                    }
-                }
+                String ruleName = getFirewallRuleName(chain, rule);
+                String oldRuleName = getFirewallRuleName(chain, oldUidFirewallRule);
 
                 if (rule == NetworkPolicyManager.FIREWALL_RULE_DEFAULT) {
                     uidFirewallRules.delete(uid);
                 } else {
                     uidFirewallRules.put(uid, rule);
                 }
-                mConnector.execute("firewall", "set_uid_rule", getFirewallChainName(chain), uid,
-                        ruleName);
+
+                if (!ruleName.equals(oldRuleName)) {
+                    mConnector.execute("firewall", "set_uid_rule", getFirewallChainName(chain), uid,
+                            ruleName);
+                }
             } catch (NativeDaemonConnectorException e) {
                 throw e.rethrowAsParcelableException();
             }
         }
     }
 
+    private @NonNull String getFirewallRuleName(int chain, int rule) {
+        String ruleName;
+        if (getFirewallType(chain) == FIREWALL_TYPE_WHITELIST) {
+            if (rule == NetworkPolicyManager.FIREWALL_RULE_ALLOW) {
+                ruleName = "allow";
+            } else {
+                ruleName = "deny";
+            }
+        } else { // Blacklist mode
+            if (rule == NetworkPolicyManager.FIREWALL_RULE_DENY) {
+                ruleName = "deny";
+            } else {
+                ruleName = "allow";
+            }
+        }
+        return ruleName;
+    }
+
     private @NonNull SparseIntArray getUidFirewallRules(int chain) {
         switch (chain) {
             case FIREWALL_CHAIN_STANDBY:
@@ -2272,7 +2287,8 @@
             pw.println("]");
         }
 
-        pw.println("UID firewall standby chain enabled: " + mStandbyChainEnabled);
+        pw.println("UID firewall standby chain enabled: " +
+                mFirewallChainStates.get(FIREWALL_CHAIN_STANDBY));
         synchronized (mUidFirewallStandbyRules) {
             pw.print("UID firewall standby rule: [");
             final int size = mUidFirewallStandbyRules.size();
@@ -2285,7 +2301,8 @@
             pw.println("]");
         }
 
-        pw.println("UID firewall dozable chain enabled: " + mDozableChainEnabled);
+        pw.println("UID firewall dozable chain enabled: " +
+                mFirewallChainStates.get(FIREWALL_CHAIN_DOZABLE));
         synchronized (mUidFirewallDozableRules) {
             pw.print("UID firewall dozable rule: [");
             final int size = mUidFirewallDozableRules.size();
diff --git a/services/core/java/com/android/server/content/AppIdleMonitor.java b/services/core/java/com/android/server/content/AppIdleMonitor.java
index 9598de8..fe5c2da 100644
--- a/services/core/java/com/android/server/content/AppIdleMonitor.java
+++ b/services/core/java/com/android/server/content/AppIdleMonitor.java
@@ -18,11 +18,6 @@
 
 import android.app.usage.UsageStatsManagerInternal;
 import android.app.usage.UsageStatsManagerInternal.AppIdleStateChangeListener;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.BatteryManager;
 import android.os.UserHandle;
 
 import com.android.server.LocalServices;
@@ -31,53 +26,32 @@
  * Helper to listen for app idle and charging status changes and restart backed off
  * sync operations.
  */
-class AppIdleMonitor implements AppIdleStateChangeListener {
+class AppIdleMonitor extends AppIdleStateChangeListener {
 
     private final SyncManager mSyncManager;
     private final UsageStatsManagerInternal mUsageStats;
-    final BatteryManager mBatteryManager;
-    /** Is the device currently plugged into power. */
-    private boolean mPluggedIn;
+    private boolean mAppIdleParoleOn;
 
-    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            onPluggedIn(mBatteryManager.isCharging());
-        }
-    };
-
-    AppIdleMonitor(SyncManager syncManager, Context context) {
+    AppIdleMonitor(SyncManager syncManager) {
         mSyncManager = syncManager;
         mUsageStats = LocalServices.getService(UsageStatsManagerInternal.class);
+        mAppIdleParoleOn = mUsageStats.isAppIdleParoleOn();
+
         mUsageStats.addAppIdleStateChangeListener(this);
-        mBatteryManager = context.getSystemService(BatteryManager.class);
-        mPluggedIn = isPowered();
-        registerReceivers(context);
     }
 
-    private void registerReceivers(Context context) {
-        // Monitor battery charging state
-        IntentFilter filter = new IntentFilter(BatteryManager.ACTION_CHARGING);
-        filter.addAction(BatteryManager.ACTION_DISCHARGING);
-        context.registerReceiver(mReceiver, filter);
-    }
-
-    private boolean isPowered() {
-        return mBatteryManager.isCharging();
-    }
-
-    void onPluggedIn(boolean pluggedIn) {
-        if (mPluggedIn == pluggedIn) {
+    void setAppIdleParoleOn(boolean appIdleParoleOn) {
+        if (mAppIdleParoleOn == appIdleParoleOn) {
             return;
         }
-        mPluggedIn = pluggedIn;
-        if (mPluggedIn) {
+        mAppIdleParoleOn = appIdleParoleOn;
+        if (mAppIdleParoleOn) {
             mSyncManager.onAppNotIdle(null, UserHandle.USER_ALL);
         }
     }
 
     boolean isAppIdle(String packageName, int userId) {
-        return !mPluggedIn && mUsageStats.isAppIdle(packageName, userId);
+        return !mAppIdleParoleOn && mUsageStats.isAppIdle(packageName, userId);
     }
 
     @Override
@@ -86,4 +60,9 @@
         if (idle) return;
         mSyncManager.onAppNotIdle(packageName, userId);
     }
+
+    @Override
+    public void onParoleStateChanged(boolean isParoleOn) {
+        setAppIdleParoleOn(isParoleOn);
+    }
 }
diff --git a/services/core/java/com/android/server/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java
index cd9c7fe..91aa745 100644
--- a/services/core/java/com/android/server/content/SyncManager.java
+++ b/services/core/java/com/android/server/content/SyncManager.java
@@ -19,6 +19,7 @@
 import android.accounts.Account;
 import android.accounts.AccountAndUser;
 import android.accounts.AccountManager;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.AlarmManager;
 import android.app.AppGlobals;
@@ -448,7 +449,7 @@
         mSyncAlarmIntent = PendingIntent.getBroadcast(
                 mContext, 0 /* ignored */, new Intent(ACTION_SYNC_ALARM), 0);
 
-        mAppIdleMonitor = new AppIdleMonitor(this, mContext);
+        mAppIdleMonitor = new AppIdleMonitor(this);
 
         IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
         context.registerReceiver(mConnectivityIntentReceiver, intentFilter);
@@ -1236,7 +1237,7 @@
      * @param userId The user for which the package has become active. Can be USER_ALL if
      * the device just plugged in.
      */
-    void onAppNotIdle(String packageName, int userId) {
+    void onAppNotIdle(@Nullable String packageName, int userId) {
         synchronized (mSyncQueue) {
             // For all sync operations in sync queue, if marked as idle, compare with package name
             // and unmark. And clear backoff for the operation.
diff --git a/services/core/java/com/android/server/job/controllers/AppIdleController.java b/services/core/java/com/android/server/job/controllers/AppIdleController.java
index 98fb11b..02d4f40 100644
--- a/services/core/java/com/android/server/job/controllers/AppIdleController.java
+++ b/services/core/java/com/android/server/job/controllers/AppIdleController.java
@@ -17,12 +17,7 @@
 package com.android.server.job.controllers;
 
 import android.app.usage.UsageStatsManagerInternal;
-import android.content.BroadcastReceiver;
 import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.BatteryManager;
-import android.os.BatteryManagerInternal;
 import android.util.Slog;
 
 import com.android.server.LocalServices;
@@ -38,8 +33,7 @@
  * for a certain amount of time (maybe hours or days) are considered idle. When the app comes
  * out of idle state, it will be allowed to run scheduled jobs.
  */
-public class AppIdleController extends StateController
-        implements UsageStatsManagerInternal.AppIdleStateChangeListener {
+public class AppIdleController extends StateController {
 
     private static final String LOG_TAG = "AppIdleController";
     private static final boolean DEBUG = false;
@@ -49,14 +43,7 @@
     private static volatile AppIdleController sController;
     final ArrayList<JobStatus> mTrackedTasks = new ArrayList<JobStatus>();
     private final UsageStatsManagerInternal mUsageStatsInternal;
-    private final BatteryManager mBatteryManager;
-    private boolean mPluggedIn;
-
-    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
-        @Override public void onReceive(Context context, Intent intent) {
-            onPluggedIn(mBatteryManager.isCharging());
-        }
-    };
+    boolean mAppIdleParoleOn;
 
     public static AppIdleController get(JobSchedulerService service) {
         synchronized (sCreationLock) {
@@ -70,17 +57,8 @@
     private AppIdleController(StateChangedListener stateChangedListener, Context context) {
         super(stateChangedListener, context);
         mUsageStatsInternal = LocalServices.getService(UsageStatsManagerInternal.class);
-        mBatteryManager = context.getSystemService(BatteryManager.class);
-        mPluggedIn = mBatteryManager.isCharging();
-        mUsageStatsInternal.addAppIdleStateChangeListener(this);
-        registerReceivers();
-    }
-
-    private void registerReceivers() {
-        // Monitor battery charging state
-        IntentFilter filter = new IntentFilter(BatteryManager.ACTION_CHARGING);
-        filter.addAction(BatteryManager.ACTION_DISCHARGING);
-        mContext.registerReceiver(mReceiver, filter);
+        mAppIdleParoleOn = mUsageStatsInternal.isAppIdleParoleOn();
+        mUsageStatsInternal.addAppIdleStateChangeListener(new AppIdleStateChangeListener());
     }
 
     @Override
@@ -88,7 +66,7 @@
         synchronized (mTrackedTasks) {
             mTrackedTasks.add(jobStatus);
             String packageName = jobStatus.job.getService().getPackageName();
-            final boolean appIdle = !mPluggedIn && mUsageStatsInternal.isAppIdle(packageName,
+            final boolean appIdle = !mAppIdleParoleOn && mUsageStatsInternal.isAppIdle(packageName,
                     jobStatus.getUserId());
             if (DEBUG) {
                 Slog.d(LOG_TAG, "Start tracking, setting idle state of "
@@ -108,7 +86,7 @@
     @Override
     public void dumpControllerState(PrintWriter pw) {
         pw.println("AppIdle");
-        pw.println("Plugged In: " + mPluggedIn);
+        pw.println("Parole On: " + mAppIdleParoleOn);
         synchronized (mTrackedTasks) {
             for (JobStatus task : mTrackedTasks) {
                 pw.print(task.job.getService().getPackageName());
@@ -119,48 +97,20 @@
         }
     }
 
-    @Override
-    public void onAppIdleStateChanged(String packageName, int userId, boolean idle) {
-        boolean changed = false;
-        synchronized (mTrackedTasks) {
-            // If currently plugged in, we don't care about app idle state
-            if (mPluggedIn) {
-                return;
-            }
-            for (JobStatus task : mTrackedTasks) {
-                if (task.job.getService().getPackageName().equals(packageName)
-                        && task.getUserId() == userId) {
-                    if (task.appNotIdleConstraintSatisfied.get() != !idle) {
-                        if (DEBUG) {
-                            Slog.d(LOG_TAG, "App Idle state changed, setting idle state of "
-                                    + packageName + " to " + idle);
-                        }
-                        task.appNotIdleConstraintSatisfied.set(!idle);
-                        changed = true;
-                    }
-                }
-            }
-        }
-        if (changed) {
-            mStateChangedListener.onControllerStateChanged();
-        }
-    }
-
-    void onPluggedIn(boolean pluggedIn) {
+    void setAppIdleParoleOn(boolean isAppIdleParoleOn) {
         // Flag if any app's idle state has changed
         boolean changed = false;
         synchronized (mTrackedTasks) {
-            if (mPluggedIn == pluggedIn) {
+            if (mAppIdleParoleOn == isAppIdleParoleOn) {
                 return;
             }
-            mPluggedIn = pluggedIn;
+            mAppIdleParoleOn = isAppIdleParoleOn;
             for (JobStatus task : mTrackedTasks) {
                 String packageName = task.job.getService().getPackageName();
-                final boolean appIdle = !mPluggedIn && mUsageStatsInternal.isAppIdle(packageName,
+                final boolean appIdle = !mAppIdleParoleOn && mUsageStatsInternal.isAppIdle(packageName,
                         task.getUserId());
                 if (DEBUG) {
-                    Slog.d(LOG_TAG, "Plugged in " + pluggedIn + ", setting idle state of "
-                            + packageName + " to " + appIdle);
+                    Slog.d(LOG_TAG, "Setting idle state of " + packageName + " to " + appIdle);
                 }
                 if (task.appNotIdleConstraintSatisfied.get() == appIdle) {
                     task.appNotIdleConstraintSatisfied.set(!appIdle);
@@ -172,4 +122,41 @@
             mStateChangedListener.onControllerStateChanged();
         }
     }
+
+    private class AppIdleStateChangeListener
+            extends UsageStatsManagerInternal.AppIdleStateChangeListener {
+        @Override
+        public void onAppIdleStateChanged(String packageName, int userId, boolean idle) {
+            boolean changed = false;
+            synchronized (mTrackedTasks) {
+                if (mAppIdleParoleOn) {
+                    return;
+                }
+                for (JobStatus task : mTrackedTasks) {
+                    if (task.job.getService().getPackageName().equals(packageName)
+                            && task.getUserId() == userId) {
+                        if (task.appNotIdleConstraintSatisfied.get() != !idle) {
+                            if (DEBUG) {
+                                Slog.d(LOG_TAG, "App Idle state changed, setting idle state of "
+                                        + packageName + " to " + idle);
+                            }
+                            task.appNotIdleConstraintSatisfied.set(!idle);
+                            changed = true;
+                        }
+                    }
+                }
+            }
+            if (changed) {
+                mStateChangedListener.onControllerStateChanged();
+            }
+        }
+
+        @Override
+        public void onParoleStateChanged(boolean isParoleOn) {
+            if (DEBUG) {
+                Slog.d(LOG_TAG, "Parole on: " + isParoleOn);
+            }
+            setAppIdleParoleOn(isParoleOn);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index b0550d6..847bcb5 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -86,7 +86,6 @@
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.app.usage.UsageStatsManagerInternal;
-import android.app.usage.UsageStatsManagerInternal.AppIdleStateChangeListener;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -154,7 +153,6 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.IndentingPrintWriter;
-import com.android.server.DeviceIdleController;
 import com.android.server.LocalServices;
 import com.google.android.collect.Lists;
 
@@ -182,8 +180,7 @@
  * and delivers to listeners, such as {@link ConnectivityManager}, for
  * enforcement.
  */
-public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub
-        implements AppIdleStateChangeListener {
+public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
     private static final String TAG = "NetworkPolicy";
     private static final boolean LOGD = false;
     private static final boolean LOGV = false;
@@ -279,6 +276,7 @@
     final SparseIntArray mUidPolicy = new SparseIntArray();
     /** Currently derived rules for each UID. */
     final SparseIntArray mUidRules = new SparseIntArray();
+    /** Set of states for the child firewall chains. True if the chain is active. */
     final SparseBooleanArray mFirewallChainStates = new SparseBooleanArray();
 
     /**
@@ -508,7 +506,7 @@
                 WifiManager.NETWORK_STATE_CHANGED_ACTION);
         mContext.registerReceiver(mWifiStateReceiver, wifiStateFilter, null, mHandler);
 
-        mUsageStats.addAppIdleStateChangeListener(this);
+        mUsageStats.addAppIdleStateChangeListener(new AppIdleStateChangeListener());
 
     }
 
@@ -2043,7 +2041,12 @@
             }
             setUidFirewallRules(FIREWALL_CHAIN_DOZABLE, uidRules);
         }
-        enableFirewallChain(FIREWALL_CHAIN_DOZABLE, mDeviceIdleMode);
+        enableFirewallChainLocked(FIREWALL_CHAIN_DOZABLE, mDeviceIdleMode);
+    }
+
+    void updateRulesForAppIdleParoleLocked() {
+        boolean enableChain = !mUsageStats.isAppIdleParoleOn();
+        enableFirewallChainLocked(FIREWALL_CHAIN_STANDBY, enableChain);
     }
 
     /**
@@ -2187,9 +2190,9 @@
         final boolean firewallReject = (uidRules & RULE_REJECT_ALL) != 0;
         if (oldFirewallReject != firewallReject) {
             setUidFirewallRule(FIREWALL_CHAIN_STANDBY, uid, firewallReject);
-            if (mDeviceIdleMode && !firewallReject) {
-                // if we are in device idle mode, and we decide to allow this uid.  we need to punch
-                // a hole in the device idle chain.
+            if (mFirewallChainStates.get(FIREWALL_CHAIN_DOZABLE) && !firewallReject) {
+                // if the dozable chain is on, and we decide to allow this uid.  we need to punch
+                // a hole in the dozable chain.
                 setUidFirewallRule(FIREWALL_CHAIN_DOZABLE, uid, false);
             }
         }
@@ -2207,15 +2210,25 @@
         }
     }
 
-    @Override
-    public void onAppIdleStateChanged(String packageName, int userId, boolean idle) {
-        try {
-            int uid = mContext.getPackageManager().getPackageUid(packageName, userId);
-            synchronized (mRulesLock) {
-                updateRulesForUidLocked(uid);
+    private class AppIdleStateChangeListener
+            extends UsageStatsManagerInternal.AppIdleStateChangeListener {
+
+        @Override
+        public void onAppIdleStateChanged(String packageName, int userId, boolean idle) {
+            try {
+                int uid = mContext.getPackageManager().getPackageUid(packageName, userId);
+                synchronized (mRulesLock) {
+                    updateRulesForUidLocked(uid);
+                }
+            } catch (NameNotFoundException nnfe) {
             }
-        } catch (NameNotFoundException nnfe) {
-            return;
+        }
+
+        @Override
+        public void onParoleStateChanged(boolean isParoleOn) {
+            synchronized (mRulesLock) {
+                updateRulesForAppIdleParoleLocked();
+            }
         }
     }
 
@@ -2381,12 +2394,13 @@
     /**
      * Add or remove a uid to the firewall blacklist for all network ifaces.
      */
-    private void enableFirewallChain(int chain, boolean enable) {
+    private void enableFirewallChainLocked(int chain, boolean enable) {
         if (mFirewallChainStates.indexOfKey(chain) >= 0 &&
                 mFirewallChainStates.get(chain) == enable) {
             // All is the same, nothing to do.
             return;
         }
+        mFirewallChainStates.put(chain, enable);
         try {
             mNetworkManager.setFirewallChainEnabled(chain, enable);
         } catch (IllegalStateException e) {
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 490236e..c49a5f9 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -119,6 +119,7 @@
     static final int MSG_CHECK_PAROLE_TIMEOUT = 6;
     static final int MSG_PAROLE_END_TIMEOUT = 7;
     static final int MSG_REPORT_CONTENT_PROVIDER_USAGE = 8;
+    static final int MSG_PAROLE_STATE_CHANGED = 9;
 
     private final Object mLock = new Object();
     Handler mHandler;
@@ -313,7 +314,7 @@
                     mLastAppIdleParoledTime = checkAndGetTimeLocked();
                     postNextParoleTimeout();
                 }
-                postCheckIdleStates(UserHandle.USER_ALL);
+                postParoleStateChanged();
             }
         }
     }
@@ -338,6 +339,12 @@
         mHandler.sendEmptyMessageDelayed(MSG_PAROLE_END_TIMEOUT, mAppIdleParoleDurationMillis);
     }
 
+    private void postParoleStateChanged() {
+        if (DEBUG) Slog.d(TAG, "Posting MSG_PAROLE_STATE_CHANGED");
+        mHandler.removeMessages(MSG_PAROLE_STATE_CHANGED);
+        mHandler.sendEmptyMessage(MSG_PAROLE_STATE_CHANGED);
+    }
+
     void postCheckIdleStates(int userId) {
         mHandler.sendMessage(mHandler.obtainMessage(MSG_CHECK_IDLE_STATES, userId, 0));
     }
@@ -756,6 +763,13 @@
         }
     }
 
+    boolean isAppIdleFilteredOrParoled(String packageName, int userId, long timeNow) {
+        if (mAppIdleParoled) {
+            return false;
+        }
+        return isAppIdleFiltered(packageName, userId, timeNow);
+    }
+
     boolean isAppIdleFiltered(String packageName, int userId, long timeNow) {
         final UserUsageStatsService userService;
         final long screenOnTime;
@@ -782,13 +796,6 @@
         if (!mAppIdleEnabled) {
             return false;
         }
-        synchronized (mLock) {
-            // Temporary exemption, probably due to device charging or occasional allowance to
-            // be allowed to sync, etc.
-            if (mAppIdleParoled) {
-                return false;
-            }
-        }
         if (packageName.equals("android")) return false;
         try {
             if (mDeviceIdleController.isPowerSaveWhitelistApp(packageName)) {
@@ -846,6 +853,12 @@
         }
     }
 
+    void informParoleStateChanged() {
+        for (AppIdleStateChangeListener listener : mPackageAccessListeners) {
+            listener.onParoleStateChanged(mAppIdleParoled);
+        }
+    }
+
     private static boolean validRange(long currentTime, long beginTime, long endTime) {
         return beginTime <= currentTime && beginTime < endTime;
     }
@@ -975,6 +988,11 @@
                     args.recycle();
                     break;
 
+                case MSG_PAROLE_STATE_CHANGED:
+                    if (DEBUG) Slog.d(TAG, "Parole state changed: " + mAppIdleParoled);
+                    informParoleStateChanged();
+                    break;
+
                 default:
                     super.handleMessage(msg);
                     break;
@@ -1126,7 +1144,7 @@
             }
             final long token = Binder.clearCallingIdentity();
             try {
-                return UsageStatsService.this.isAppIdleFiltered(packageName, userId, -1);
+                return UsageStatsService.this.isAppIdleFilteredOrParoled(packageName, userId, -1);
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
@@ -1251,6 +1269,11 @@
         }
 
         @Override
+        public boolean isAppIdleParoleOn() {
+            return mAppIdleParoled;
+        }
+
+        @Override
         public void prepareShutdown() {
             // This method *WILL* do IO work, but we must block until it is finished or else
             // we might not shutdown cleanly. This is ok to do with the 'am' lock held, because
@@ -1261,6 +1284,7 @@
         @Override
         public void addAppIdleStateChangeListener(AppIdleStateChangeListener listener) {
             UsageStatsService.this.addListener(listener);
+            listener.onParoleStateChanged(isAppIdleParoleOn());
         }
 
         @Override