前言:之前在(一百四十四)Android P WiFi 上网校验流程梳理 简单的过了一遍网络校验流程,现在前后梳理下,将流程连起来。
1.网络校验的发起
ConnectivityService
private void updateNetworkInfo(NetworkAgentInfo networkAgent, NetworkInfo newInfo) {
final NetworkInfo.State state = newInfo.getState();
NetworkInfo oldInfo = null;
final int oldScore = networkAgent.getCurrentScore();
synchronized (networkAgent) {
oldInfo = networkAgent.networkInfo;
networkAgent.networkInfo = newInfo;
}
notifyLockdownVpn(networkAgent);
if (DBG) {
log(networkAgent.name() + " EVENT_NETWORK_INFO_CHANGED, going from " +
(oldInfo == null ? "null" : oldInfo.getState()) +
" to " + state);
}
if (!networkAgent.created
&& (state == NetworkInfo.State.CONNECTED
|| (state == NetworkInfo.State.CONNECTING && networkAgent.isVPN()))) {
// A network that has just connected has zero requests and is thus a foreground network.
networkAgent.networkCapabilities.addCapability(NET_CAPABILITY_FOREGROUND);
try {
// This should never fail. Specifying an already in use NetID will cause failure.
if (networkAgent.isVPN()) {
mNetd.createVirtualNetwork(networkAgent.network.netId,
!networkAgent.linkProperties.getDnsServers().isEmpty(),
(networkAgent.networkMisc == null ||
!networkAgent.networkMisc.allowBypass));
} else {
mNetd.createPhysicalNetwork(networkAgent.network.netId,
getNetworkPermission(networkAgent.networkCapabilities));
}
} catch (Exception e) {
loge("Error creating network " + networkAgent.network.netId + ": "
+ e.getMessage());
return;
}
networkAgent.created = true;
}
if (!networkAgent.everConnected && state == NetworkInfo.State.CONNECTED) {
networkAgent.everConnected = true;
handlePerNetworkPrivateDnsConfig(networkAgent, mDnsManager.getPrivateDnsConfig());
updateLinkProperties(networkAgent, null);
notifyIfacesChangedForNetworkStats();
networkAgent.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_CONNECTED);
scheduleUnvalidatedPrompt(networkAgent);
if (networkAgent.isVPN()) {
// Temporarily disable the default proxy (not global).
synchronized (mProxyLock) {
if (!mDefaultProxyDisabled) {
mDefaultProxyDisabled = true;
if (mGlobalProxy == null && mDefaultProxy != null) {
sendProxyBroadcast(null);
}
}
}
// TODO: support proxy per network.
}
// Whether a particular NetworkRequest listen should cause signal strength thresholds to
// be communicated to a particular NetworkAgent depends only on the network's immutable,
// capabilities, so it only needs to be done once on initial connect, not every time the
// network's capabilities change. Note that we do this before rematching the network,
// so we could decide to tear it down immediately afterwards. That's fine though - on
// disconnection NetworkAgents should stop any signal strength monitoring they have been
// doing.
updateSignalStrengthThresholds(networkAgent, "CONNECT", null);
// Consider network even though it is not yet validated.
final long now = SystemClock.elapsedRealtime();
rematchNetworkAndRequests(networkAgent, ReapUnvalidatedNetworks.REAP, now);
// This has to happen after matching the requests, because callbacks are just requests.
notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_PRECHECK);
} else if (state == NetworkInfo.State.DISCONNECTED) {
networkAgent.asyncChannel.disconnect();
if (networkAgent.isVPN()) {
synchronized (mProxyLock) {
if (mDefaultProxyDisabled) {
mDefaultProxyDisabled = false;
if (mGlobalProxy == null && mDefaultProxy != null) {
sendProxyBroadcast(mDefaultProxy);
}
}
}
updateUids(networkAgent, networkAgent.networkCapabilities, null);
}
disconnectAndDestroyNetwork(networkAgent);
} else if ((oldInfo != null && oldInfo.getState() == NetworkInfo.State.SUSPENDED) ||
state == NetworkInfo.State.SUSPENDED) {
// going into or coming out of SUSPEND: rescore and notify
if (networkAgent.getCurrentScore() != oldScore) {
rematchAllNetworksAndRequests(networkAgent, oldScore);
}
updateCapabilities(networkAgent.getCurrentScore(), networkAgent,
networkAgent.networkCapabilities);
// TODO (b/73132094) : remove this call once the few users of onSuspended and
// onResumed have been removed.
notifyNetworkCallbacks(networkAgent, (state == NetworkInfo.State.SUSPENDED ?
ConnectivityManager.CALLBACK_SUSPENDED :
ConnectivityManager.CALLBACK_RESUMED));
mLegacyTypeTracker.update(networkAgent);
}
}
updateNetworkInfo是将WifiStateMachine那边的networkInfo更新到ConnectivityService中来,具体流程后续梳理,这边有两句关键的代码
networkAgent.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_CONNECTED);
scheduleUnvalidatedPrompt(networkAgent);
先只看发送消息NetworkMonitor.CMD_NETWORK_CONNECTED
NetworkMonitor
// DefaultState is the parent of all States. It exists only to handle CMD_* messages but
// does not entail any real state (hence no enter() or exit() routines).
private class DefaultState extends State {
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CMD_NETWORK_CONNECTED:
logNetworkEvent(NetworkEvent.NETWORK_CONNECTED);
transitionTo(mEvaluatingState);
return HANDLED;
进入到EvaluatingState进行网络的有效性评估,接上了(一百四十四)Android P WiFi 上网校验流程梳理的流程
// Being in the EvaluatingState State indicates the Network is being evaluated for internet
// connectivity, or that the user has indicated that this network is unwanted.
private class EvaluatingState extends State {
private int mReevaluateDelayMs;
private int mAttempts;
@Override
public void enter() {
// If we have already started to track time spent in EvaluatingState
// don't reset the timer due simply to, say, commands or events that
// cause us to exit and re-enter EvaluatingState.
if (!mEvaluationTimer.isStarted()) {
mEvaluationTimer.start();
}
sendMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
if (mUidResponsibleForReeval != INVALID_UID) {
TrafficStats.setThreadStatsUid(mUidResponsibleForReeval);
mUidResponsibleForReeval = INVALID_UID;
}
mReevaluateDelayMs = INITIAL_REEVALUATE_DELAY_MS;
mAttempts = 0;
}
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CMD_REEVALUATE:
2.网络校验结果的上报
2.1 通知形式
之前梳理网络校验的发起的时候updateNetworkInfo中还有一句没有梳理
scheduleUnvalidatedPrompt(networkAgent);
看下具体实现
private void scheduleUnvalidatedPrompt(NetworkAgentInfo nai) {
if (VDBG) log("scheduleUnvalidatedPrompt " + nai.network);
mHandler.sendMessageDelayed(
mHandler.obtainMessage(EVENT_PROMPT_UNVALIDATED, nai.network),
PROMPT_UNVALIDATED_DELAY_MS);
}
// How long to wait before putting up a "This network doesn't have an Internet connection,
// connect anyway?" dialog after the user selects a network that doesn't validate.
private static final int PROMPT_UNVALIDATED_DELAY_MS = 8 * 1000;
/**
* used to ask the user to confirm a connection to an unvalidated network.
* obj = network
*/
private static final int EVENT_PROMPT_UNVALIDATED = 29;
8s延时发送一个提示网络不可用的消息。看下EVENT_PROMPT_UNVALIDATED如何处理
case EVENT_PROMPT_UNVALIDATED: {
handlePromptUnvalidated((Network) msg.obj);
break;
}
private void handlePromptUnvalidated(Network network) {
if (VDBG) log("handlePromptUnvalidated " + network);
NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
// Only prompt if the network is unvalidated and was explicitly selected by the user, and if
// we haven't already been told to switch to it regardless of whether it validated or not.
// Also don't prompt on captive portals because we're already prompting the user to sign in.
if (nai == null || nai.everValidated || nai.everCaptivePortalDetected ||
!nai.networkMisc.explicitlySelected || nai.networkMisc.acceptUnvalidated) {
return;
}
showValidationNotification(nai, NotificationType.NO_INTERNET);
}
这里可以看到8s提示网络不可用的通知不是一定会发出来的,当网络是
- 显示被用户选择
- 校验不过
- 用户不接受校验不过
的时候才会被提醒。
PS:captive portal 不用提醒用户因为已经提醒用户去登录了(提醒登录流程后续梳理下)
// Set to true if this Network successfully passed validation or if it did not satisfy the
// default NetworkRequest in which case validation will not be attempted.
// This is a sticky bit; once set it is never cleared even if future validation attempts fail.
public boolean everValidated;
// Whether a captive portal was ever detected on this network.
// This is a sticky bit; once set it is never cleared.
public boolean everCaptivePortalDetected;
/**
* Set if the network was manually/explicitly connected to by the user either from settings
* or a 3rd party app. For example, turning on cell data is not explicit but tapping on a wifi
* ap in the wifi settings to trigger a connection is explicit. A 3rd party app asking to
* connect to a particular access point is also explicit, though this may change in the future
* as we want apps to use the multinetwork apis.
*/
public boolean explicitlySelected;
/**
* Set if the user desires to use this network even if it is unvalidated. This field has meaning
* only if {#link explicitlySelected} is true. If it is, this field must also be set to the
* appropriate value based on previous user choice.
*/
public boolean acceptUnvalidated;
看下通知显示流程
private void showValidationNotification(NetworkAgentInfo nai, NotificationType type) {
final String action;
switch (type) {
case NO_INTERNET:
action = ConnectivityManager.ACTION_PROMPT_UNVALIDATED;
break;
case LOST_INTERNET:
action = ConnectivityManager.ACTION_PROMPT_LOST_VALIDATION;
break;
default:
Slog.wtf(TAG, "Unknown notification type " + type);
return;
}
Intent intent = new Intent(action);
intent.setData(Uri.fromParts("netId", Integer.toString(nai.network.netId), null));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setClassName("com.android.settings",
"com.android.settings.wifi.WifiNoInternetDialog");
PendingIntent pendingIntent = PendingIntent.getActivityAsUser(
mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
mNotifier.showNotification(nai.network.netId, type, nai, null, pendingIntent, true);
}
NetworkNotificationManager
/**
* Show or hide network provisioning notifications.
*
* We use notifications for two purposes: to notify that a network requires sign in
* (NotificationType.SIGN_IN), or to notify that a network does not have Internet access
* (NotificationType.NO_INTERNET). We display at most one notification per ID, so on a
* particular network we can display the notification type that was most recently requested.
* So for example if a captive portal fails to reply within a few seconds of connecting, we
* might first display NO_INTERNET, and then when the captive portal check completes, display
* SIGN_IN.
*
* @param id an identifier that uniquely identifies this notification. This must match
* between show and hide calls. We use the NetID value but for legacy callers
* we concatenate the range of types with the range of NetIDs.
* @param nai the network with which the notification is associated. For a SIGN_IN, NO_INTERNET,
* or LOST_INTERNET notification, this is the network we're connecting to. For a
* NETWORK_SWITCH notification it's the network that we switched from. When this network
* disconnects the notification is removed.
* @param switchToNai for a NETWORK_SWITCH notification, the network we are switching to. Null
* in all other cases. Only used to determine the text of the notification.
*/
public void showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai,
NetworkAgentInfo switchToNai, PendingIntent intent, boolean highPriority) {
final String tag = tagFor(id);
final int eventId = notifyType.eventId;
final int transportType;
final String name;
if (nai != null) {
transportType = getFirstTransportType(nai);
final String extraInfo = nai.networkInfo.getExtraInfo();
name = TextUtils.isEmpty(extraInfo) ? nai.networkCapabilities.getSSID() : extraInfo;
// Only notify for Internet-capable networks.
if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET)) return;
} else {
// Legacy notifications.
transportType = TRANSPORT_CELLULAR;
name = null;
}
// Clear any previous notification with lower priority, otherwise return. http://b/63676954.
// A new SIGN_IN notification with a new intent should override any existing one.
final int previousEventId = mNotificationTypeMap.get(id);
final NotificationType previousNotifyType = NotificationType.getFromId(previousEventId);
if (priority(previousNotifyType) > priority(notifyType)) {
Slog.d(TAG, String.format(
"ignoring notification %s for network %s with existing notification %s",
notifyType, id, previousNotifyType));
return;
}
clearNotification(id);
if (DBG) {
Slog.d(TAG, String.format(
"showNotification tag=%s event=%s transport=%s name=%s highPriority=%s",
tag, nameOf(eventId), getTransportName(transportType), name, highPriority));
}
Resources r = Resources.getSystem();
CharSequence title;
CharSequence details;
int icon = getIcon(transportType);
if (notifyType == NotificationType.NO_INTERNET && transportType == TRANSPORT_WIFI) {
title = r.getString(R.string.wifi_no_internet, 0);
details = r.getString(R.string.wifi_no_internet_detailed);
} else if (notifyType == NotificationType.LOST_INTERNET &&
transportType == TRANSPORT_WIFI) {
title = r.getString(R.string.wifi_no_internet, 0);
details = r.getString(R.string.wifi_no_internet_detailed);
} else if (notifyType == NotificationType.SIGN_IN) {
switch (transportType) {
case TRANSPORT_WIFI:
title = r.getString(R.string.wifi_available_sign_in, 0);
details = r.getString(R.string.network_available_sign_in_detailed,
WifiInfo.removeDoubleQuotes(nai.networkCapabilities.getSSID()));
break;
case TRANSPORT_CELLULAR:
title = r.getString(R.string.network_available_sign_in, 0);
// TODO: Change this to pull from NetworkInfo once a printable
// name has been added to it
details = mTelephonyManager.getNetworkOperatorName();
break;
default:
title = r.getString(R.string.network_available_sign_in, 0);
details = r.getString(R.string.network_available_sign_in_detailed, name);
break;
}
} else if (notifyType == NotificationType.NETWORK_SWITCH) {
String fromTransport = getTransportName(transportType);
String toTransport = getTransportName(getFirstTransportType(switchToNai));
title = r.getString(R.string.network_switch_metered, toTransport);
details = r.getString(R.string.network_switch_metered_detail, toTransport,
fromTransport);
} else {
Slog.wtf(TAG, "Unknown notification type " + notifyType + " on network transport "
+ getTransportName(transportType));
return;
}
final String channelId = highPriority ? SystemNotificationChannels.NETWORK_ALERTS :
SystemNotificationChannels.NETWORK_STATUS;
Notification.Builder builder = new Notification.Builder(mContext, channelId)
.setWhen(System.currentTimeMillis())
.setShowWhen(notifyType == NotificationType.NETWORK_SWITCH)
.setSmallIcon(icon)
.setAutoCancel(true)
.setTicker(title)
.setColor(mContext.getColor(
com.android.internal.R.color.system_notification_accent_color))
.setContentTitle(title)
.setContentIntent(intent)
.setLocalOnly(true)
.setOnlyAlertOnce(true);
if (notifyType == NotificationType.NETWORK_SWITCH) {
builder.setStyle(new Notification.BigTextStyle().bigText(details));
} else {
builder.setContentText(details);
}
if (notifyType == NotificationType.SIGN_IN) {
builder.extend(new Notification.TvExtender().setChannelId(channelId));
}
Notification notification = builder.build();
mNotificationTypeMap.put(id, eventId);
try {
mNotificationManager.notifyAsUser(tag, eventId, notification, UserHandle.ALL);
} catch (NullPointerException npe) {
Slog.d(TAG, "setNotificationVisible: visible notificationManager error", npe);
}
}
这边的通知逻辑是糅合很多类型的,可以看到captive portal的sign in逻辑也在这边的。
2.2 Settings Preference形式
AccessPoint更新summary的时候会从当前网络的NetworkCapabilities是否包含
NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
NetworkCapabilities.NET_CAPABILITY_VALIDATED
判断是portal网络还是网络校验通过与否
public static String getSummary(Context context, String ssid, DetailedState state,
boolean isEphemeral, String passpointProvider) {
if (state == DetailedState.CONNECTED && ssid == null) {
if (TextUtils.isEmpty(passpointProvider) == false) {
// Special case for connected + passpoint networks.
String format = context.getString(R.string.connected_via_passpoint);
return String.format(format, passpointProvider);
} else if (isEphemeral) {
// Special case for connected + ephemeral networks.
final NetworkScoreManager networkScoreManager = context.getSystemService(
NetworkScoreManager.class);
NetworkScorerAppData scorer = networkScoreManager.getActiveScorer();
if (scorer != null && scorer.getRecommendationServiceLabel() != null) {
String format = context.getString(R.string.connected_via_network_scorer);
return String.format(format, scorer.getRecommendationServiceLabel());
} else {
return context.getString(R.string.connected_via_network_scorer_default);
}
}
}
// Case when there is wifi connected without internet connectivity.
final ConnectivityManager cm = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (state == DetailedState.CONNECTED) {
IWifiManager wifiManager = IWifiManager.Stub.asInterface(
ServiceManager.getService(Context.WIFI_SERVICE));
NetworkCapabilities nc = null;
try {
nc = cm.getNetworkCapabilities(wifiManager.getCurrentNetwork());
} catch (RemoteException e) {}
if (nc != null) {
if (nc.hasCapability(nc.NET_CAPABILITY_CAPTIVE_PORTAL)) {
int id = context.getResources()
.getIdentifier("network_available_sign_in", "string", "android");
return context.getString(id);
} else if (!nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
return context.getString(R.string.wifi_connected_no_internet);
}
}
}
if (state == null) {
Log.w(TAG, "state is null, returning empty summary");
return "";
}
String[] formats = context.getResources().getStringArray((ssid == null)
? R.array.wifi_status : R.array.wifi_status_with_ssid);
int index = state.ordinal();
if (index >= formats.length || formats[index].length() == 0) {
return "";
}
return String.format(formats[index], ssid);
}
3.总结
流程现在很清楚了,WiFi连接上了以后就进行网络校验了,但网络校验没有广播提示,当前已知的两种通知上层的逻辑是
1)连接完成往后8s会再判断一遍可用性,若不可用,弹出提示
2)Settings每次更新Preference都会拉一下网络相关属性判断一下当前网络的状况以更新给用户,有点像轮询