From 84e6f1232c92988617a1155fd7356f90e6c84b4e Mon Sep 17 00:00:00 2001 From: Lorenzo Colitti Date: Mon, 29 Aug 2016 14:03:11 +0900 Subject: [PATCH] Network switch notifications: rate & daily limits This patch adds a daily limit to the maximum number of notifications shown when switching networks. It also adds a rate limit to prevent rapid successive notifications in flapping scenarios. Bug: 31132499 Change-Id: Iccb6d0899646ea6df3cfad32a421922263e0eb85 --- core/java/android/provider/Settings.java | 52 +++++++---- .../com/android/server/ConnectivityService.java | 9 +- .../android/server/connectivity/LingerMonitor.java | 87 +++++++++++++----- .../server/connectivity/LingerMonitorTest.java | 100 +++++++++++++++++++-- 4 files changed, 198 insertions(+), 50 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 300deea1c873..515a91c4d422 100755 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7405,27 +7405,41 @@ public final class Settings { public static final String WEBVIEW_DATA_REDUCTION_PROXY_KEY = "webview_data_reduction_proxy_key"; - /** - * Whether or not the WebView fallback mechanism should be enabled. - * 0=disabled, 1=enabled. - * @hide - */ - public static final String WEBVIEW_FALLBACK_LOGIC_ENABLED = - "webview_fallback_logic_enabled"; + /** + * Whether or not the WebView fallback mechanism should be enabled. + * 0=disabled, 1=enabled. + * @hide + */ + public static final String WEBVIEW_FALLBACK_LOGIC_ENABLED = + "webview_fallback_logic_enabled"; - /** - * Name of the package used as WebView provider (if unset the provider is instead determined - * by the system). - * @hide - */ - public static final String WEBVIEW_PROVIDER = "webview_provider"; + /** + * Name of the package used as WebView provider (if unset the provider is instead determined + * by the system). + * @hide + */ + public static final String WEBVIEW_PROVIDER = "webview_provider"; - /** - * Developer setting to enable WebView multiprocess rendering. - * @hide - */ - @SystemApi - public static final String WEBVIEW_MULTIPROCESS = "webview_multiprocess"; + /** + * Developer setting to enable WebView multiprocess rendering. + * @hide + */ + @SystemApi + public static final String WEBVIEW_MULTIPROCESS = "webview_multiprocess"; + + /** + * The maximum number of notifications shown in 24 hours when switching networks. + * @hide + */ + public static final String NETWORK_SWITCH_NOTIFICATION_DAILY_LIMIT = + "network_switch_notification_daily_limit"; + + /** + * The minimum time in milliseconds between notifications when switching networks. + * @hide + */ + public static final String NETWORK_SWITCH_NOTIFICATION_RATE_LIMIT_MILLIS = + "network_switch_notification_rate_limit_millis"; /** * Whether Wifi display is enabled/disabled diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index 86afe08cedce..58431c856dcf 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -838,7 +838,14 @@ public class ConnectivityService extends IConnectivityManager.Stub mKeepaliveTracker = new KeepaliveTracker(mHandler); mNotifier = new NetworkNotificationManager(mContext, mTelephonyManager, mContext.getSystemService(NotificationManager.class)); - mLingerMonitor = new LingerMonitor(mContext, mNotifier); + + final int dailyLimit = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.NETWORK_SWITCH_NOTIFICATION_DAILY_LIMIT, + LingerMonitor.DEFAULT_NOTIFICATION_DAILY_LIMIT); + final long rateLimit = Settings.Global.getLong(mContext.getContentResolver(), + Settings.Global.NETWORK_SWITCH_NOTIFICATION_RATE_LIMIT_MILLIS, + LingerMonitor.DEFAULT_NOTIFICATION_RATE_LIMIT_MILLIS); + mLingerMonitor = new LingerMonitor(mContext, mNotifier, dailyLimit, rateLimit); } private NetworkRequest createInternetRequestForTransport(int transportType) { diff --git a/services/core/java/com/android/server/connectivity/LingerMonitor.java b/services/core/java/com/android/server/connectivity/LingerMonitor.java index 1ffccdd33405..635db19521d6 100644 --- a/services/core/java/com/android/server/connectivity/LingerMonitor.java +++ b/services/core/java/com/android/server/connectivity/LingerMonitor.java @@ -17,12 +17,14 @@ package com.android.server.connectivity; import android.app.PendingIntent; -import android.net.NetworkCapabilities; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.net.NetworkCapabilities; +import android.os.SystemClock; import android.os.UserHandle; import android.text.TextUtils; +import android.text.format.DateUtils; import android.util.Log; import android.util.SparseArray; import android.util.SparseIntArray; @@ -30,6 +32,7 @@ import android.util.SparseBooleanArray; import java.util.Arrays; import java.util.HashMap; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.MessageUtils; import com.android.server.connectivity.NetworkNotificationManager; @@ -50,6 +53,9 @@ public class LingerMonitor { private static final boolean VDBG = false; private static final String TAG = LingerMonitor.class.getSimpleName(); + public static final int DEFAULT_NOTIFICATION_DAILY_LIMIT = 3; + public static final long DEFAULT_NOTIFICATION_RATE_LIMIT_MILLIS = DateUtils.MINUTE_IN_MILLIS; + private static final HashMap TRANSPORT_NAMES = makeTransportToNameMap(); @VisibleForTesting public static final Intent CELLULAR_SETTINGS = new Intent().setComponent(new ComponentName( @@ -65,6 +71,12 @@ public class LingerMonitor { private final Context mContext; private final NetworkNotificationManager mNotifier; + private final int mDailyLimit; + private final long mRateLimitMillis; + + private long mFirstNotificationMillis; + private long mLastNotificationMillis; + private int mNotificationCounter; /** Current notifications. Maps the netId we switched away from to the netId we switched to. */ private final SparseIntArray mNotifications = new SparseIntArray(); @@ -72,9 +84,12 @@ public class LingerMonitor { /** Whether we ever notified that we switched away from a particular network. */ private final SparseBooleanArray mEverNotified = new SparseBooleanArray(); - public LingerMonitor(Context context, NetworkNotificationManager notifier) { + public LingerMonitor(Context context, NetworkNotificationManager notifier, + int dailyLimit, long rateLimitMillis) { mContext = context; mNotifier = notifier; + mDailyLimit = dailyLimit; + mRateLimitMillis = rateLimitMillis; } private static HashMap makeTransportToNameMap() { @@ -109,8 +124,8 @@ public class LingerMonitor { @VisibleForTesting public boolean isNotificationEnabled(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) { // TODO: Evaluate moving to CarrierConfigManager. - String[] notifySwitches = mContext.getResources().getStringArray( - com.android.internal.R.array.config_networkNotifySwitches); + String[] notifySwitches = + mContext.getResources().getStringArray(R.array.config_networkNotifySwitches); if (VDBG) { Log.d(TAG, "Notify on network switches: " + Arrays.toString(notifySwitches)); @@ -156,41 +171,37 @@ public class LingerMonitor { // Notify the user of a network switch using a notification or a toast. private void notify(NetworkAgentInfo fromNai, NetworkAgentInfo toNai, boolean forceToast) { - boolean notify = false; - int notifyType = mContext.getResources().getInteger( - com.android.internal.R.integer.config_networkNotifySwitchType); - + int notifyType = + mContext.getResources().getInteger(R.integer.config_networkNotifySwitchType); if (notifyType == NOTIFY_TYPE_NOTIFICATION && forceToast) { notifyType = NOTIFY_TYPE_TOAST; } + if (VDBG) { + Log.d(TAG, "Notify type: " + sNotifyTypeNames.get(notifyType, "" + notifyType)); + } + switch (notifyType) { case NOTIFY_TYPE_NONE: - break; + return; case NOTIFY_TYPE_NOTIFICATION: showNotification(fromNai, toNai); - notify = true; break; case NOTIFY_TYPE_TOAST: mNotifier.showToast(fromNai, toNai); - notify = true; break; default: Log.e(TAG, "Unknown notify type " + notifyType); + return; } - if (VDBG) { - Log.d(TAG, "Notify type: " + sNotifyTypeNames.get(notifyType, "" + notifyType)); + if (DBG) { + Log.d(TAG, "Notifying switch from=" + fromNai.name() + " to=" + toNai.name() + + " type=" + sNotifyTypeNames.get(notifyType, "unknown(" + notifyType + ")")); } - if (notify) { - if (DBG) { - Log.d(TAG, "Notifying switch from=" + fromNai.name() + " to=" + toNai.name() + - " type=" + sNotifyTypeNames.get(notifyType, "unknown(" + notifyType + ")")); - } - mNotifications.put(fromNai.network.netId, toNai.network.netId); - mEverNotified.put(fromNai.network.netId, true); - } + mNotifications.put(fromNai.network.netId, toNai.network.netId); + mEverNotified.put(fromNai.network.netId, true); } // The default network changed from fromNai to toNai due to a change in score. @@ -251,9 +262,12 @@ public class LingerMonitor { // unvalidated. if (fromNai.lastValidated) return; - if (isNotificationEnabled(fromNai, toNai)) { - notify(fromNai, toNai, forceToast); - } + if (!isNotificationEnabled(fromNai, toNai)) return; + + final long now = SystemClock.elapsedRealtime(); + if (isRateLimited(now) || isAboveDailyLimit(now)) return; + + notify(fromNai, toNai, forceToast); } public void noteDisconnect(NetworkAgentInfo nai) { @@ -262,4 +276,29 @@ public class LingerMonitor { maybeStopNotifying(nai); // No need to cancel notifications on nai: NetworkMonitor does that on disconnect. } + + private boolean isRateLimited(long now) { + final long millisSinceLast = now - mLastNotificationMillis; + if (millisSinceLast < mRateLimitMillis) { + return true; + } + mLastNotificationMillis = now; + return false; + } + + private boolean isAboveDailyLimit(long now) { + if (mFirstNotificationMillis == 0) { + mFirstNotificationMillis = now; + } + final long millisSinceFirst = now - mFirstNotificationMillis; + if (millisSinceFirst > DateUtils.DAY_IN_MILLIS) { + mNotificationCounter = 0; + mFirstNotificationMillis = 0; + } + if (mNotificationCounter >= mDailyLimit) { + return true; + } + mNotificationCounter++; + return false; + } } diff --git a/services/tests/servicestests/src/com/android/server/connectivity/LingerMonitorTest.java b/services/tests/servicestests/src/com/android/server/connectivity/LingerMonitorTest.java index 55108e7ac298..bce5787ed9a5 100644 --- a/services/tests/servicestests/src/com/android/server/connectivity/LingerMonitorTest.java +++ b/services/tests/servicestests/src/com/android/server/connectivity/LingerMonitorTest.java @@ -24,6 +24,7 @@ import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.net.NetworkMisc; +import android.text.format.DateUtils; import com.android.internal.R; import com.android.server.ConnectivityService; import com.android.server.connectivity.NetworkNotificationManager; @@ -40,11 +41,18 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.reset; public class LingerMonitorTest extends TestCase { static final String CELLULAR = "CELLULAR"; static final String WIFI = "WIFI"; + static final long LOW_RATE_LIMIT = DateUtils.MINUTE_IN_MILLIS; + static final long HIGH_RATE_LIMIT = 0; + + static final int LOW_DAILY_LIMIT = 2; + static final int HIGH_DAILY_LIMIT = 1000; + LingerMonitor mMonitor; @Mock ConnectivityService mConnService; @@ -59,7 +67,7 @@ public class LingerMonitorTest extends TestCase { when(mCtx.getPackageName()).thenReturn("com.android.server.connectivity"); when(mConnService.createNetworkMonitor(any(), any(), any(), any())).thenReturn(null); - mMonitor = new TestableLingerMonitor(mCtx, mNotifier); + mMonitor = new TestableLingerMonitor(mCtx, mNotifier, HIGH_DAILY_LIMIT, HIGH_RATE_LIMIT); } public void testTransitions() { @@ -129,8 +137,78 @@ public class LingerMonitorTest extends TestCase { mMonitor.noteLingerDefaultNetwork(to, from); verify(mNotifier, times(1)).clearNotification(100); + reset(mNotifier); mMonitor.noteLingerDefaultNetwork(from, to); - verifyToast(from, to); + verifyNoNotifications(); + } + + public void testMultipleNotifications() { + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION); + NetworkAgentInfo wifi1 = wifiNai(100); + NetworkAgentInfo wifi2 = wifiNai(101); + NetworkAgentInfo cell = cellNai(102); + + mMonitor.noteLingerDefaultNetwork(wifi1, cell); + verifyNotification(wifi1, cell); + + mMonitor.noteLingerDefaultNetwork(cell, wifi2); + verify(mNotifier, times(1)).clearNotification(100); + + reset(mNotifier); + mMonitor.noteLingerDefaultNetwork(wifi2, cell); + verifyNotification(wifi2, cell); + } + + public void testRateLimiting() throws InterruptedException { + mMonitor = new TestableLingerMonitor(mCtx, mNotifier, HIGH_DAILY_LIMIT, LOW_RATE_LIMIT); + + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION); + NetworkAgentInfo wifi1 = wifiNai(100); + NetworkAgentInfo wifi2 = wifiNai(101); + NetworkAgentInfo wifi3 = wifiNai(102); + NetworkAgentInfo cell = cellNai(103); + + mMonitor.noteLingerDefaultNetwork(wifi1, cell); + verifyNotification(wifi1, cell); + reset(mNotifier); + + Thread.sleep(50); + mMonitor.noteLingerDefaultNetwork(cell, wifi2); + mMonitor.noteLingerDefaultNetwork(wifi2, cell); + verifyNoNotifications(); + + Thread.sleep(50); + mMonitor.noteLingerDefaultNetwork(cell, wifi3); + mMonitor.noteLingerDefaultNetwork(wifi3, cell); + verifyNoNotifications(); + } + + public void testDailyLimiting() throws InterruptedException { + mMonitor = new TestableLingerMonitor(mCtx, mNotifier, LOW_DAILY_LIMIT, HIGH_RATE_LIMIT); + + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION); + NetworkAgentInfo wifi1 = wifiNai(100); + NetworkAgentInfo wifi2 = wifiNai(101); + NetworkAgentInfo wifi3 = wifiNai(102); + NetworkAgentInfo cell = cellNai(103); + + mMonitor.noteLingerDefaultNetwork(wifi1, cell); + verifyNotification(wifi1, cell); + reset(mNotifier); + + Thread.sleep(50); + mMonitor.noteLingerDefaultNetwork(cell, wifi2); + mMonitor.noteLingerDefaultNetwork(wifi2, cell); + verifyNotification(wifi2, cell); + reset(mNotifier); + + Thread.sleep(50); + mMonitor.noteLingerDefaultNetwork(cell, wifi3); + mMonitor.noteLingerDefaultNetwork(wifi3, cell); + verifyNoNotifications(); } public void testUniqueNotification() { @@ -149,7 +227,7 @@ public class LingerMonitorTest extends TestCase { verifyNotification(from, to); } - public void testIgnoreUnvalidatedNetworks() { + public void testIgnoreNeverValidatedNetworks() { setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST); setNotificationSwitch(transition(WIFI, CELLULAR)); NetworkAgentInfo from = wifiNai(100); @@ -160,6 +238,17 @@ public class LingerMonitorTest extends TestCase { verifyNoNotifications(); } + public void testIgnoreCurrentlyValidatedNetworks() { + setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST); + setNotificationSwitch(transition(WIFI, CELLULAR)); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + from.lastValidated = true; + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNoNotifications(); + } + public void testNoNotificationType() { setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST); setNotificationSwitch(); @@ -215,7 +304,6 @@ public class LingerMonitorTest extends TestCase { void verifyNoNotifications() { verifyNoToast(); verifyNoNotification(); - verify(mNotifier, never()).clearNotification(anyInt()); } void verifyToast(NetworkAgentInfo from, NetworkAgentInfo to) { @@ -251,8 +339,8 @@ public class LingerMonitorTest extends TestCase { } public static class TestableLingerMonitor extends LingerMonitor { - public TestableLingerMonitor(Context c, NetworkNotificationManager n) { - super(c, n); + public TestableLingerMonitor(Context c, NetworkNotificationManager n, int l, long r) { + super(c, n, l, r); } @Override protected PendingIntent createNotificationIntent() { return null; -- 2.11.0