1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
|
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.connectivity;
import static android.net.ConnectivityManager.NETID_UNSET;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.net.ConnectivityResources;
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.SparseBooleanArray;
import android.util.SparseIntArray;
import com.android.connectivity.resources.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.MessageUtils;
import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
import java.util.Arrays;
import java.util.HashMap;
/**
* Class that monitors default network linger events and possibly notifies the user of network
* switches.
*
* This class is not thread-safe and all its methods must be called on the ConnectivityService
* handler thread.
*/
public class LingerMonitor {
private static final boolean DBG = true;
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<String, Integer> TRANSPORT_NAMES = makeTransportToNameMap();
@VisibleForTesting
public static final Intent CELLULAR_SETTINGS = new Intent().setComponent(new ComponentName(
"com.android.settings", "com.android.settings.Settings$DataUsageSummaryActivity"));
@VisibleForTesting
public static final int NOTIFY_TYPE_NONE = 0;
public static final int NOTIFY_TYPE_NOTIFICATION = 1;
public static final int NOTIFY_TYPE_TOAST = 2;
private static SparseArray<String> sNotifyTypeNames = MessageUtils.findMessageNames(
new Class[] { LingerMonitor.class }, new String[]{ "NOTIFY_TYPE_" });
private final Context mContext;
final Resources mResources;
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();
/** Whether we ever notified that we switched away from a particular network. */
private final SparseBooleanArray mEverNotified = new SparseBooleanArray();
public LingerMonitor(Context context, NetworkNotificationManager notifier,
int dailyLimit, long rateLimitMillis) {
mContext = context;
mResources = new ConnectivityResources(mContext).get();
mNotifier = notifier;
mDailyLimit = dailyLimit;
mRateLimitMillis = rateLimitMillis;
// Ensure that (now - mLastNotificationMillis) >= rateLimitMillis at first
mLastNotificationMillis = -rateLimitMillis;
}
private static HashMap<String, Integer> makeTransportToNameMap() {
SparseArray<String> numberToName = MessageUtils.findMessageNames(
new Class[] { NetworkCapabilities.class }, new String[]{ "TRANSPORT_" });
HashMap<String, Integer> nameToNumber = new HashMap<>();
for (int i = 0; i < numberToName.size(); i++) {
// MessageUtils will fail to initialize if there are duplicate constant values, so there
// are no duplicates here.
nameToNumber.put(numberToName.valueAt(i), numberToName.keyAt(i));
}
return nameToNumber;
}
private static boolean hasTransport(NetworkAgentInfo nai, int transport) {
return nai.networkCapabilities.hasTransport(transport);
}
private int getNotificationSource(NetworkAgentInfo toNai) {
for (int i = 0; i < mNotifications.size(); i++) {
if (mNotifications.valueAt(i) == toNai.network.getNetId()) {
return mNotifications.keyAt(i);
}
}
return NETID_UNSET;
}
private boolean everNotified(NetworkAgentInfo nai) {
return mEverNotified.get(nai.network.getNetId(), false);
}
@VisibleForTesting
public boolean isNotificationEnabled(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
// TODO: Evaluate moving to CarrierConfigManager.
String[] notifySwitches = mResources.getStringArray(R.array.config_networkNotifySwitches);
if (VDBG) {
Log.d(TAG, "Notify on network switches: " + Arrays.toString(notifySwitches));
}
for (String notifySwitch : notifySwitches) {
if (TextUtils.isEmpty(notifySwitch)) continue;
String[] transports = notifySwitch.split("-", 2);
if (transports.length != 2) {
Log.e(TAG, "Invalid network switch notification configuration: " + notifySwitch);
continue;
}
int fromTransport = TRANSPORT_NAMES.get("TRANSPORT_" + transports[0]);
int toTransport = TRANSPORT_NAMES.get("TRANSPORT_" + transports[1]);
if (hasTransport(fromNai, fromTransport) && hasTransport(toNai, toTransport)) {
return true;
}
}
return false;
}
private void showNotification(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
mNotifier.showNotification(fromNai.network.getNetId(), NotificationType.NETWORK_SWITCH,
fromNai, toNai, createNotificationIntent(), true);
}
@VisibleForTesting
protected PendingIntent createNotificationIntent() {
return PendingIntent.getActivity(
mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
0 /* requestCode */,
CELLULAR_SETTINGS,
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
}
// Removes any notification that was put up as a result of switching to nai.
private void maybeStopNotifying(NetworkAgentInfo nai) {
int fromNetId = getNotificationSource(nai);
if (fromNetId != NETID_UNSET) {
mNotifications.delete(fromNetId);
mNotifier.clearNotification(fromNetId);
// Toasts can't be deleted.
}
}
// Notify the user of a network switch using a notification or a toast.
private void notify(NetworkAgentInfo fromNai, NetworkAgentInfo toNai, boolean forceToast) {
int notifyType = mResources.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:
return;
case NOTIFY_TYPE_NOTIFICATION:
showNotification(fromNai, toNai);
break;
case NOTIFY_TYPE_TOAST:
mNotifier.showToast(fromNai, toNai);
break;
default:
Log.e(TAG, "Unknown notify type " + notifyType);
return;
}
if (DBG) {
Log.d(TAG, "Notifying switch from=" + fromNai.toShortString()
+ " to=" + toNai.toShortString()
+ " type=" + sNotifyTypeNames.get(notifyType, "unknown(" + notifyType + ")"));
}
mNotifications.put(fromNai.network.getNetId(), toNai.network.getNetId());
mEverNotified.put(fromNai.network.getNetId(), true);
}
/**
* Put up or dismiss a notification or toast for of a change in the default network if needed.
*
* Putting up a notification when switching from no network to some network is not supported
* and as such this method can't be called with a null |fromNai|. It can be called with a
* null |toNai| if there isn't a default network any more.
*
* @param fromNai switching from this NAI
* @param toNai switching to this NAI
*/
// The default network changed from fromNai to toNai due to a change in score.
public void noteLingerDefaultNetwork(@NonNull final NetworkAgentInfo fromNai,
@Nullable final NetworkAgentInfo toNai) {
if (VDBG) {
Log.d(TAG, "noteLingerDefaultNetwork from=" + fromNai.toShortString()
+ " everValidated=" + fromNai.everValidated
+ " lastValidated=" + fromNai.lastValidated
+ " to=" + toNai.toShortString());
}
// If we are currently notifying the user because the device switched to fromNai, now that
// we are switching away from it we should remove the notification. This includes the case
// where we switch back to toNai because its score improved again (e.g., because it regained
// Internet access).
maybeStopNotifying(fromNai);
// If the network was simply lost (either because it disconnected or because it stopped
// being the default with no replacement), then don't show a notification.
if (null == toNai) return;
// If this network never validated, don't notify. Otherwise, we could do things like:
//
// 1. Unvalidated wifi connects.
// 2. Unvalidated mobile data connects.
// 3. Cell validates, and we show a notification.
// or:
// 1. User connects to wireless printer.
// 2. User turns on cellular data.
// 3. We show a notification.
if (!fromNai.everValidated) return;
// If this network is a captive portal, don't notify. This cannot happen on initial connect
// to a captive portal, because the everValidated check above will fail. However, it can
// happen if the captive portal reasserts itself (e.g., because its timeout fires). In that
// case, as soon as the captive portal reasserts itself, we'll show a sign-in notification.
// We don't want to overwrite that notification with this one; the user has already been
// notified, and of the two, the captive portal notification is the more useful one because
// it allows the user to sign in to the captive portal. In this case, display a toast
// in addition to the captive portal notification.
//
// Note that if the network we switch to is already up when the captive portal reappears,
// this won't work because NetworkMonitor tells ConnectivityService that the network is
// unvalidated (causing a switch) before asking it to show the sign in notification. In this
// case, the toast won't show and we'll only display the sign in notification. This is the
// best we can do at this time.
boolean forceToast = fromNai.networkCapabilities.hasCapability(
NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL);
// Only show the notification once, in order to avoid irritating the user every time.
// TODO: should we do this?
if (everNotified(fromNai)) {
if (VDBG) {
Log.d(TAG, "Not notifying handover from " + fromNai.toShortString()
+ ", already notified");
}
return;
}
// Only show the notification if we switched away because a network became unvalidated, not
// because its score changed.
// TODO: instead of just skipping notification, keep a note of it, and show it if it becomes
// unvalidated.
if (fromNai.lastValidated) return;
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) {
mNotifications.delete(nai.network.getNetId());
mEverNotified.delete(nai.network.getNetId());
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;
}
}
|