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/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;