Bootstrap

从【连接受限】看Android网络

现象

连接受限
谷歌手机,连接受限,经典现象了。
都知道是谷歌网络验证没过,但具体的流程脉络呢?
不如来从现象开始摸一下本质。

摸索

从通知开始

首先找“连接受限”四个字符准没错;
于是发现相关的字符资源在packages/modules/Connectivity/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml中;

<string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> 的连接受限"</string>

顺藤摸瓜又找到使用这个字符资源的地方,即packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkNotificationManager.java,一个位于Connectivity模块中的通知管理类(官方逐渐将各类组件移出核心包,作为模块包使用);

 public void showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai,
        //...略
		if (notifyType == NotificationType.PARTIAL_CONNECTIVITY
                && transportType == TRANSPORT_WIFI) {
            title = r.getString(R.string.network_partial_connectivity, name);
            details = r.getString(R.string.network_partial_connectivity_detailed);
        } 
     	//...略
    }

在这个包里搜索,找到使用这个通知管理类的地方,即packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java
关注NotificationType.PARTIAL_CONNECTIVITY即可;

    private void showNetworkNotification(NetworkAgentInfo nai, NotificationType type) {
        final String action;
        final boolean highPriority;
        switch (type) {
            //...略
            case PARTIAL_CONNECTIVITY:
                action = ConnectivityManager.ACTION_PROMPT_PARTIAL_CONNECTIVITY;
                highPriority = nai.networkAgentConfig.explicitlySelected;
                break;
        }
		//...略
        mNotifier.showNotification(
                nai.network.getNetId(), type, nai, null, pendingIntent, highPriority);
    }

接着往上摸,发现是由handlePromptUnvalidated方法来处理是否发出连接受限的通知;

    private void handlePromptUnvalidated(Network network) {
        if (VDBG || DDBG) log("handlePromptUnvalidated " + network);
        NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);

        if (nai == null || !shouldPromptUnvalidated(nai)) {
            return;
        }
        nai.onPreventAutomaticReconnect();
        if (nai.partialConnectivity) {
            showNetworkNotification(nai, NotificationType.PARTIAL_CONNECTIVITY);
        } else {
            showNetworkNotification(nai, NotificationType.NO_INTERNET);
        }
    }

而且可以明显看到,是否受限是由NetworkAgentInfopartialConnectivity属性来进行直接判断的——这说明,在此之前,这个属性可能就已经进行了赋值。

是Handler发的通知

先抛开赋值不管,继续看谁在调用handlePromptUnvalidated这个方法;
然后发现有两处,两处都在Handler中;

一处在InternalHandler中,看起来就是会在某个时间点发消息,延迟8秒后去处理不正常的网络;

    private class InternalHandler extends Handler {

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
				//...略
				case EVENT_PROMPT_UNVALIDATED: {
                    handlePromptUnvalidated((Network) msg.obj);
                    break;
                }
                //...略
            }
        }
    }

    private static final int PROMPT_UNVALIDATED_DELAY_MS = 8 * 1000;

    private void scheduleUnvalidatedPrompt(NetworkAgentInfo nai) {
        mHandler.sendMessageDelayed(
                mHandler.obtainMessage(EVENT_PROMPT_UNVALIDATED, nai.network),
                PROMPT_UNVALIDATED_DELAY_MS);
    }

另一处则像是专门的网络状态的监听NetworkStateTrackerHandler,会经由多重判断处理,最终由handleNetworkTested方法调用;

    private class NetworkStateTrackerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            if (!maybeHandleNetworkMonitorMessage(msg)
                    && !maybeHandleNetworkAgentInfoMessage(msg)) {
                maybeHandleNetworkAgentMessage(msg);
            }
        }

        private boolean maybeHandleNetworkMonitorMessage(Message msg) {
            final int netId = msg.arg2;
            final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);

            if (nai != null && nai.destroyed) return true;
            switch (msg.what) {
               	//...略
                case EVENT_NETWORK_TESTED: {
                    final NetworkTestedResults results = (NetworkTestedResults) msg.obj;

                    if (nai == null) break;

                    handleNetworkTested(nai, results.mTestResult,
                            (results.mRedirectUrl == null) ? "" : results.mRedirectUrl);
                    break;
                }
                //...略 
            }
            return true;
        }

        
		//按照源码注释理解,这个方法如果先行测试出网络有问题,会先一步通知并取消Handler的通知
        private void handleNetworkTested(
                @NonNull NetworkAgentInfo nai, int testResult, @NonNull String redirectUrl) {
			final boolean wasPartial = nai.partialConnectivity;
            nai.partialConnectivity = ((testResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0);
			//...略
            if (!wasPartial && nai.partialConnectivity) {
                // Remove delayed message if there is a pending message.
                mHandler.removeMessages(EVENT_PROMPT_UNVALIDATED, nai.network);
                handlePromptUnvalidated(nai.network);
            }
			//...略
        }
    }

这里注意到partialConnectivity值很可能经过了一次变动,在这个阶段并不清楚是否为首次赋值操作,并且与之相关的testResult值似乎也只是一个传递下来的结果,只能暂且记下;

看看NetworkStateTrackerHandler

两个Handler中下面这个Handler看着专业一点,就先看看这个吧;

那么主要关注EVENT_NETWORK_TESTED消息是在哪些地方发起的;

然后就会发现是从另一个网络监听NetworkMonitorCallbacks的回调中发起的;

    private class NetworkMonitorCallbacks extends INetworkMonitorCallbacks.Stub {
    
        @Override
        public void notifyNetworkTestedWithExtras(NetworkTestResultParcelable p) {
            final Message msg = mTrackerHandler.obtainMessage(
                    EVENT_NETWORK_TESTED,
                    0, mNetId,
                    new NetworkTestedResults(
                            mNetId, p.result, p.timestampMillis, p.redirectUrl));
            mTrackerHandler.sendMessage(msg);

            //...略
        }

是从notifyNetworkTestedWithExtras方法中发起的;

那么依旧顺藤摸瓜,看这个监听类是从哪里创建的以及相关方法是在什么时候触发的。

创建的地方找到了registerNetworkAgentInternal方法;

    private Network registerNetworkAgentInternal(INetworkAgent na, NetworkInfo networkInfo,
            LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
            NetworkScore currentScore, NetworkAgentConfig networkAgentConfig, int providerId,
            int uid) {
        
        final NetworkAgentInfo nai = new NetworkAgentInfo(na,
                new Network(mNetIdManager.reserveNetId()), new NetworkInfo(networkInfo),
                linkProperties, networkCapabilities,
                currentScore, mContext, mTrackerHandler, new NetworkAgentConfig(networkAgentConfig),
                this, mNetd, mDnsResolver, providerId, uid, mLingerDelayMs,
                mQosCallbackTracker, mDeps);

        final String extraInfo = networkInfo.getExtraInfo();
        final String name = TextUtils.isEmpty(extraInfo)
                ? nai.networkCapabilities.getSsid() : extraInfo;
        if (DBG) log("registerNetworkAgent " + nai);
        
        mDeps.getNetworkStack().makeNetworkMonitor(
                nai.network, name, new NetworkMonitorCallbacks(nai));
        return nai.network;
    }

一看到register字眼就知道可能抓到大鱼了,事实也确实如此;

registerNetworkAgentInternal方法中创建了上文中的关键信息NetworkAgentInfo,并在此创建了网络状态监听类NetworkMonitor及相关回调类NetworkMonitorCallbacks

触发notifyNetworkTestedWithExtras的地方则找到了packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java中;

    private void notifyNetworkTested(NetworkTestResultParcelable result) {
        try {
            if (mCallbackVersion <= 5) {
                mCallback.notifyNetworkTested(
                        getLegacyTestResult(result.result, result.probesSucceeded),
                        result.redirectUrl);
            } else {
                mCallback.notifyNetworkTestedWithExtras(result);
            }
        } catch (RemoteException e) {
            Log.e(TAG, "Error sending network test result", e);
        }
    }

这也和上面需要创建NetworkMonitor对应上了,这个方法的参数NetworkTestResultParcelable也可以推断大致就是网络测试的结果信息。

那么现在又有两条分支了;

一条是可以检查NetworkAgentInfo创建后,哪些地方进行了赋值操作,尤其是partialConnectivity属性(不忘初心),因为从构造方法上看是没有这条属性的,说明在其他地方进行了处理;

一条是关注NetworkMonitor的生命周期,以及关注NetworkMonitorCallbacks的相关调用。

先从partialConnectivity属性开始追查,发现除了上文中的ConnectivityService#handleNetworkTested方法中,没有其他地方进行过赋值操作,那么基本可以锁定这个方法;

        private void handleNetworkTested(
                @NonNull NetworkAgentInfo nai, int testResult, @NonNull String redirectUrl) {
			final boolean wasPartial = nai.partialConnectivity;
            nai.partialConnectivity = ((testResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0);
            //...略
        }

常量值INetworkMonitor中定义了常量值NETWORK_VALIDATION_RESULT_PARTIAL

    const int NETWORK_VALIDATION_RESULT_PARTIAL = 0x02;

也就是说,只要testResult的值为2,就会被判定为连接受限

而这个方法中的的testResult又是NetworkMonitor和NetworkMonitorCallbacks相关的;

两条分支其实是一条路;

于是目光再次集中到NetworkMonitor这边来。

NetworkMonitor做了什么

那么接着上面的NetworkMonitor#notifyNetworkTested方法继续往上顺,发现了内部类EvaluationState中的reportEvaluationResult方法;

    protected class EvaluationState {
        
		protected void reportEvaluationResult(int result, @Nullable String redirectUrl) {
            if (mCaptivePortalWantedAsIs) {
                result = NETWORK_VALIDATION_RESULT_VALID;
            } else if (!isValidationRequired() && mProbeCompleted == 0 && mCallbackVersion >= 11) {
                result |= NETWORK_VALIDATION_RESULT_SKIPPED;
            }

            mEvaluationResult = result;
            final NetworkTestResultParcelable p = new NetworkTestResultParcelable();
            p.result = result;
            p.probesSucceeded = mProbeResults;
            p.probesAttempted = mProbeCompleted;
            p.redirectUrl = redirectUrl;
            p.timestampMillis = SystemClock.elapsedRealtime();
            notifyNetworkTested(p);
            recordValidationResult(result, redirectUrl);
        }
    }

这里在创建NetworkTestResultParcelable,但result依旧是传递下来的,同样也受其他值影响;

只能再往上追,追出几个状态类;

public class NetworkMonitor extends StateMachine {
	
    private class DefaultState extends State {
        //...略
    }
    private class ValidatedState extends State {
    	//...略
    }
	private class ProbingState extends State {
        //...略
    }
    private class EvaluatingState extends State {
		//...略     
    }
    private class EvaluatingPrivateDnsState extends State {
        //...略
    }
    //...略
}

代码过多,不作纠结,只需要知道是状态机就行,毕竟NetworkMonitor直接继承了frameworks/base/core/java/com/android/internal/util/StateMachine,这一段代码中大部分操作都是凭借StateMachine来处理的;

先关注ProbingState,因为其中会有关键常量值NETWORK_VALIDATION_RESULT_PARTIAL出现;

    private class ProbingState extends State {

        private Thread mThread;

        @Override
        public void enter() {
            //...略
            final int token = ++mProbeToken;
            final ValidationProperties deps = new ValidationProperties(mNetworkCapabilities);
            final URL fallbackUrl = nextFallbackUrl();
            final URL[] httpsUrls = Arrays.copyOf(
                    mCaptivePortalHttpsUrls, mCaptivePortalHttpsUrls.length);
            final URL[] httpUrls = Arrays.copyOf(
                    mCaptivePortalHttpUrls, mCaptivePortalHttpUrls.length);
            mThread = new Thread(() -> sendMessage(obtainMessage(CMD_PROBE_COMPLETE, token, 0,
                    isCaptivePortal(deps, httpsUrls, httpUrls, fallbackUrl))));
            mThread.start();
        }
        
        @Override
        public boolean processMessage(Message message) {
            switch (message.what) {
                case CMD_PROBE_COMPLETE:
                    if (message.arg1 != mProbeToken) {
                        return HANDLED;
                    }

                    final CaptivePortalProbeResult probeResult =
                            (CaptivePortalProbeResult) message.obj;
					//...略

                    if (probeResult.isSuccessful()) {
                        transitionTo(mEvaluatingPrivateDnsState);
                    } else if (isTermsAndConditionsCaptive(
                            mInfoShim.getCaptivePortalData(mLinkProperties))) {
                        //...略
                    } else if (probeResult.isPortal()) {
                       //...略
                    } else if (probeResult.isPartialConnectivity()) {
                        mEvaluationState.reportEvaluationResult(NETWORK_VALIDATION_RESULT_PARTIAL,
                                null /* redirectUrl */);
                        //...略
                    } else {
                        //...略
                    }
                    return HANDLED;
                //...略
            }
        }
    }

可以看到,转换到此状态后(enter),就发了消息CMD_PROBE_COMPLETE,并将结果由isCaptivePortal方法封装塞进了message;
因此问题变成了怎样去判断probeResult.isPartialConnectivity()

先找到这个类的所在packages/modules/NetworkStack/common/captiveportal/src/android/net/captiveportal/CaptivePortalProbeResult.java,看看相关的方法和属性;

public class CaptivePortalProbeResult {
  
    public static final int PARTIAL_CODE = -1;
    public boolean isPartialConnectivity() {
        return mHttpResponseCode == PARTIAL_CODE;
    }
}

也就是CaptivePortalProbeResultmHttpResponseCode只要被赋值为-1,probeResult.isPartialConnectivity()则为true。

到这里又可以通过两方面来摸索:

一,状态机的启动顺序,什么时候转换到ProbingState;

二,CaptivePortalProbeResult是依据什么来进行创建赋值的。

NetworkMonitor是一个状态机

是一个状态机无疑,可以从其构方法中看出有哪些状态可供转换;

        addState(mDefaultState);
        addState(mMaybeNotifyState, mDefaultState);
            addState(mEvaluatingState, mMaybeNotifyState);
                addState(mProbingState, mEvaluatingState);
                addState(mWaitingForNextProbeState, mEvaluatingState);
            addState(mCaptivePortalState, mMaybeNotifyState);
        addState(mEvaluatingPrivateDnsState, mDefaultState);
        addState(mEvaluatingBandwidthState, mDefaultState);
        addState(mValidatedState, mDefaultState);
        setInitialState(mDefaultState);

默认状态或者初始状态正是DefaultState

    public void notifyNetworkConnected(LinkProperties lp, NetworkCapabilities nc) {
        final NetworkMonitorParameters params = new NetworkMonitorParameters();
        params.linkProperties = lp;
        params.networkCapabilities = nc;
        notifyNetworkConnectedParcel(params);
    }
    
	public void notifyNetworkConnectedParcel(NetworkMonitorParameters params) {
        sendMessage(CMD_NETWORK_CONNECTED, params);
    }

    private class DefaultState extends State {
        @Override
        public void enter() {
            mContext.registerReceiver(mConfigurationReceiver,
                    new IntentFilter(ACTION_CONFIGURATION_CHANGED));
            checkAndRenewResourceConfig();
        }

        @Override
        public boolean processMessage(Message message) {
            switch (message.what) {
                case CMD_NETWORK_CONNECTED:
                    updateConnectedNetworkAttributes(message);
                    logNetworkEvent(NetworkEvent.NETWORK_CONNECTED);
                    transitionTo(mEvaluatingState);
                    return HANDLED;
		}
	}

checkAndRenewResourceConfig方法关系到下文中一些重要参数;
ProbingState是转换EvaluatingState后随即进入的;

    private class EvaluatingState extends State {
        private Uri mEvaluatingCapportUrl;

        @Override
        public void enter() {
            //...略
            sendMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
            //...略
        }

        @Override
        public boolean processMessage(Message message) {
            switch (message.what) {
                case CMD_REEVALUATE:
                    //...略
                    transitionTo(mProbingState);
                    return HANDLED;
			}
		}

EvaluatingState又是接收到CMD_NETWORK_CONNECTED消息后进入的;
那么很明显,当网络连接成功后,就会进入EvaluatingState;
也就是连接成功后,就会进行评估和嗅探操作;

CaptivePortalProbeResult从何而来

再来关注可以得到嗅探结果的isCaptivePortal方法;

    private CaptivePortalProbeResult isCaptivePortal(ValidationProperties properties,
            URL[] httpsUrls, URL[] httpUrls, URL fallbackUrl) {
        //...略

        final CaptivePortalProbeResult result;
        if (pacUrl != null) {
            result = sendDnsAndHttpProbes(null, pacUrl, ValidationProbeEvent.PROBE_PAC);
            reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, result);
        } else if (mUseHttps && httpsUrls.length == 1 && httpUrls.length == 1) {
            result = sendHttpAndHttpsParallelWithFallbackProbes(properties, proxyInfo,
                    httpsUrls[0], httpUrls[0], fallbackUrl);
        } else if (mUseHttps) {
            result = sendMultiParallelHttpAndHttpsProbes(properties, proxyInfo, httpsUrls,
                    httpUrls);
        } else {
            result = sendDnsAndHttpProbes(proxyInfo, httpUrls[0], ValidationProbeEvent.PROBE_HTTP);
            reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, result);
        }
		//...略
        return result;
    }

看着一大串if,来一个个排除;

pacUrl,大概理解为代理地址,默认为空;

//packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java 
	final ProxyInfo proxyInfo = mLinkProperties.getHttpProxy();
        if (proxyInfo != null && !Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) {
            pacUrl = makeURL(proxyInfo.getPacFileUrl().toString());
            if (pacUrl == null) {
                return CaptivePortalProbeResult.failed(CaptivePortalProbeResult.PROBE_UNKNOWN);
            }
        }    

	//构造方法,LinkProperties创建时为无参构造
    public NetworkMonitor(Context context, INetworkMonitorCallbacks cb, Network network,
            IpConnectivityLog logger, SharedLog validationLogs,
            @NonNull NetworkStackServiceManager serviceManager, Dependencies deps,
            @Nullable TcpSocketTracker tst) {
		//...略
        mLinkProperties = new LinkProperties();
    }
//packages/modules/Connectivity/framework/src/android/net/LinkProperties.java
	public LinkProperties() {
        mParcelSensitiveFields = false;
    }

    public LinkProperties(@Nullable LinkProperties source, boolean parcelSensitiveFields) {
		//...略
        mHttpProxy = (source.mHttpProxy == null) ? null : new ProxyInfo(source.mHttpProxy);
		//...略
    }

    public @Nullable ProxyInfo getHttpProxy() {
        return mHttpProxy;
    }

mUseHttps,大概理解为是否使用https方式,默认为true;

//packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java 
    public NetworkMonitor(Context context, INetworkMonitorCallbacks cb, Network network,
            IpConnectivityLog logger, SharedLog validationLogs,
            @NonNull NetworkStackServiceManager serviceManager, Dependencies deps,
            @Nullable TcpSocketTracker tst) {
		//...略
		mUseHttps = getUseHttpsValidation();
    }

    private boolean getUseHttpsValidation() {
        return mDependencies.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY,
                CAPTIVE_PORTAL_USE_HTTPS, 1) == 1;
    }

httpsUrls和httpUrls,是传递进来的参数,往回退一下,看从哪里取值;

   		final URL[] httpsUrls = Arrays.copyOf(
                    mCaptivePortalHttpsUrls, mCaptivePortalHttpsUrls.length);
        final URL[] httpUrls = Arrays.copyOf(
                    mCaptivePortalHttpUrls, mCaptivePortalHttpUrls.length);

发现在checkAndRenewResourceConfig方法中有过赋值,与上文中的DefaultState呼应;

    private boolean checkAndRenewResourceConfig() {
        //...略
        final URL[] captivePortalHttpsUrls = makeCaptivePortalHttpsUrls(customizedContext);
        if (!Arrays.equals(mCaptivePortalHttpsUrls, captivePortalHttpsUrls)) {
            mCaptivePortalHttpsUrls = captivePortalHttpsUrls;
            reevaluationNeeded = true;
            log("checkAndRenewResourceConfig: update captive portal https urls to "
                    + Arrays.toString(mCaptivePortalHttpsUrls));
        }
        
        final URL[] captivePortalHttpUrls = makeCaptivePortalHttpUrls(customizedContext);
        if (!Arrays.equals(mCaptivePortalHttpUrls, captivePortalHttpUrls)) {
            mCaptivePortalHttpUrls = captivePortalHttpUrls;
            reevaluationNeeded = true;
            log("checkAndRenewResourceConfig: update captive portal http urls to "
                    + Arrays.toString(mCaptivePortalHttpUrls));
        }
		//...略
    }

这里又直接抄了captivePortalHttpsUrls和mCaptivePortalHttpUrls的值;

    private URL[] makeCaptivePortalHttpsUrls(@NonNull Context context) {
        final URL testUrl = getTestUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL);
        if (testUrl != null) return new URL[] { testUrl };

        final String firstUrl = getCaptivePortalServerHttpsUrl(context);
        try {
            final URL[] settingProviderUrls =
                combineCaptivePortalUrls(firstUrl, CAPTIVE_PORTAL_OTHER_HTTPS_URLS);
            // firstUrl will at least be default configuration, so default value in
            // getProbeUrlArrayConfig is actually never used.
            return getProbeUrlArrayConfig(context, settingProviderUrls,
                    R.array.config_captive_portal_https_urls,
                    DEFAULT_CAPTIVE_PORTAL_HTTPS_URLS, this::makeURL);
        } catch (Exception e) {
            // Don't let a misconfiguration bootloop the system.
            Log.e(TAG, "Error parsing configured https URLs", e);
            // Ensure URL aligned with legacy configuration.
            return new URL[]{makeURL(firstUrl)};
        }
    }

    private String getCaptivePortalServerHttpsUrl(@NonNull Context context) {
        return getSettingFromResource(context,
                R.string.config_captive_portal_https_url, mCaptivePortalHttpsUrlFromSetting,
                context.getResources().getString(
                R.string.default_captive_portal_https_url));
    }

    private URL[] makeCaptivePortalHttpUrls(@NonNull Context context) {
        final URL testUrl = getTestUrl(TEST_CAPTIVE_PORTAL_HTTP_URL);
        if (testUrl != null) return new URL[] { testUrl };

        final String firstUrl = getCaptivePortalServerHttpUrl(context);
        try {
            final URL[] settingProviderUrls =
                    combineCaptivePortalUrls(firstUrl, CAPTIVE_PORTAL_OTHER_HTTP_URLS);
            // firstUrl will at least be default configuration, so default value in
            // getProbeUrlArrayConfig is actually never used.
            return getProbeUrlArrayConfig(context, settingProviderUrls,
                    R.array.config_captive_portal_http_urls,
                    DEFAULT_CAPTIVE_PORTAL_HTTP_URLS, this::makeURL);
        } catch (Exception e) {
            // Don't let a misconfiguration bootloop the system.
            Log.e(TAG, "Error parsing configured http URLs", e);
            // Ensure URL aligned with legacy configuration.
            return new URL[]{makeURL(firstUrl)};
        }
    }

    public String getCaptivePortalServerHttpUrl(@NonNull Context context) {
        return getSettingFromResource(context,
                R.string.config_captive_portal_http_url, mCaptivePortalHttpUrlFromSetting,
                context.getResources().getString(
                R.string.default_captive_portal_http_url));
    }

最终又是在资源文件中取值,而且源码中的注释也说得很清楚,url[]应当只有firstUrl一个值,因此只要default_captive_portal_http_urldefault_captive_portal_https_url的值不为空,那么整个url[]确实就只有一个值;

连接受限的直接原因

packages/modules/NetworkStack/res/values/config.xml中;

    <string name="config_captive_portal_https_url" translatable="false"></string>
    <string name="default_captive_portal_https_url" translatable="false">https://www.google.com/generate_204</string>

    <string name="config_captive_portal_http_url" translatable="false"></string>
    <string name="default_captive_portal_http_url" translatable="false">http://connectivitycheck.gstatic.com/generate_204</string>

有值,两个值的长度恰好为1,mUseHttps又为true,于是满足条件:

    private CaptivePortalProbeResult isCaptivePortal(ValidationProperties properties,
            URL[] httpsUrls, URL[] httpUrls, URL fallbackUrl) {
        //...略
		if (mUseHttps && httpsUrls.length == 1 && httpUrls.length == 1) {
            result = sendHttpAndHttpsParallelWithFallbackProbes(properties, proxyInfo,
                    httpsUrls[0], httpUrls[0], fallbackUrl);
        }
        return result;
    }

到这里,已经初步证实了最初的猜想,https://www.google.com/generate_204这个验证地址,国内肯定连接不上,因此被判定为连接受限
也就是如果能改变default_captive_portal_https_url的值,连接受限4个字将不再出现。

嗅探是怎样进行的

嗅探的结果才是最终的判断依据,那么了解嗅探是怎样进行的也是有必要的了;
先看sendHttpAndHttpsParallelWithFallbackProbes方法;

    private static final int PROBE_TIMEOUT_MS  = 3000;
    
    private CaptivePortalProbeResult sendHttpAndHttpsParallelWithFallbackProbes(
            ValidationProperties properties, ProxyInfo proxy, URL httpsUrl, URL httpUrl,
            URL fallbackUrl) {
        // Number of probes to wait for. If a probe completes with a conclusive answer
        // it shortcuts the latch immediately by forcing the count to 0.
        final CountDownLatch latch = new CountDownLatch(2);

        final Uri capportApiUrl = getCaptivePortalApiUrl(mLinkProperties);
        final ProbeThread httpsProbe = new ProbeThread(latch, properties, proxy, httpsUrl,
                ValidationProbeEvent.PROBE_HTTPS, capportApiUrl);
        final ProbeThread httpProbe = new ProbeThread(latch, properties, proxy, httpUrl,
                ValidationProbeEvent.PROBE_HTTP, capportApiUrl);

        try {
            httpsProbe.start();
            httpProbe.start();
            latch.await(PROBE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            validationLog("Error: probes wait interrupted!");
            return CaptivePortalProbeResult.failed(CaptivePortalProbeResult.PROBE_UNKNOWN);
        }

        final CaptivePortalProbeResult httpsResult = httpsProbe.result();
        final CaptivePortalProbeResult httpResult = httpProbe.result();
		
		if (isConclusiveResult(httpResult, capportApiUrl)) {
            reportProbeResult(httpProbe.result());
            return httpResult;
        }

        if (isConclusiveResult(httpsResult, capportApiUrl)) {
            reportProbeResult(httpsProbe.result());
            return httpsResult;
        }
		//...略
		
        try {
            httpProbe.join();
            reportProbeResult(httpProbe.result());

            if (httpProbe.result().isPortal()) {
                return httpProbe.result();
            }

            httpsProbe.join();
            reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTPS, httpsProbe.result());

            if (httpsProbe.result().isFailed() && httpProbe.result().isSuccessful()) {
                return CaptivePortalProbeResult.PARTIAL;
            }
            return httpsProbe.result();
        } catch (InterruptedException e) {
            validationLog("Error: http or https probe wait interrupted!");
            return CaptivePortalProbeResult.failed(CaptivePortalProbeResult.PROBE_UNKNOWN);
        }
    }

代码较多,但逻辑比较清晰,就是通过httpProbehttpsProbe两个探针同时进行嗅探,如果3秒内不能得到确切结果,就先后嗅探http和https的结果,http有问题就直接返回结果;
而如果http成功https失败,就返回PARTIAL结果,也就是连接受限
这里要结合CaptivePortalProbeResult类的一些属性和方法看才行:

public class CaptivePortalProbeResult {
    public static final int SUCCESS_CODE = 204;
    public static final int FAILED_CODE = 599;
    public static final int PORTAL_CODE = 302;
    
    public static final int PARTIAL_CODE = -1;

    public static final CaptivePortalProbeResult PRIVATE_IP =
            new CaptivePortalProbeResult(DNS_PRIVATE_IP_RESPONSE_CODE, 1 << PROBE_HTTP);
    // Partial connectivity should be concluded from both HTTP and HTTPS probes.
    @NonNull
    public static final CaptivePortalProbeResult PARTIAL = new CaptivePortalProbeResult(
            PARTIAL_CODE, 1 << PROBE_HTTP | 1 << PROBE_HTTPS);

    @ProbeType
    public final int probeType;

    @IntDef(value = {
        PROBE_UNKNOWN,
        1 << PROBE_HTTP,
        1 << PROBE_HTTPS,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ProbeType {
    }

    //...略
    
    public boolean isSuccessful() {
        return isSuccessCode(mHttpResponseCode);
    }

    public boolean isPortal() {
        return isPortalCode(mHttpResponseCode);
    }

    private static boolean isSuccessCode(int responseCode) {
        return responseCode == SUCCESS_CODE;
    }
    
    public static boolean isPortalCode(int responseCode) {
        return !isSuccessCode(responseCode) && (responseCode >= 200) && (responseCode <= 399);
    }

    public boolean isFailed() {
        return !isSuccessful() && !isPortal();
    }

    public boolean isPartialConnectivity() {
        return mHttpResponseCode == PARTIAL_CODE;
    }

结合着响应码来看,如果嗅探或请求成功了,响应码会是204,即无内容的响应码,其他2XX或3XX代表至少地址有所反应,除此之外则为失败。

ProbeThread

上面可以看出探网操作被封装为一个可执行的线程类:

    private class ProbeThread extends Thread {
        //...略
        private volatile CaptivePortalProbeResult mResult;

        public CaptivePortalProbeResult result() {
            return mResult;
        }

        @Override
        public void run() {
            mResult = mProbe.sendProbe();
            if (isConclusiveResult(mResult, mProbe.mCaptivePortalApiUrl)) {
                // Stop waiting immediately if any probe is conclusive.
                while (mLatch.getCount() > 0) {
                    mLatch.countDown();
                }
            }
            // Signal this probe has completed.
            mLatch.countDown();
        }
    }

再拿其中一个具体的probe来看嗅探的具体内容,就HttpsProbe 吧;

    final class HttpsProbe extends Probe {
        HttpsProbe(ValidationProperties properties, ProxyInfo proxy, URL url,
                Uri captivePortalApiUrl) {
            super(properties, proxy, url, captivePortalApiUrl);
        }

        @Override
        protected CaptivePortalProbeResult sendProbe() {
            return sendDnsAndHttpProbes(mProxy, mUrl, ValidationProbeEvent.PROBE_HTTPS);
        }
    }

接下来会看到具体的操作,先在sendDnsAndHttpProbes方法中解析地址,再在makeProbeConnection方法中创建连接,最后由sendHttpProbe发送具体的请求并接收结果;

    private CaptivePortalProbeResult sendDnsAndHttpProbes(ProxyInfo proxy, URL url, int probeType) {
        final String host = (proxy != null) ? proxy.getHost() : url.getHost();
        final InetAddress[] resolvedAddr = sendDnsProbe(host);
        if (mPrivateIpNoInternetEnabled && probeType == ValidationProbeEvent.PROBE_HTTP
                && (proxy == null) && hasPrivateIpAddress(resolvedAddr)) {
            recordProbeEventMetrics(NetworkValidationMetrics.probeTypeToEnum(probeType),
                    0 /* latency */, ProbeResult.PR_PRIVATE_IP_DNS, null /* capportData */);
            return CaptivePortalProbeResult.PRIVATE_IP;
        }
        return sendHttpProbe(url, probeType, null);
    }

    protected CaptivePortalProbeResult sendHttpProbe(URL url, int probeType,
            @Nullable CaptivePortalProbeSpec probeSpec) {
        HttpURLConnection urlConnection = null;
        int httpResponseCode = CaptivePortalProbeResult.FAILED_CODE;
        String redirectUrl = null;
        final Stopwatch probeTimer = new Stopwatch().start();
        final int oldTag = TrafficStats.getAndSetThreadStatsTag(
                NetworkStackConstants.TAG_SYSTEM_PROBE);
        try {
            urlConnection = makeProbeConnection(url, followRedirect);
            
            String requestHeader = urlConnection.getRequestProperties().toString();
            
            httpResponseCode = urlConnection.getResponseCode();
            redirectUrl = urlConnection.getHeaderField("location");
			//...略
        } catch (IOException e) {
            validationLog(probeType, url, "Probe failed with exception " + e);
            if (httpResponseCode == CaptivePortalProbeResult.FAILED_CODE) {
                // TODO: Ping gateway and DNS server and log results.
            }
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            TrafficStats.setThreadStatsTag(oldTag);
        }
        //...略
        return probeResult;
    }

    private HttpURLConnection makeProbeConnection(URL url, boolean followRedirects)
            throws IOException {
        final HttpURLConnection conn = (HttpURLConnection) mCleartextDnsNetwork.openConnection(url);
        conn.setInstanceFollowRedirects(followRedirects);
        conn.setConnectTimeout(SOCKET_TIMEOUT_MS);
        conn.setReadTimeout(SOCKET_TIMEOUT_MS);
        conn.setRequestProperty("Connection", "close");
        conn.setUseCaches(false);
        if (mCaptivePortalUserAgent != null) {
            conn.setRequestProperty("User-Agent", mCaptivePortalUserAgent);
        }
        return conn;
    }

回过头看看InternalHander

不要忘记以上都是从NetworkStateTrackerHandler中追踪而得,另一个InternalHander是也可以触发通知的;
现在再回过头看看InternalHander中会如何处理,也就是看谁调用了scheduleUnvalidatedPrompt方法,这个方法的作用是8秒后就向InternalHander发送消息通知当前的网络存在故障;
于是就在当前ConnectivityService中找到updateNetworkInfo方法;

    private void updateNetworkInfo(NetworkAgentInfo networkAgent, NetworkInfo info) {
        //...略
        if (!networkAgent.everConnected && state == NetworkInfo.State.CONNECTED) {
            networkAgent.everConnected = true;
            networkAgent.getAndSetNetworkCapabilities(networkAgent.networkCapabilities);

            handlePerNetworkPrivateDnsConfig(networkAgent, mDnsManager.getPrivateDnsConfig());
            updateLinkProperties(networkAgent, new LinkProperties(networkAgent.linkProperties),
                    null);
			//...略
            if (networkAgent.networkAgentConfig.acceptPartialConnectivity) {
                networkAgent.networkMonitor().setAcceptPartialConnectivity();
            }
            //...略
            scheduleUnvalidatedPrompt(networkAgent);

            //...略
        } else if (state == NetworkInfo.State.DISCONNECTED) {
            //...略
        } else if (networkAgent.created && (oldInfo.getState() == NetworkInfo.State.SUSPENDED ||
                state == NetworkInfo.State.SUSPENDED)) {
            //...略
        }
    }

这方法一看调用的地方就不会只有一处;
果然找到3处;
但最值得关注无疑是下面的handleRegisterNetworkAgent方法:

    private void handleRegisterNetworkAgent(NetworkAgentInfo nai, INetworkMonitor networkMonitor) {
        final NetworkCapabilities nc = new NetworkCapabilities(nai.networkCapabilities);
        final LinkProperties lp = new LinkProperties(nai.linkProperties);
        processCapabilitiesFromAgent(nai, nc);
        nai.getAndSetNetworkCapabilities(mixInCapabilities(nai, nc));
        processLinkPropertiesFromAgent(nai, lp);
        nai.linkProperties = lp;

        nai.onNetworkMonitorCreated(networkMonitor);

        mNetworkAgentInfos.add(nai);
        synchronized (mNetworkForNetId) {
            mNetworkForNetId.put(nai.network.getNetId(), nai);
        }

        try {
            networkMonitor.start();
        } catch (RemoteException e) {
            e.rethrowAsRuntimeException();
        }

        nai.notifyRegistered();
        NetworkInfo networkInfo = nai.networkInfo;
        updateNetworkInfo(nai, networkInfo);
        updateVpnUids(nai, null, nai.networkCapabilities);
    }

在这里启动networkMonitor,似乎又和上文中某些节点对应上了;
并且我们记得NetworkAgentInfoINetworkMonitor正好是在上文中另一个Handler分析中的registerNetworkAgentInternal方法中创建的:

    private Network registerNetworkAgentInternal(INetworkAgent na, NetworkInfo networkInfo,
            LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
            NetworkScore currentScore, NetworkAgentConfig networkAgentConfig, int providerId,
            int uid) {
            
        final NetworkAgentInfo nai = new NetworkAgentInfo(na,
                new Network(mNetIdManager.reserveNetId()), new NetworkInfo(networkInfo),
                linkProperties, networkCapabilities,
                currentScore, mContext, mTrackerHandler, new NetworkAgentConfig(networkAgentConfig),
                this, mNetd, mDnsResolver, providerId, uid, mLingerDelayMs,
                mQosCallbackTracker, mDeps);
                
        mDeps.getNetworkStack().makeNetworkMonitor(
                nai.network, name, new NetworkMonitorCallbacks(nai));
        return nai.network;
    }

来看看是谁把这里创建的两种信息传递到InternalHandler并交由handleRegisterNetworkAgent方法的;

    private class InternalHandler extends Handler {
        public InternalHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case EVENT_REGISTER_NETWORK_AGENT: {
                    final Pair<NetworkAgentInfo, INetworkMonitor> arg =
                            (Pair<NetworkAgentInfo, INetworkMonitor>) msg.obj;
                    handleRegisterNetworkAgent(arg.first, arg.second);
                    break;
                }
            }
        }
     }

然后惊喜地发现,是老熟人了;

    private class NetworkMonitorCallbacks extends INetworkMonitorCallbacks.Stub {

        @Override
        public void onNetworkMonitorCreated(INetworkMonitor networkMonitor) {
            mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_AGENT,
                    new Pair<>(mNai.getAndDestroy(), networkMonitor)));
        }
	}

当然这里的回调是进程间的调用,关系到packages/modules/NetworkStack/src/com/android/server/NetworkStackService.java

public class NetworkStackService extends Service {
        @Override
        public void makeNetworkMonitor(Network network, String name, INetworkMonitorCallbacks cb)
                throws RemoteException {
            mPermChecker.enforceNetworkStackCallingPermission();
            updateNetworkStackAidlVersion(cb.getInterfaceVersion(), cb.getInterfaceHash());
            final SharedLog log = addValidationLogs(network, name);
            final NetworkMonitor nm = mDeps.makeNetworkMonitor(mContext, cb, network, log, this);
            cb.onNetworkMonitorCreated(new NetworkMonitorConnector(nm, mPermChecker));
        }
}

于是大致的脉络就有了——
registerNetworkAgentInternal方法中创建了NetworkAgentInfo和INetworkMonitor,并且由于调用了makeNetworkMonitor方法以及NetworkMonitorCallbacks回调存在的原因,NetworkMonitor创建时就回调了onNetworkMonitorCreated方法,随之向InternalHandler发送消息,调用了handleRegisterNetworkAgent方法。

那么接下来的问题就是,registerNetworkAgentInternal是干什么的,什么时候会调用呢?

registerNetworkAgentInternal的来由

往上追溯,还在ConnectivityService中,但却罕见的是一个公有方法了,意味着这可能是一个入口;

    public Network registerNetworkAgent(INetworkAgent na, NetworkInfo networkInfo,
            LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
            @NonNull NetworkScore initialScore, NetworkAgentConfig networkAgentConfig,
            int providerId) {
        //...略
        final int uid = mDeps.getCallingUid();
        final long token = Binder.clearCallingIdentity();
        try {
            return registerNetworkAgentInternal(na, networkInfo, linkProperties,
                    networkCapabilities, initialScore, networkAgentConfig, providerId, uid);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

它也确实是一个入口,毕竟ConnectivityService本身就太眼熟了;
packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java中找到了;

    public Network registerNetworkAgent(INetworkAgent na, NetworkInfo ni, LinkProperties lp,
            NetworkCapabilities nc, @NonNull NetworkScore score, NetworkAgentConfig config,
            int providerId) {
        try {
            return mService.registerNetworkAgent(na, ni, lp, nc, score, config, providerId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

ConnectivityManager就更熟悉了;
不过调用这个方法的地方却极少,只在packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java中出现;

public abstract class NetworkAgent {
    public Network register() {
        if (VDBG) log("Registering NetworkAgent");
        synchronized (mRegisterLock) {
            if (mNetwork != null) {
                throw new IllegalStateException("Agent already registered");
            }
            final ConnectivityManager cm = (ConnectivityManager) mInitialConfiguration.context
                    .getSystemService(Context.CONNECTIVITY_SERVICE);
            mNetwork = cm.registerNetworkAgent(new NetworkAgentBinder(mHandler),
                    new NetworkInfo(mInitialConfiguration.info),
                    mInitialConfiguration.properties, mInitialConfiguration.capabilities,
                    mInitialConfiguration.score, mInitialConfiguration.config, providerId);
            mInitialConfiguration = null; // All this memory can now be GC'd
        }
        return mNetwork;
    }
}

NetworkAgent的作用

NetworkAgent,网络代理,注册;
既然是抽象类,那么就肯定有具体的实现类;
如果只讨论wifi的话,就是packages/modules/Wifi/service/java/com/android/server/wifi/WifiNetworkAgent.java

public class WifiNetworkAgent extends NetworkAgent {
    public WifiNetworkAgent(
            @NonNull Context context,
            @NonNull Looper looper,
            @NonNull NetworkCapabilities nc,
            @NonNull LinkProperties lp,
            @NonNull NetworkAgentConfig config,
            @Nullable NetworkProvider provider,
            @NonNull Callback wifiNetworkAgentCallback) {
        super(context, looper, TAG, nc, lp, ConnectedScore.WIFI_INITIAL_SCORE, config, provider);
        mCurrentNetworkCapabilities = nc;
        mCallback = wifiNetworkAgentCallback;
        register();
    }
}

构造方法中就有register方法;
哪里有相关应用呢?
比如packages/modules/Wifi/service/java/com/android/server/wifi/ClientModeImpl.java中;

public class ClientModeImpl extends StateMachine implements ClientMode {
    class L2ConnectedState extends State {

        @Override
        public void enter() {
            //...略
            mNetworkAgent = mWifiInjector.makeWifiNetworkAgent(nc, mLinkProperties, naConfig,
                    mNetworkFactory.getProvider(), new WifiNetworkAgentCallback());
			//...略
        }
        //...略
    }
}

ClientModeImpl也是一个状态机,根据其构造方法中添加的各类状态来看,它关系着wifi的连接以及相关配置;

        addState(mConnectableState); {
            addState(mConnectingOrConnectedState, mConnectableState); {
                addState(mL2ConnectingState, mConnectingOrConnectedState);
                addState(mL2ConnectedState, mConnectingOrConnectedState); {
                    addState(mL3ProvisioningState, mL2ConnectedState);
                    addState(mL3ConnectedState, mL2ConnectedState);
                    addState(mRoamingState, mL2ConnectedState);
                }
            }
            addState(mDisconnectedState, mConnectableState);
        }

        setInitialState(mDisconnectedState);

具体的状态转换不再赘述,理解大致脉络即可。

总结时序

照例来张时序图梳理一下;
连接受限时序
Frameworks里的东西就这样,牵扯太广,凑合着看,其实只要有个模糊的概念就行;
主要是要习惯并掌握追溯流程的方法。
以上。

;