Alert user on rapid/heavy data usage.

Now that we have accurate information about a user's carrier data
plan, we can alert them if the current usage patterns would end up
with a nasty surprise towards the end of the current billing cycle.

For example, a single abusive app could use 90% of the user's budget
within the first few days of a billing cycle, leaving the user to
limp along for the remainder of the month.

The simple algorithm here extrapolates to see if the average usage
over the last 4 days would be more than 150% of the data limit for
the full billing cycle.  This period is short enough to catch rapid
recent usage, but long enough to smooth over short-term habit
changes, such as a weekend getaway.  This was chosen after
backtesting the proposed algorithm against real-world data usage
from a handful of internal users.

Fix NPMS unit tests, and write new ones, but leave the existing
@Ignored annotation intact for now.

Test: bit FrameworksServicesTests:com.android.server.NetworkPolicyManagerServiceTest
Bug: 64133169
Change-Id: I0d394b133257e8569a9aa2631b57638839d870ce
diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java
index 7b023f4..621619c 100644
--- a/core/java/com/android/internal/util/ArrayUtils.java
+++ b/core/java/com/android/internal/util/ArrayUtils.java
@@ -619,6 +619,10 @@
         return size - leftIdx;
     }
 
+    public static @NonNull int[] defeatNullable(@Nullable int[] val) {
+        return (val != null) ? val : EmptyArray.INT;
+    }
+
     public static @NonNull String[] defeatNullable(@Nullable String[] val) {
         return (val != null) ? val : EmptyArray.STRING;
     }
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index b2fa294..404cb83 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -3754,6 +3754,11 @@
     <!-- Notification body when background data usage is limited. -->
     <string name="data_usage_restricted_body">Tap to remove restriction.</string>
 
+    <!-- Notification title when there has been recent excessive data usage. [CHAR LIMIT=32] -->
+    <string name="data_usage_rapid_title">Large data usage</string>
+    <!-- Notification body when there has been recent excessive data usage. [CHAR LIMIT=128] -->
+    <string name="data_usage_rapid_body">Your data usage over the last few days is larger than normal. Tap to view usage and settings.</string>
+
     <!-- SSL Certificate dialogs -->
     <!-- Title for an SSL Certificate dialog -->
     <string name="ssl_certificate">Security certificate</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 90be99c..cfb2a14 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1973,6 +1973,8 @@
   <java-symbol type="string" name="data_usage_warning_title" />
   <java-symbol type="string" name="data_usage_wifi_limit_snoozed_title" />
   <java-symbol type="string" name="data_usage_wifi_limit_title" />
+  <java-symbol type="string" name="data_usage_rapid_title" />
+  <java-symbol type="string" name="data_usage_rapid_body" />
   <java-symbol type="string" name="default_wallpaper_component" />
   <java-symbol type="string" name="device_storage_monitor_notification_channel" />
   <java-symbol type="string" name="dlg_ok" />
diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto
index d817da5..7c6019e 100644
--- a/proto/src/system_messages.proto
+++ b/proto/src/system_messages.proto
@@ -193,6 +193,9 @@
     // Inform the user that Wifi Wake has automatically re-enabled Wifi
     NOTE_WIFI_WAKE_TURNED_BACK_ON = 44;
 
+    // Inform the user that unexpectedly rapid network usage is happening
+    NOTE_NET_RAPID = 45;
+
     // ADD_NEW_IDS_ABOVE_THIS_LINE
     // Legacy IDs with arbitrary values appear below
     // Legacy IDs existed as stable non-conflicting constants prior to the O release
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index a06b11a..1318fc8 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -99,6 +99,7 @@
 
 import android.Manifest;
 import android.annotation.IntDef;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
@@ -206,8 +207,10 @@
 import com.android.server.LocalServices;
 import com.android.server.ServiceThread;
 import com.android.server.SystemConfig;
+import com.android.server.SystemService;
 
 import libcore.io.IoUtils;
+import libcore.util.EmptyArray;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlSerializer;
@@ -283,6 +286,8 @@
     public static final int TYPE_LIMIT = SystemMessage.NOTE_NET_LIMIT;
     @VisibleForTesting
     public static final int TYPE_LIMIT_SNOOZED = SystemMessage.NOTE_NET_LIMIT_SNOOZED;
+    @VisibleForTesting
+    public static final int TYPE_RAPID = SystemMessage.NOTE_NET_RAPID;
 
     private static final String TAG_POLICY_LIST = "policy-list";
     private static final String TAG_NETWORK_POLICY = "network-policy";
@@ -998,6 +1003,13 @@
         }
     };
 
+    @VisibleForTesting
+    public void updateNotifications() {
+        synchronized (mNetworkPoliciesSecondLock) {
+            updateNotificationsNL();
+        }
+    }
+
     /**
      * Check {@link NetworkPolicy} against current {@link INetworkStatsService}
      * to show visible notifications as needed.
@@ -1042,6 +1054,44 @@
             }
         }
 
+        // Alert the user about heavy recent data usage that might result in
+        // going over their carrier limit.
+        for (int i = 0; i < mNetIdToSubId.size(); i++) {
+            final int subId = mNetIdToSubId.valueAt(i);
+            final SubscriptionPlan plan = getPrimarySubscriptionPlanLocked(subId);
+            if (plan == null) continue;
+
+            final long limitBytes = plan.getDataLimitBytes();
+            if (limitBytes == SubscriptionPlan.BYTES_UNKNOWN) {
+                // Ignore missing limits
+            } else if (limitBytes == SubscriptionPlan.BYTES_UNLIMITED) {
+                // Unlimited data; no rapid usage alerting
+            } else {
+                // Warn if average usage over last 4 days is on track to blow
+                // pretty far past the plan limits.
+                final long recentDuration = TimeUnit.DAYS.toMillis(4);
+                final long end = RecurrenceRule.sClock.millis();
+                final long start = end - recentDuration;
+
+                final NetworkTemplate template = NetworkTemplate.buildTemplateMobileAll(
+                        mContext.getSystemService(TelephonyManager.class).getSubscriberId(subId));
+                final long recentBytes = getTotalBytes(template, start, end);
+
+                final Pair<ZonedDateTime, ZonedDateTime> cycle = plan.cycleIterator().next();
+                final long cycleDuration = cycle.second.toInstant().toEpochMilli()
+                        - cycle.first.toInstant().toEpochMilli();
+
+                final long projectedBytes = (recentBytes * cycleDuration) / recentDuration;
+                final long alertBytes = (limitBytes * 3) / 2;
+                if (projectedBytes > alertBytes) {
+                    final NetworkPolicy policy = new NetworkPolicy(template, plan.getCycleRule(),
+                            NetworkPolicy.WARNING_DISABLED, NetworkPolicy.LIMIT_DISABLED,
+                            NetworkPolicy.SNOOZE_NEVER, NetworkPolicy.SNOOZE_NEVER, true, true);
+                    enqueueNotification(policy, TYPE_RAPID, 0);
+                }
+            }
+        }
+
         // cancel stale notifications that we didn't renew above
         for (int i = beforeNotifs.size()-1; i >= 0; i--) {
             final NotificationId notificationId = beforeNotifs.valueAt(i);
@@ -1063,7 +1113,7 @@
             final SubscriptionManager sub = SubscriptionManager.from(mContext);
 
             // Mobile template is relevant when any active subscriber matches
-            final int[] subIds = sub.getActiveSubscriptionIdList();
+            final int[] subIds = ArrayUtils.defeatNullable(sub.getActiveSubscriptionIdList());
             for (int subId : subIds) {
                 final String subscriberId = tele.getSubscriberId(subId);
                 final NetworkIdentity probeIdent = new NetworkIdentity(TYPE_MOBILE,
@@ -1200,6 +1250,21 @@
                         mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
                 break;
             }
+            case TYPE_RAPID: {
+                final CharSequence title = res.getText(R.string.data_usage_rapid_title);
+                body = res.getText(R.string.data_usage_rapid_body);
+
+                builder.setOngoing(true);
+                builder.setSmallIcon(R.drawable.stat_notify_error);
+                builder.setTicker(title);
+                builder.setContentTitle(title);
+                builder.setContentText(body);
+
+                final Intent intent = buildViewDataUsageIntent(res, policy.template);
+                builder.setContentIntent(PendingIntent.getActivity(
+                        mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
+                break;
+            }
         }
 
         // TODO: move to NotificationManager once we can mock it
@@ -1253,6 +1318,11 @@
         }
     };
 
+    @VisibleForTesting
+    public void updateNetworks() {
+        mConnReceiver.onReceive(null, null);
+    }
+
     /**
      * Update mobile policies with data cycle information from {@link CarrierConfigManager}
      * if necessary.
@@ -1471,7 +1541,7 @@
             final SubscriptionManager sm = SubscriptionManager.from(mContext);
             final TelephonyManager tm = TelephonyManager.from(mContext);
 
-            final int[] subIds = sm.getActiveSubscriptionIdList();
+            final int[] subIds = ArrayUtils.defeatNullable(sm.getActiveSubscriptionIdList());
             for (int subId : subIds) {
                 final String subscriberId = tm.getSubscriberId(subId);
                 final NetworkIdentity probeIdent = new NetworkIdentity(TYPE_MOBILE,
@@ -1510,7 +1580,7 @@
 
         final NetworkState[] states;
         try {
-            states = mConnManager.getAllNetworkState();
+            states = defeatNullable(mConnManager.getAllNetworkState());
         } catch (RemoteException e) {
             // ignored; service lives in system_server
             return;
@@ -1521,7 +1591,9 @@
         mNetIdToSubId.clear();
         final ArrayMap<NetworkState, NetworkIdentity> identified = new ArrayMap<>();
         for (NetworkState state : states) {
-            mNetIdToSubId.put(state.network.netId, parseSubId(state));
+            if (state.network != null) {
+                mNetIdToSubId.put(state.network.netId, parseSubId(state));
+            }
             if (state.networkInfo != null && state.networkInfo.isConnected()) {
                 final NetworkIdentity ident = NetworkIdentity.buildNetworkIdentity(mContext, state);
                 identified.put(state, ident);
@@ -1627,23 +1699,23 @@
         // TODO: add experiments support to disable or tweak ratios
         mSubscriptionOpportunisticQuota.clear();
         for (NetworkState state : states) {
+            if (state.network == null) continue;
             final int subId = getSubIdLocked(state.network);
-            final SubscriptionPlan[] plans = mSubscriptionPlans.get(subId);
-            final SubscriptionPlan plan = ArrayUtils.isEmpty(plans) ? null : plans[0];
+            final SubscriptionPlan plan = getPrimarySubscriptionPlanLocked(subId);
             if (plan == null) continue;
 
             // By default assume we have no quota
-            long limitBytes = plan.getDataLimitBytes();
             long quotaBytes = 0;
 
+            final long limitBytes = plan.getDataLimitBytes();
             if (limitBytes == SubscriptionPlan.BYTES_UNKNOWN) {
                 // Ignore missing limits
-            } else if (plan.getDataLimitBytes() == SubscriptionPlan.BYTES_UNLIMITED) {
+            } else if (limitBytes == SubscriptionPlan.BYTES_UNLIMITED) {
                 // Unlimited data; let's use 20MiB/day (600MiB/month)
                 quotaBytes = DataUnit.MEBIBYTES.toBytes(20);
             } else {
                 // Limited data; let's only use 10% of remaining budget
-                final Pair<ZonedDateTime, ZonedDateTime> cycle = plans[0].cycleIterator().next();
+                final Pair<ZonedDateTime, ZonedDateTime> cycle = plan.cycleIterator().next();
                 final long start = cycle.first.toInstant().toEpochMilli();
                 final long end = cycle.second.toInstant().toEpochMilli();
                 final long totalBytes = getTotalBytes(
@@ -1676,7 +1748,7 @@
         final TelephonyManager tele = TelephonyManager.from(mContext);
         final SubscriptionManager sub = SubscriptionManager.from(mContext);
 
-        final int[] subIds = sub.getActiveSubscriptionIdList();
+        final int[] subIds = ArrayUtils.defeatNullable(sub.getActiveSubscriptionIdList());
         for (int subId : subIds) {
             final String subscriberId = tele.getSubscriberId(subId);
             ensureActiveMobilePolicyAL(subId, subscriberId);
@@ -4503,8 +4575,8 @@
         @Override
         public SubscriptionPlan getSubscriptionPlan(Network network) {
             synchronized (mNetworkPoliciesSecondLock) {
-                final SubscriptionPlan[] plans = mSubscriptionPlans.get(getSubIdLocked(network));
-                return ArrayUtils.isEmpty(plans) ? null : plans[0];
+                final int subId = getSubIdLocked(network);
+                return getPrimarySubscriptionPlanLocked(subId);
             }
         }
 
@@ -4537,10 +4609,19 @@
         return mNetIdToSubId.get(network.netId, INVALID_SUBSCRIPTION_ID);
     }
 
+    private SubscriptionPlan getPrimarySubscriptionPlanLocked(int subId) {
+        final SubscriptionPlan[] plans = mSubscriptionPlans.get(subId);
+        return ArrayUtils.isEmpty(plans) ? null : plans[0];
+    }
+
     private static boolean hasRule(int uidRules, int rule) {
         return (uidRules & rule) != 0;
     }
 
+    private static @NonNull NetworkState[] defeatNullable(@Nullable NetworkState[] val) {
+        return (val != null) ? val : new NetworkState[0];
+    }
+
     private class NotificationId {
         private final String mTag;
         private final int mId;
diff --git a/services/tests/servicestests/src/com/android/server/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/NetworkPolicyManagerServiceTest.java
index fbcccf0..7c3082f 100644
--- a/services/tests/servicestests/src/com/android/server/NetworkPolicyManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/NetworkPolicyManagerServiceTest.java
@@ -18,6 +18,7 @@
 
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkPolicy.LIMIT_DISABLED;
 import static android.net.NetworkPolicy.SNOOZE_NEVER;
 import static android.net.NetworkPolicy.WARNING_DISABLED;
@@ -34,6 +35,7 @@
 import static android.telephony.CarrierConfigManager.KEY_DATA_LIMIT_THRESHOLD_BYTES_LONG;
 import static android.telephony.CarrierConfigManager.KEY_DATA_WARNING_THRESHOLD_BYTES_LONG;
 import static android.telephony.CarrierConfigManager.KEY_MONTHLY_DATA_CYCLE_DAY_INT;
+import static android.telephony.SubscriptionPlan.LIMIT_BEHAVIOR_THROTTLED;
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 import static android.text.format.Time.TIMEZONE_UTC;
 
@@ -62,6 +64,7 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -86,13 +89,16 @@
 import android.net.INetworkPolicyListener;
 import android.net.INetworkStatsService;
 import android.net.LinkProperties;
+import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
 import android.net.NetworkInfo.DetailedState;
 import android.net.NetworkPolicy;
 import android.net.NetworkState;
 import android.net.NetworkStats;
+import android.net.NetworkStatsHistory;
 import android.net.NetworkTemplate;
+import android.net.StringNetworkSpecifier;
 import android.os.Binder;
 import android.os.INetworkManagementService;
 import android.os.PersistableBundle;
@@ -105,9 +111,11 @@
 import android.support.test.runner.AndroidJUnit4;
 import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
+import android.telephony.SubscriptionPlan;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.text.format.Time;
+import android.util.DataUnit;
 import android.util.Log;
 import android.util.Pair;
 import android.util.RecurrenceRule;
@@ -186,6 +194,7 @@
     private static final long TEST_START = 1194220800000L;
     private static final String TEST_IFACE = "test0";
     private static final String TEST_SSID = "AndroidAP";
+    private static final String TEST_IMSI = "310210";
 
     private static NetworkTemplate sTemplateWifi = NetworkTemplate.buildTemplateWifi(TEST_SSID);
 
@@ -309,6 +318,11 @@
                         return super.getSystemService(name);
                 }
             }
+
+            @Override
+            public void enforceCallingOrSelfPermission(String permission, String message) {
+                // Assume that we're AID_SYSTEM
+            }
         };
 
         setNetpolicyXml(context);
@@ -1065,6 +1079,67 @@
     }
 
     @Test
+    public void testRapidNotification() throws Exception {
+        // Create a place to store fake usage
+        final NetworkStatsHistory history = new NetworkStatsHistory(TimeUnit.HOURS.toMillis(1));
+        when(mStatsService.getNetworkTotalBytes(any(), anyLong(), anyLong()))
+                .thenAnswer(new Answer<Long>() {
+                    @Override
+                    public Long answer(InvocationOnMock invocation) throws Throwable {
+                        final NetworkStatsHistory.Entry entry = history.getValues(
+                                invocation.getArgument(1), invocation.getArgument(2), null);
+                        return entry.rxBytes + entry.txBytes;
+                    }
+                });
+
+        // Define simple data plan which gives us effectively 60MB/day
+        final SubscriptionPlan plan = SubscriptionPlan.Builder
+                .createRecurringMonthly(ZonedDateTime.parse("2015-11-01T00:00:00.00Z"))
+                .setDataLimit(DataUnit.MEGABYTES.toBytes(1800), LIMIT_BEHAVIOR_THROTTLED)
+                .build();
+        mService.setSubscriptionPlans(42, new SubscriptionPlan[] { plan },
+                mServiceContext.getOpPackageName());
+
+        // And get that active network in place
+        when(mConnManager.getAllNetworkState()).thenReturn(new NetworkState[] {
+                new NetworkState(null, new LinkProperties(),
+                        new NetworkCapabilities().addTransportType(TRANSPORT_CELLULAR)
+                                .setNetworkSpecifier(new StringNetworkSpecifier("42")),
+                        new Network(42), TEST_IMSI, null)
+        });
+        mService.updateNetworks();
+
+        // We're 20% through the month (6 days)
+        final long start = parseTime("2015-11-01T00:00Z");
+        final long end = parseTime("2015-11-07T00:00Z");
+        setCurrentTimeMillis(end);
+
+        // Using 20% of data in 20% is normal
+        {
+            history.removeBucketsBefore(Long.MAX_VALUE);
+            history.recordData(start, end,
+                    new NetworkStats.Entry(DataUnit.MEGABYTES.toBytes(360), 0L, 0L, 0L, 0));
+
+            reset(mNotifManager);
+            mService.updateNotifications();
+            verify(mNotifManager, never()).enqueueNotificationWithTag(any(), any(), any(),
+                    anyInt(), any(), anyInt());
+        }
+
+        // Using 80% data in 20% time is alarming
+        {
+            history.removeBucketsBefore(Long.MAX_VALUE);
+            history.recordData(start, end,
+                    new NetworkStats.Entry(DataUnit.MEGABYTES.toBytes(1440), 0L, 0L, 0L, 0));
+
+            reset(mNotifManager);
+            mService.updateNotifications();
+            verify(mNotifManager, atLeastOnce()).enqueueNotificationWithTag(any(), any(), any(),
+                    anyInt(), any(), anyInt());
+        }
+    }
+
+    @Test
     public void testMeteredNetworkWithoutLimit() throws Exception {
         NetworkState[] state = null;
         NetworkStats stats = null;