diff options
author | Jeff Sharkey <jsharkey@android.com> | 2018-01-19 18:02:47 +0900 |
---|---|---|
committer | Jeff Sharkey <jsharkey@android.com> | 2018-01-19 18:18:51 +0900 |
commit | 2e4714575ff7aac65a0e107cbf2dd03fa3affe95 (patch) | |
tree | 91f96552a4d5769dfe0208bf325db43e47762939 | |
parent | 9252b34065809731ea2f6d3ffad91f678f809c93 (diff) |
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
6 files changed, 182 insertions, 12 deletions
diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java index 7b023f412cbc..621619c5134d 100644 --- a/core/java/com/android/internal/util/ArrayUtils.java +++ b/core/java/com/android/internal/util/ArrayUtils.java @@ -619,6 +619,10 @@ public class ArrayUtils { 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 b2fa294f77be..404cb83ffb5f 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 90be99cd6157..cfb2a148ea57 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 d817da53f523..7c6019e76416 100644 --- a/proto/src/system_messages.proto +++ b/proto/src/system_messages.proto @@ -193,6 +193,9 @@ message SystemMessage { // 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 a06b11a41024..1318fc833c20 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 static org.xmlpull.v1.XmlPullParser.START_TAG; 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.EventLogTags; 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 class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { 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 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { } }; + @VisibleForTesting + public void updateNotifications() { + synchronized (mNetworkPoliciesSecondLock) { + updateNotificationsNL(); + } + } + /** * Check {@link NetworkPolicy} against current {@link INetworkStatsService} * to show visible notifications as needed. @@ -1042,6 +1054,44 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { } } + // 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 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { 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 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { 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 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { } }; + @VisibleForTesting + public void updateNetworks() { + mConnReceiver.onReceive(null, null); + } + /** * Update mobile policies with data cycle information from {@link CarrierConfigManager} * if necessary. @@ -1471,7 +1541,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { 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 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { 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 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { 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 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { // 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 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { 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 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { @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 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { 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 fbcccf0fec2a..7c3082fb93de 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 @@ package com.android.server; 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.DATA_CYCLE_USE_PLATFORM_DEF 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.atLeastOnce; 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.INetworkManagementEventObserver; 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.filters.MediumTest; 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 @@ public class NetworkPolicyManagerServiceTest { 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 @@ public class NetworkPolicyManagerServiceTest { return super.getSystemService(name); } } + + @Override + public void enforceCallingOrSelfPermission(String permission, String message) { + // Assume that we're AID_SYSTEM + } }; setNetpolicyXml(context); @@ -1065,6 +1079,67 @@ public class NetworkPolicyManagerServiceTest { } @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; |