Data usage warning and limit notifications.

Watch for network statistics to cross NetworkPolicy warning or limit,
and show notifications to user as needed.  Currently checks during
any statistics update, but will eventually move to event registration
through netd when kernel supports.

Fixed accounting bug in getSummaryForNetwork().  Only apply UID policy
to applications; applying to system processes could break critical
services like RIL.

Change-Id: Iac0f20e910e205f3cbc54ec96395ff268b1aa379
diff --git a/core/java/android/net/NetworkPolicyManager.java b/core/java/android/net/NetworkPolicyManager.java
index 13ece40..e9d65e6 100644
--- a/core/java/android/net/NetworkPolicyManager.java
+++ b/core/java/android/net/NetworkPolicyManager.java
@@ -168,6 +168,15 @@
         time.normalize(true);
     }
 
+    /**
+     * Check if given UID can have a {@link #setUidPolicy(int, int)} defined,
+     * usually to protect critical system services.
+     */
+    public static boolean isUidValidForPolicy(Context context, int uid) {
+        return (uid >= android.os.Process.FIRST_APPLICATION_UID
+                && uid <= android.os.Process.LAST_APPLICATION_UID);
+    }
+
     /** {@hide} */
     public static void dumpPolicy(PrintWriter fout, int policy) {
         fout.write("[");
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 966dbe5..5e13282 100755
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -2932,4 +2932,19 @@
 
     <!-- Button text for the edit menu in input method extract mode. [CHAR LIMIT=16] -->
     <string name="extract_edit_menu_button">Edit...</string>
+
+    <!-- Notification title when data usage has exceeded warning threshold. [CHAR LIMIT=32] -->
+    <string name="data_usage_warning_title">Data usage warning</string>
+    <!-- Notification body when data usage has exceeded warning threshold. [CHAR LIMIT=32] -->
+    <string name="data_usage_warning_body">usage exceeds <xliff:g id="size" example="3.8GB">%s</xliff:g></string>
+
+    <!-- Notification title when 2G-3G data usage has exceeded limit threshold, and has been disabled. [CHAR LIMIT=32] -->
+    <string name="data_usage_3g_limit_title">2G-3G data disabled</string>
+    <!-- Notification title when 4G data usage has exceeded limit threshold, and has been disabled. [CHAR LIMIT=32] -->
+    <string name="data_usage_4g_limit_title">4G data disabled</string>
+    <!-- Notification title when mobile data usage has exceeded limit threshold, and has been disabled. [CHAR LIMIT=32] -->
+    <string name="data_usage_mobile_limit_title">Mobile data disabled</string>
+    <!-- Notification body when data usage has exceeded limit threshold, and has been disabled. [CHAR LIMIT=32] -->
+    <string name="data_usage_limit_body">tap to enable</string>
+
 </resources>
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index a820139..2769004 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -329,6 +329,7 @@
                 Slog.i(TAG, "Notification Manager");
                 notification = new NotificationManagerService(context, statusBar, lights);
                 ServiceManager.addService(Context.NOTIFICATION_SERVICE, notification);
+                networkPolicy.bindNotificationManager(notification);
             } catch (Throwable e) {
                 Slog.e(TAG, "Failure starting Notification Manager", e);
             }
diff --git a/services/java/com/android/server/net/NetworkPolicyManagerService.java b/services/java/com/android/server/net/NetworkPolicyManagerService.java
index 99569a8..2164334 100644
--- a/services/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -20,9 +20,11 @@
 import static android.Manifest.permission.DUMP;
 import static android.Manifest.permission.MANAGE_APP_TOKENS;
 import static android.Manifest.permission.MANAGE_NETWORK_POLICY;
+import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY;
 import static android.Manifest.permission.READ_PHONE_STATE;
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.NetworkPolicy.LIMIT_DISABLED;
+import static android.net.NetworkPolicy.WARNING_DISABLED;
 import static android.net.NetworkPolicyManager.POLICY_NONE;
 import static android.net.NetworkPolicyManager.POLICY_REJECT_PAID_BACKGROUND;
 import static android.net.NetworkPolicyManager.RULE_ALLOW_ALL;
@@ -30,19 +32,27 @@
 import static android.net.NetworkPolicyManager.computeLastCycleBoundary;
 import static android.net.NetworkPolicyManager.dumpPolicy;
 import static android.net.NetworkPolicyManager.dumpRules;
+import static android.net.NetworkPolicyManager.isUidValidForPolicy;
+import static android.net.TrafficStats.TEMPLATE_MOBILE_3G_LOWER;
+import static android.net.TrafficStats.TEMPLATE_MOBILE_4G;
 import static android.net.TrafficStats.TEMPLATE_MOBILE_ALL;
 import static android.net.TrafficStats.isNetworkTemplateMobile;
 import static android.text.format.DateUtils.DAY_IN_MILLIS;
 import static com.android.internal.util.Preconditions.checkNotNull;
+import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_UPDATED;
 import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
 import static org.xmlpull.v1.XmlPullParser.START_TAG;
 
 import android.app.IActivityManager;
+import android.app.INotificationManager;
 import android.app.IProcessObserver;
+import android.app.Notification;
+import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.res.Resources;
 import android.net.ConnectivityManager;
 import android.net.IConnectivityManager;
 import android.net.INetworkPolicyListener;
@@ -58,6 +68,7 @@
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.telephony.TelephonyManager;
+import android.text.format.Formatter;
 import android.text.format.Time;
 import android.util.NtpTrustedTime;
 import android.util.Slog;
@@ -67,6 +78,7 @@
 import android.util.TrustedTime;
 import android.util.Xml;
 
+import com.android.internal.R;
 import com.android.internal.os.AtomicFile;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.Objects;
@@ -110,6 +122,9 @@
     private static final long MB_IN_BYTES = KB_IN_BYTES * 1024;
     private static final long GB_IN_BYTES = MB_IN_BYTES * 1024;
 
+    private static final int TYPE_WARNING = 0x1;
+    private static final int TYPE_LIMIT = 0x2;
+
     private static final String TAG_POLICY_LIST = "policy-list";
     private static final String TAG_NETWORK_POLICY = "network-policy";
     private static final String TAG_UID_POLICY = "uid-policy";
@@ -123,6 +138,11 @@
     private static final String ATTR_UID = "uid";
     private static final String ATTR_POLICY = "policy";
 
+    public static final String ACTION_DATA_USAGE_WARNING =
+            "android.intent.action.DATA_USAGE_WARNING";
+    public static final String ACTION_DATA_USAGE_LIMIT =
+            "android.intent.action.DATA_USAGE_LIMIT";
+
     private static final long TIME_CACHE_MAX_AGE = DAY_IN_MILLIS;
 
     private final Context mContext;
@@ -132,6 +152,7 @@
     private final TrustedTime mTime;
 
     private IConnectivityManager mConnManager;
+    private INotificationManager mNotifManager;
 
     private final Object mRulesLock = new Object();
 
@@ -192,10 +213,15 @@
         mConnManager = checkNotNull(connManager, "missing IConnectivityManager");
     }
 
+    public void bindNotificationManager(INotificationManager notifManager) {
+        mNotifManager = checkNotNull(notifManager, "missing INotificationManager");
+    }
+
     public void systemReady() {
         synchronized (mRulesLock) {
             // read policy from disk
             readPolicyLocked();
+            updateNotificationsLocked();
         }
 
         updateScreenOn();
@@ -221,6 +247,11 @@
         ifaceFilter.addAction(CONNECTIVITY_ACTION);
         mContext.registerReceiver(mIfaceReceiver, ifaceFilter, CONNECTIVITY_INTERNAL, mHandler);
 
+        // listen for warning polling events; currently dispatched by
+        final IntentFilter statsFilter = new IntentFilter(ACTION_NETWORK_STATS_UPDATED);
+        mContext.registerReceiver(
+                mStatsReceiver, statsFilter, READ_NETWORK_USAGE_HISTORY, mHandler);
+
     }
 
     private IProcessObserver mProcessObserver = new IProcessObserver.Stub() {
@@ -229,6 +260,9 @@
             // only someone like AMS should only be calling us
             mContext.enforceCallingOrSelfPermission(MANAGE_APP_TOKENS, TAG);
 
+            // skip when UID couldn't have any policy
+            if (!isUidValidForPolicy(mContext, uid)) return;
+
             synchronized (mRulesLock) {
                 // because a uid can have multiple pids running inside, we need to
                 // remember all pid states and summarize foreground at uid level.
@@ -249,6 +283,9 @@
             // only someone like AMS should only be calling us
             mContext.enforceCallingOrSelfPermission(MANAGE_APP_TOKENS, TAG);
 
+            // skip when UID couldn't have any policy
+            if (!isUidValidForPolicy(mContext, uid)) return;
+
             synchronized (mRulesLock) {
                 // clear records and recompute, when they exist
                 final SparseBooleanArray pidForeground = mUidPidForeground.get(uid);
@@ -272,6 +309,158 @@
     };
 
     /**
+     * Receiver that watches for {@link INetworkStatsService} updates, which we
+     * use to check against {@link NetworkPolicy#warningBytes}.
+     */
+    private BroadcastReceiver mStatsReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            // on background handler thread, and verified
+            // READ_NETWORK_USAGE_HISTORY permission above.
+
+            synchronized (mRulesLock) {
+                updateNotificationsLocked();
+            }
+        }
+    };
+
+    /**
+     * Check {@link NetworkPolicy} against current {@link INetworkStatsService}
+     * to show visible notifications as needed.
+     */
+    private void updateNotificationsLocked() {
+        if (LOGV) Slog.v(TAG, "updateNotificationsLocked()");
+
+        // try refreshing time source when stale
+        if (mTime.getCacheAge() > TIME_CACHE_MAX_AGE) {
+            mTime.forceRefresh();
+        }
+
+        final long currentTime = mTime.hasCache() ? mTime.currentTimeMillis()
+                : System.currentTimeMillis();
+
+        // TODO: when switching to kernel notifications, compute next future
+        // cycle boundary to recompute notifications.
+
+        // examine stats for each policy defined
+        for (NetworkPolicy policy : mNetworkPolicy) {
+            final long start = computeLastCycleBoundary(currentTime, policy);
+            final long end = currentTime;
+
+            final long total;
+            try {
+                final NetworkStats stats = mNetworkStats.getSummaryForNetwork(
+                        start, end, policy.networkTemplate, policy.subscriberId);
+                total = stats.rx[0] + stats.tx[0];
+            } catch (RemoteException e) {
+                Slog.w(TAG, "problem reading summary for template " + policy.networkTemplate);
+                continue;
+            }
+
+            if (policy.limitBytes != LIMIT_DISABLED && total >= policy.limitBytes) {
+                cancelNotification(policy, TYPE_WARNING);
+                enqueueNotification(policy, TYPE_LIMIT);
+            } else {
+                cancelNotification(policy, TYPE_LIMIT);
+
+                if (policy.warningBytes != WARNING_DISABLED && total >= policy.warningBytes) {
+                    enqueueNotification(policy, TYPE_WARNING);
+                } else {
+                    cancelNotification(policy, TYPE_WARNING);
+                }
+            }
+        }
+    }
+
+    /**
+     * Build unique tag that identifies an active {@link NetworkPolicy}
+     * notification of a specific type, like {@link #TYPE_LIMIT}.
+     */
+    private String buildNotificationTag(NetworkPolicy policy, int type) {
+        // TODO: consider splicing subscriberId hash into mix
+        return TAG + ":" + policy.networkTemplate + ":" + type;
+    }
+
+    /**
+     * Show notification for combined {@link NetworkPolicy} and specific type,
+     * like {@link #TYPE_LIMIT}. Okay to call multiple times.
+     */
+    private void enqueueNotification(NetworkPolicy policy, int type) {
+        final String tag = buildNotificationTag(policy, type);
+        final Notification.Builder builder = new Notification.Builder(mContext);
+        builder.setOnlyAlertOnce(true);
+        builder.setOngoing(true);
+
+        final Resources res = mContext.getResources();
+        switch (type) {
+            case TYPE_WARNING: {
+                final String title = res.getString(R.string.data_usage_warning_title);
+                final String body = res.getString(R.string.data_usage_warning_body,
+                        Formatter.formatFileSize(mContext, policy.warningBytes));
+
+                builder.setSmallIcon(R.drawable.ic_menu_info_details);
+                builder.setTicker(title);
+                builder.setContentTitle(title);
+                builder.setContentText(body);
+                builder.setContentIntent(PendingIntent.getActivity(mContext, 0,
+                        new Intent(ACTION_DATA_USAGE_WARNING),
+                        PendingIntent.FLAG_UPDATE_CURRENT));
+                break;
+            }
+            case TYPE_LIMIT: {
+                final String title;
+                final String body = res.getString(R.string.data_usage_limit_body);
+                switch (policy.networkTemplate) {
+                    case TEMPLATE_MOBILE_3G_LOWER:
+                        title = res.getString(R.string.data_usage_3g_limit_title);
+                        break;
+                    case TEMPLATE_MOBILE_4G:
+                        title = res.getString(R.string.data_usage_4g_limit_title);
+                        break;
+                    default:
+                        title = res.getString(R.string.data_usage_mobile_limit_title);
+                        break;
+                }
+
+                builder.setSmallIcon(com.android.internal.R.drawable.ic_menu_block);
+                builder.setTicker(title);
+                builder.setContentTitle(title);
+                builder.setContentText(body);
+                builder.setContentIntent(PendingIntent.getActivity(mContext, 0,
+                        new Intent(ACTION_DATA_USAGE_LIMIT),
+                        PendingIntent.FLAG_UPDATE_CURRENT));
+                break;
+            }
+        }
+
+        // TODO: move to NotificationManager once we can mock it
+        try {
+            final String packageName = mContext.getPackageName();
+            final int[] idReceived = new int[1];
+            mNotifManager.enqueueNotificationWithTag(
+                    packageName, tag, 0x0, builder.getNotification(), idReceived);
+        } catch (RemoteException e) {
+            Slog.w(TAG, "problem during enqueueNotification: " + e);
+        }
+    }
+
+    /**
+     * Cancel any notification for combined {@link NetworkPolicy} and specific
+     * type, like {@link #TYPE_LIMIT}.
+     */
+    private void cancelNotification(NetworkPolicy policy, int type) {
+        final String tag = buildNotificationTag(policy, type);
+
+        // TODO: move to NotificationManager once we can mock it
+        try {
+            final String packageName = mContext.getPackageName();
+            mNotifManager.cancelNotificationWithTag(packageName, tag, 0x0);
+        } catch (RemoteException e) {
+            Slog.w(TAG, "problem during enqueueNotification: " + e);
+        }
+    }
+
+    /**
      * Receiver that watches for {@link IConnectivityManager} to claim network
      * interfaces. Used to apply {@link NetworkPolicy} to matching networks.
      */
@@ -372,8 +561,7 @@
             if (policy.limitBytes != NetworkPolicy.LIMIT_DISABLED) {
                 // remaining "quota" is based on usage in current cycle
                 final long quota = Math.max(0, policy.limitBytes - total);
-
-                // TODO: push quota rule down through NMS
+                //kernelSetIfacesQuota(ifaces, quota);
             }
         }
     }
@@ -447,7 +635,11 @@
                         final int uid = readIntAttribute(in, ATTR_UID);
                         final int policy = readIntAttribute(in, ATTR_POLICY);
 
-                        mUidPolicy.put(uid, policy);
+                        if (isUidValidForPolicy(mContext, uid)) {
+                            setUidPolicyUnchecked(uid, policy, false);
+                        } else {
+                            Slog.w(TAG, "unable to apply policy to UID " + uid + "; ignoring");
+                        }
                     }
                 }
             }
@@ -495,6 +687,9 @@
                 final int uid = mUidPolicy.keyAt(i);
                 final int policy = mUidPolicy.valueAt(i);
 
+                // skip writing empty policies
+                if (policy == POLICY_NONE) continue;
+
                 out.startTag(null, TAG_UID_POLICY);
                 writeIntAttribute(out, ATTR_UID, uid);
                 writeIntAttribute(out, ATTR_POLICY, policy);
@@ -516,6 +711,14 @@
     public void setUidPolicy(int uid, int policy) {
         mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
 
+        if (!isUidValidForPolicy(mContext, uid)) {
+            throw new IllegalArgumentException("cannot apply policy to UID " + uid);
+        }
+
+        setUidPolicyUnchecked(uid, policy, true);
+    }
+
+    private void setUidPolicyUnchecked(int uid, int policy, boolean persist) {
         final int oldPolicy;
         synchronized (mRulesLock) {
             oldPolicy = getUidPolicy(uid);
@@ -523,7 +726,9 @@
 
             // uid policy changed, recompute rules and persist policy.
             updateRulesForUidLocked(uid);
-            writePolicyLocked();
+            if (persist) {
+                writePolicyLocked();
+            }
         }
     }
 
@@ -572,6 +777,7 @@
             }
 
             updateIfacesLocked();
+            updateNotificationsLocked();
             writePolicyLocked();
         }
     }
@@ -640,6 +846,8 @@
 
     @Override
     public boolean isUidForeground(int uid) {
+        mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
+
         synchronized (mRulesLock) {
             // only really in foreground when screen is also on
             return mUidForeground.get(uid, false) && mScreenOn;
@@ -696,6 +904,8 @@
     }
 
     private void updateRulesForUidLocked(int uid) {
+        if (!isUidValidForPolicy(mContext, uid)) return;
+
         final int uidPolicy = getUidPolicy(uid);
         final boolean uidForeground = isUidForeground(uid);
 
@@ -711,6 +921,9 @@
         // record rule locally to dispatch to new listeners
         mUidRules.put(uid, uidRules);
 
+        final boolean rejectPaid = (uidRules & RULE_REJECT_PAID) != 0;
+        //kernelSetUidRejectPaid(uid, rejectPaid);
+
         // dispatch changed rule to existing listeners
         final int length = mListeners.beginBroadcast();
         for (int i = 0; i < length; i++) {
diff --git a/services/java/com/android/server/net/NetworkStatsService.java b/services/java/com/android/server/net/NetworkStatsService.java
index de69849..f762123 100644
--- a/services/java/com/android/server/net/NetworkStatsService.java
+++ b/services/java/com/android/server/net/NetworkStatsService.java
@@ -106,6 +106,8 @@
     // @VisibleForTesting
     public static final String ACTION_NETWORK_STATS_POLL =
             "com.android.server.action.NETWORK_STATS_POLL";
+    public static final String ACTION_NETWORK_STATS_UPDATED =
+            "com.android.server.action.NETWORK_STATS_UPDATED";
 
     private PendingIntent mPollIntent;
 
@@ -203,7 +205,6 @@
         mContext.registerReceiver(mIfaceReceiver, ifaceFilter, CONNECTIVITY_INTERNAL, mHandler);
 
         // listen for periodic polling events
-        // TODO: switch to stronger internal permission
         final IntentFilter pollFilter = new IntentFilter(ACTION_NETWORK_STATS_POLL);
         mContext.registerReceiver(mPollReceiver, pollFilter, READ_NETWORK_USAGE_HISTORY, mHandler);
 
@@ -298,7 +299,7 @@
             }
 
             final NetworkStats stats = new NetworkStats(end - start, 1);
-            stats.addEntry(IFACE_ALL, UID_ALL, tx, tx);
+            stats.addEntry(IFACE_ALL, UID_ALL, rx, tx);
             return stats;
         }
     }
@@ -446,6 +447,11 @@
                 break;
             }
         }
+
+        // finally, dispatch updated event to any listeners
+        final Intent updatedIntent = new Intent(ACTION_NETWORK_STATS_UPDATED);
+        updatedIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+        mContext.sendBroadcast(updatedIntent, READ_NETWORK_USAGE_HISTORY);
     }
 
     /**