【Android-WIFI】(三) Wifi 连接

Station 模式下的连接的实现主要是在 ClientModeImpl 中。ClientModeImpl 继承自一个状态机,它的状态转换模型大如下图所示:
在这里插入图片描述

1 扫描结果可连接,自动回调连接

//packages/modules/Wifi/service/java/com/android/server/wifi/WifiConnectivityManager.java
private void handleScanResults(@NonNull List<ScanDetail> scanDetails,
        @NonNull String listenerName,
        boolean isFullScan,
        @NonNull HandleScanResultsListener handleScanResultsListener) {
    List<WifiNetworkSelector.ClientModeManagerState> cmmStates = new ArrayList<>();
    Set<String> connectedSsids = new HashSet<>();
    boolean hasExistingSecondaryCmm = false;
    for (ClientModeManager clientModeManager :
            mActiveModeWarden.getInternetConnectivityClientModeManagers()) {
        Log.d(TAG, "getInternetConnectivityClientModeManagers = " + clientModeManager.getRole());
        if (clientModeManager.getRole() == ROLE_CLIENT_SECONDARY_LONG_LIVED) {
            hasExistingSecondaryCmm = true;
            Log.d(TAG, "hasExistingSecondaryCmm = " + hasExistingSecondaryCmm);
        }
        mWifiChannelUtilization.refreshChannelStatsAndChannelUtilization(
                clientModeManager.getWifiLinkLayerStats(),
                WifiChannelUtilization.UNKNOWN_FREQ);
        WifiInfo wifiInfo = clientModeManager.syncRequestConnectionInfo();
        if (clientModeManager.isConnected()) {
            connectedSsids.add(wifiInfo.getSSID());
        }
        cmmStates.add(new WifiNetworkSelector.ClientModeManagerState(clientModeManager));
    }
    // We don't have any existing secondary CMM, but are we allowed to create a secondary CMM
    // and do we have a request for OEM_PAID/OEM_PRIVATE request? If yes, we need to perform
    // network selection to check if we have any potential candidate for the secondary CMM
    // creation.
    if (!hasExistingSecondaryCmm
            && (mOemPaidConnectionAllowed || mOemPrivateConnectionAllowed)) {
        // prefer OEM PAID requestor if it exists.
        WorkSource oemPaidOrOemPrivateRequestorWs =
                mOemPaidConnectionRequestorWs != null
                        ? mOemPaidConnectionRequestorWs
                        : mOemPrivateConnectionRequestorWs;
        if (oemPaidOrOemPrivateRequestorWs == null) {
            Log.e(TAG, "Both mOemPaidConnectionRequestorWs & mOemPrivateConnectionRequestorWs "
                    + "are null!");
        }
        if (oemPaidOrOemPrivateRequestorWs != null
                && mActiveModeWarden.canRequestMoreClientModeManagersInRole(
                        oemPaidOrOemPrivateRequestorWs,
                        ROLE_CLIENT_SECONDARY_LONG_LIVED)) {
            // Add a placeholder CMM state to ensure network selection is performed for a
            // potential second STA creation.
            cmmStates.add(new WifiNetworkSelector.ClientModeManagerState());
        }
    }
    for (ScanDetail scanDetail : scanDetails) {
        Log.d(TAG, "handleScanResults:" + scanDetail.getScanResult());
    }
    // Check if any blocklisted BSSIDs can be freed.
    mWifiBlocklistMonitor.tryEnablingBlockedBssids(scanDetails);
    for (ScanDetail scanDetail : scanDetails) {
        Log.d(TAG, "handleScanResults After mWifiBlocklistMonitor :" + scanDetail.getScanResult());
    }

    Set<String> bssidBlocklist = mWifiBlocklistMonitor.updateAndGetBssidBlocklistForSsids(
            connectedSsids);
    updateUserDisabledList(scanDetails);
    for (ScanDetail scanDetail : scanDetails) {
        Log.d(TAG, "handleScanResults After:" + scanDetail.getScanResult());
    }
    // Clear expired recent failure statuses
    mConfigManager.cleanupExpiredRecentFailureReasons();

    localLog(listenerName + " onResults: start network selection");

    List<WifiCandidates.Candidate> candidates = mNetworkSelector.getCandidatesFromScan(
            scanDetails, bssidBlocklist, cmmStates, mUntrustedConnectionAllowed,
            mOemPaidConnectionAllowed, mOemPrivateConnectionAllowed);
    mLatestCandidates = candidates;
    mLatestCandidatesTimestampMs = mClock.getElapsedSinceBootMillis();

    if (mDeviceMobilityState == WifiManager.DEVICE_MOBILITY_STATE_HIGH_MVMT
            && mContext.getResources().getBoolean(
                    R.bool.config_wifiHighMovementNetworkSelectionOptimizationEnabled)) {
        candidates = filterCandidatesHighMovement(candidates, listenerName, isFullScan);
    }

    mLastNetworkSelectionTimeStamp = mClock.getElapsedSinceBootMillis();
    mWifiLastResortWatchdog.updateAvailableNetworks(
            mNetworkSelector.getConnectableScanDetails());
    mWifiMetrics.countScanResults(scanDetails);
    // No candidates, return early.
    if (candidates == null || candidates.size() == 0) {
        localLog(listenerName + ":  No candidates");
        handleScanResultsWithNoCandidate(handleScanResultsListener);
        return;
    }
    // We have an oem paid/private network request and device supports STA + STA, check if there
    // are oem paid/private suggestions.
    if ((mOemPaidConnectionAllowed || mOemPrivateConnectionAllowed)
            && mActiveModeWarden.isStaStaConcurrencySupportedForRestrictedConnections()) {
        // Split the candidates based on whether they are oem paid/oem private or not.
        Map<Boolean, List<WifiCandidates.Candidate>> candidatesPartitioned =
                candidates.stream()
                        .collect(Collectors.groupingBy(c -> c.isOemPaid() || c.isOemPrivate()));
        List<WifiCandidates.Candidate> primaryCmmCandidates =
                candidatesPartitioned.getOrDefault(false, Collections.emptyList());
        List<WifiCandidates.Candidate> secondaryCmmCandidates =
                candidatesPartitioned.getOrDefault(true, Collections.emptyList());
        List<WifiCandidates.Candidate> secondaryCmmCandidates_filtered =
                getSecondaryCandidatesFiltered(secondaryCmmCandidates);
        // Some oem paid/private suggestions found, use secondary cmm flow.
        if (!secondaryCmmCandidates.isEmpty()) {
            handleCandidatesFromScanResultsUsingSecondaryCmmIfAvailable(
                    listenerName, primaryCmmCandidates, secondaryCmmCandidates_filtered,
                    handleScanResultsListener);
            return;
        }
        // intentional fallthrough: No oem paid/private suggestions, fallback to legacy flow.
    }
    handleCandidatesFromScanResultsForPrimaryCmmUsingMbbIfAvailable(
            listenerName, candidates, handleScanResultsListener);
}

startScan 完成以后回调到 handleScanResults 函数来连接到扫描到的网络中。ClientModeImpl 的初始状态 DisconnectedState 的父状态 ConnectableState 的 enter 中创建了 IpClient。

当处理 Wi-Fi 扫描结果时,这段代码涉及到对不同状态和候选网络的处理。下面逐行进行分析:

创建一个空的 WifiNetworkSelector.ClientModeManagerState 列表,用于存储各个客户端模式管理器的状态。

创建一个空的 Set,用于存储连接的 SSID(Service Set Identifier)。

初始化一个标志变量 hasExistingSecondaryCmm,用于检测是否存在次要的客户端模式管理器。

对于每个活动的客户端模式管理器,执行以下操作:

  • 检查客户端模式管理器的角色,如果是 ROLE_CLIENT_SECONDARY_LONG_LIVED,则设置 hasExistingSecondaryCmm 为 true。
  • 刷新 Wi-Fi 通道统计信息和通道利用率。
  • 同步获取 Wi-Fi 连接信息。
  • 如果客户端模式管理器已连接,则将连接的 SSID 添加到集合中。
  • 创建并添加客户端模式管理器的状态到 cmmStates 列表中。

在没有现有次要客户端模式管理器的情况下,检查是否允许创建次要客户端模式管理器,如果允许则执行以下操作:

  • 根据是否存在 OEM_PAID 或 OEM_PRIVATE 请求,选择合适的请求器。
  • 如果请求器存在且可以在 ROLE_CLIENT_SECONDARY_LONG_LIVED 角色中请求更多的客户端模式管理器,则添加一个占位的客户端模式管理器状态。

尝试启用被阻止的 BSSID,更新并获取被阻止的 BSSID 列表。更新用户禁用的网络列表。清除过期的失败连接。从扫描结果中获取候选网络。如果设备的移动性状态为高移动性且启用了高移动性网络选择优化,则过滤候选网络。更新看门狗的可用网络列表。记录扫描结果计数。如果没有候选网络,记录日志并通过 handleScanResultsWithNoCandidate 处理没有候选网络的情况。

如果存在 OEM_PAID 或 OEM_PRIVATE 请求并且设备支持 STA + STA 并发连接,执行以下操作:

  • 将候选网络分成两组:oem paid/oem private 和非 oem paid/oem private。
  • 如果存在次要候选网络,则使用 handleCandidatesFromScanResultsUsingSecondaryCmmIfAvailable 处理。
  • 否则,使用 handleCandidatesFromScanResultsForPrimaryCmmUsingMbbIfAvailable 处理。

两个 handle 函数继续向下都调用到 connectToNetworkUsingCmm,在正常情况下函数执行到最后,调用 ConnectHandler 的 triggerConnectWhenDisconnected -> …-> ClientModeImpl 的 startConnectToNetwork。startConnectToNetwork 向 ClientModeImpl 的内部状态机发送一个 CMD_START_CONNECT 消息,处理过程在手动连接过程也会有。

2 手动连接

//packages/apps/Settings/src/com/android/settings/wifi/WifiSettings.java
public void onSubmit(WifiDialog2 dialog) {
    final int dialogMode = dialog.getMode();
    final WifiConfiguration config = dialog.getController().getConfig();
    final WifiEntry wifiEntry = dialog.getWifiEntry();

    if (dialogMode == WifiConfigUiBase2.MODE_MODIFY) {
        if (config == null) {
            Toast.makeText(getContext(), R.string.wifi_failed_save_message,
                    Toast.LENGTH_SHORT).show();
        } else {
            mWifiManager.save(config, mSaveListener);
        }
    } else if (dialogMode == WifiConfigUiBase2.MODE_CONNECT
            || (dialogMode == WifiConfigUiBase2.MODE_VIEW && wifiEntry.canConnect())) {
        if (config == null) {
            connect(wifiEntry, false /* editIfNoConfig */,
                    false /* fullScreenEdit*/);
        } else {
            mWifiManager.connect(config, new WifiConnectActionListener());
        }
    }
}
  1. 获取对话框模式 (dialogMode)、config 和 WifiEntry。
  2. 如果对话框模式是修改模式,则检查配置是否为空。如果为空,显示一个短暂的 Toast 提示用户保存失败;如果不为空,使用 WifiManager 保存配置,并传入一个 SaveListener。
  3. 如果对话框模式是连接模式,或者是查看模式且可以连接,则继续执行下面的操作。
    1. 如果配置为空,调用connect方法,传入WifiEntry,编辑标志位false,全屏编辑标志位false。
    2. 如果配置不为空,则使用WifiManager连接配置,并传入一个连接操作监听器(WifiConnectActionListener)。

我们主要考虑连接模式。

//packages/modules/Wifi/framework/java/android/net/wifi/WifiManager.java
public void connect(@NonNull WifiConfiguration config, @Nullable ActionListener listener) {
    if (config == null) throw new IllegalArgumentException("config cannot be null");
    connectInternal(config, WifiConfiguration.INVALID_NETWORK_ID, listener);
}

private void connectInternal(@Nullable WifiConfiguration config, int networkId,
        @Nullable ActionListener listener) {
    ActionListenerProxy listenerProxy = null;
    if (listener != null) {
        listenerProxy = new ActionListenerProxy("connect", mLooper, listener);
    }
    mService.connect(config, networkId, listenerProxy);
    
}

通过 Binder 机制调用到 WifiServiceImpl 的 connect 方法。

//packages/modules/Wifi/service/java/com/android/server/wifi/WifiServiceImpl.java
public void connect(WifiConfiguration config, int netId, @Nullable IActionListener callback) {
    int uid = Binder.getCallingUid();
    if (!isPrivileged(Binder.getCallingPid(), uid)) {
        throw new SecurityException(TAG + ": Permission denied");
    }
    if (config != null) {
        config.networkId = removeSecurityTypeFromNetworkId(config.networkId);
    }
    final int netIdArg = removeSecurityTypeFromNetworkId(netId);
    mLog.info("connect uid=%").c(uid).flush();
    mWifiThreadRunner.post(() -> {
        ActionListenerWrapper wrapper = new ActionListenerWrapper(callback);
        final NetworkUpdateResult result;
        // if connecting using WifiConfiguration, save the network first
        if (config != null) {
            if (mWifiPermissionsUtil.checkNetworkSettingsPermission(uid)) {
                mWifiMetrics.logUserActionEvent(
                        UserActionEvent.EVENT_ADD_OR_UPDATE_NETWORK, config.networkId);
            }
            result = mWifiConfigManager.addOrUpdateNetwork(config, uid);
            if (!result.isSuccess()) {
                Log.e(TAG, "connect adding/updating config=" + config + " failed");
                wrapper.sendFailure(WifiManager.ERROR);
                return;
            }
            broadcastWifiCredentialChanged(WifiManager.WIFI_CREDENTIAL_SAVED, config);
        } else {
            if (mWifiPermissionsUtil.checkNetworkSettingsPermission(uid)) {
                mWifiMetrics.logUserActionEvent(
                        UserActionEvent.EVENT_MANUAL_CONNECT, netIdArg);
            }
            result = new NetworkUpdateResult(netIdArg);
        }
        WifiConfiguration configuration = mWifiConfigManager
                .getConfiguredNetwork(result.getNetworkId());
        if (configuration == null) {
            Log.e(TAG, "connect to Invalid network Id=" + netIdArg);
            wrapper.sendFailure(WifiManager.ERROR);
            return;
        }
        if (configuration.enterpriseConfig != null
                && configuration.enterpriseConfig.isAuthenticationSimBased()) {
            int subId = mWifiCarrierInfoManager.getBestMatchSubscriptionId(configuration);
            if (!mWifiCarrierInfoManager.isSimReady(subId)) {
                Log.e(TAG, "connect to SIM-based config=" + configuration
                        + "while SIM is absent");
                wrapper.sendFailure(WifiManager.ERROR);
                return;
            }
            if (mWifiCarrierInfoManager.requiresImsiEncryption(subId)
                    && !mWifiCarrierInfoManager.isImsiEncryptionInfoAvailable(subId)) {
                Log.e(TAG, "Imsi protection required but not available for Network="
                        + configuration);
                wrapper.sendFailure(WifiManager.ERROR);
                return;
            }
        }
        mMakeBeforeBreakManager.stopAllSecondaryTransientClientModeManagers(() ->
                mConnectHelper.connectToNetwork(result, wrapper, uid));
    });
}

它首先检查调用进程是否具有特权权限(privileged),如果没有则抛出 SecurityException 异常。

接下来,它将从网络 ID 和 WifiConfiguration 对象中删除安全类型信息,然后记录连接尝试的用户ID(uid)。

然后,它将在 WifiThreadRunner 线程上运行一个 Runnable,该 Runnable 执行以下操作:

  1. 如果使用 WifiConfiguration 连接,会先保存网络配置。它首先检查调用进程是否具有网络设置权限,并记录用户操作事件。然后调用 WifiConfigManager 的 addOrUpdateNetwork 方法添加或更新网络配置,并检查操作结果。如果操作失败,将发送一个失败回调并返回。
  2. 如果不是使用 WifiConfiguration 连接,而是使用网络 ID 连接,它会继续进行连接操作。
  3. 根据网络 ID 获取配置的网络对象,如果获取失败,则发送一个失败回调并返回。
  4. 如果配置的网络是SIM卡认证类型的网络,并且 SIM 卡不可用,则发送一个失败回调并返回。
  5. 如果使用 IMSI 加密保护,并且 IMSI 加密信息不可用,则发送一个失败回调并返回。
  6. 最后,调用 MakeBeforeBreakManager 的 stopAllSecondaryTransientClientModeManagers 方法,停止所有辅助短暂客户端模式管理器。然后调用 ConnectHelper的connectToNetwork 方法来连接到网络。
//packages/modules/Wifi/service/java/com/android/server/wifi/ConnectHelper.java
public void connectToNetwork(
        @NonNull NetworkUpdateResult result,
        @NonNull ActionListenerWrapper wrapper,
        int callingUid) {
    mActiveModeWarden.disconnectSecondaryClientIfNecessary(
            mWifiConfigManager.getConfiguredNetwork(result.getNetworkId()));
    connectToNetwork(
            mActiveModeWarden.getPrimaryClientModeManager(), result, wrapper, callingUid);
}

public void connectToNetwork(
        @NonNull ClientModeManager clientModeManager,
        @NonNull NetworkUpdateResult result,
        @NonNull ActionListenerWrapper wrapper,
        int callingUid) {
    int netId = result.getNetworkId();
    if (mWifiConfigManager.getConfiguredNetwork(netId) == null) {
        Log.e(TAG, "connectToNetwork Invalid network Id=" + netId);
        wrapper.sendFailure(WifiManager.ERROR);
        return;
    }
    mWifiConfigManager.updateBeforeConnect(netId, callingUid);
    clientModeManager.connectNetwork(result, wrapper, callingUid);
}

//packages/modules/Wifi/service/java/com/android/server/wifi/ConcreteClientModeManager.java
public void connectNetwork(NetworkUpdateResult result, ActionListenerWrapper wrapper,
        int callingUid) {
    Log.d(getTag(), "connectNetwork: " + result);
    getClientMode().connectNetwork(result, wrapper, callingUid);
}
    
//packages/modules/Wifi/service/java/com/android/server/wifi/ClientModeImpl.java
public void connectNetwork(NetworkUpdateResult result, ActionListenerWrapper wrapper,
        int callingUid) {
    Message message =
            obtainMessage(CMD_CONNECT_NETWORK, new ConnectNetworkMessage(result, wrapper));
    message.sendingUid = callingUid;
    sendMessage(message);
}

经过一系列调用,一直调用到 ClientModeImpl 中,调用到 connectNetwork 函数,其中向自身状态机中发送了一个 CMD_CONNECT_NETWORK 消息。

//packages/modules/Wifi/service/java/com/android/server/wifi/ClientModeImpl.java/DisconnectedState.class
case CMD_CONNECT_NETWORK: {
    ConnectNetworkMessage cnm = (ConnectNetworkMessage) message.obj;
    if (mIpClient == null) {
        logd("IpClient is not ready, CONNECT_NETWORK dropped");
        cnm.listener.sendFailure(WifiManager.ERROR);
        break;
    }
    NetworkUpdateResult result = cnm.result;
    int netId = result.getNetworkId();
    connectToUserSelectNetwork(
            netId, message.sendingUid, result.hasCredentialChanged());
    mWifiMetrics.logStaEvent(mInterfaceName, StaEvent.TYPE_CONNECT_NETWORK,
            mWifiConfigManager.getConfiguredNetwork(netId));
    cnm.listener.sendSuccess();
    break;
}

private void connectToUserSelectNetwork(int netId, int uid, boolean forceReconnect) {
    logd("connectToUserSelectNetwork netId " + netId + ", uid " + uid
            + ", forceReconnect = " + forceReconnect);
    if (!forceReconnect && (mLastNetworkId == netId || mTargetNetworkId == netId)) {
        // We're already connecting/connected to the user specified network, don't trigger a
        // reconnection unless it was forced.
        logi("connectToUserSelectNetwork already connecting/connected=" + netId);
    } else {
        mWifiConnectivityManager.prepareForForcedConnection(netId);
        if (uid == Process.SYSTEM_UID) {
            mWifiMetrics.setNominatorForNetwork(netId,
                    WifiMetricsProto.ConnectionEvent.NOMINATOR_MANUAL);
        }
        startConnectToNetwork(netId, uid, SUPPLICANT_BSSID_ANY);
    }
}

public void startConnectToNetwork(int networkId, int uid, String bssid) {
    sendMessage(CMD_START_CONNECT, networkId, uid, bssid);
}

在 CMD_CONNECT_NETWORK 的情况下,首先获取 ConnectNetworkMessage 对象中的数据。然后检查 mIpClient是否为空,如果为空表示 IpClient 没有准备好,会记录日志,发送一个错误回调给监听器,并结束。否则,从结果中获取网络 ID,并调用 connectToUserSelectNetwork 方法连接到用户选择的网络。连接过程中会记录连接事件到 WifiMetrics,然后发送一个成功回调给监听器。

connectToUserSelectNetwork 方法会比较当前正在连接/已连接的网络 ID 和用户选择的网络 ID,如果相同且没有强制重新连接的需求,则不触发重新连接。否则,调用 WifiConnectivityManager 的 prepareForForcedConnection 方法准备强制连接网络,如果连接是由系统进程发起的,则将网络 ID 设为手动指定,并调用 startConnectToNetwork 方法开始连接网络。

startConnectToNetwork 方法向自身状态机发送 CMD_START_CONNECT 消息,将网络 ID、UID 和 BSSID 作为参数传递。

//packages/modules/Wifi/service/java/com/android/server/wifi/ClientModeImpl.java/DisconnectedState.class
case CMD_START_CONNECT: {
    if (mIpClient == null) {
        logd("IpClient is not ready, START_CONNECT dropped");

        break;
    }
    /* connect command coming from auto-join */
    int netId = message.arg1;
    int uid = message.arg2;
    String bssid = (String) message.obj;
    mSentHLPs = false;
    // Stop lingering (if it was lingering before) if we start a new connection.
    // This means that the ClientModeManager was reused for another purpose, so it
    // should no longer be in lingering mode.
    mClientModeManager.setShouldReduceNetworkScore(false);

    if (!hasConnectionRequests()) {
        if (mNetworkAgent == null) {
            loge("CMD_START_CONNECT but no requests and not connected,"
                    + " bailing");
            //break;
        } else if (!mWifiPermissionsUtil.checkNetworkSettingsPermission(uid)) {
            loge("CMD_START_CONNECT but no requests and connected, but app "
                    + "does not have sufficient permissions, bailing");
            break;
        }
    }
    WifiConfiguration config =
            mWifiConfigManager.getConfiguredNetworkWithoutMasking(netId);
    logd("CMD_START_CONNECT "
            + " my state " + getCurrentState().getName()
            + " nid=" + netId
            + " roam=" + mIsAutoRoaming);
    if (config == null) {
        loge("CMD_START_CONNECT and no config, bail out...");
        break;
    }
    mTargetNetworkId = netId;
    // Update scorecard while there is still state from existing connection
    mLastScanRssi = mWifiConfigManager.findScanRssi(netId,
            mWifiHealthMonitor.getScanRssiValidTimeMs());
    mWifiScoreCard.noteConnectionAttempt(mWifiInfo, mLastScanRssi, config.SSID);
    mWifiBlocklistMonitor.setAllowlistSsids(config.SSID, Collections.emptyList());
    mWifiBlocklistMonitor.updateFirmwareRoamingConfiguration(Set.of(config.SSID));

    updateWifiConfigOnStartConnection(config, bssid);
    reportConnectionAttemptStart(config, mTargetBssid,
            WifiMetricsProto.ConnectionEvent.ROAM_UNRELATED);

    String currentMacAddress = mWifiNative.getMacAddress(mInterfaceName);
    mWifiInfo.setMacAddress(currentMacAddress);
    Log.i(getTag(), "Connecting with " + currentMacAddress + " as the mac address");

    mTargetWifiConfiguration = config;
    mNetworkNotFoundEventCount = 0;
    /* Check for FILS configuration again after updating the config */
    if (config.isFilsSha256Enabled() || config.isFilsSha384Enabled()) {
        boolean isIpClientStarted = startIpClient(config, true);
        if (isIpClientStarted) {
            mIpClientWithPreConnection = true;
            break;
        }
    }
    connectToNetwork(config);
    break;
}

首先检查 IpClient 是否为空,如果为空,则打印日志并中断该消息的处理。

获取消息的参数:netId、UID 和 BSSID。

设置 SentHLPs 标志位为 false。

如果没有连接请求,则检查是否存在 NetworkAgent。如果不存在网络代理且应用程序没有足够的权限访问网络设置,则打印日志并中断该消息的处理。

通过网络ID获取对应的 WifiConfiguration 配置。

更新一些状态和数据,包括 TargetNetworkId、上一次扫描到的信号强度 LastScanRssi、连接尝试记入WifiScoreCard、更新 WifiBlocklistMonitor 的配置等。

更新 Wi-Fi 接口的 MAC 地址为当前的物理地址。

设置目标 Wi-Fi 配置为当前配置。

连接前检查FILS配置,如果开启了FILS SHA256或FILS SHA384,则启动IP客户端。

调用connectToNetwork方法连接到指定的网络。

//packages/modules/Wifi/service/java/com/android/server/wifi/ClientModeImpl.java
    private void connectToNetwork(WifiConfiguration config) {
        if ((config != null) && mWifiNative.connectToNetwork(mInterfaceName, config)) {
            mWifiLastResortWatchdog.noteStartConnectTime(config.networkId);
            mWifiMetrics.logStaEvent(mInterfaceName, StaEvent.TYPE_CMD_START_CONNECT, config);
            mIsAutoRoaming = false;
            transitionTo(mL2ConnectingState);
        } else {
            loge("CMD_START_CONNECT Failed to start connection to network " + config);
            mTargetWifiConfiguration = null;
            stopIpClient();
            reportConnectionAttemptEnd(
                    WifiMetrics.ConnectionEvent.FAILURE_CONNECT_NETWORK_FAILED,
                    WifiMetricsProto.ConnectionEvent.HLF_NONE,
                    WifiMetricsProto.ConnectionEvent.FAILURE_REASON_UNKNOWN);
        }
    }

调用 WifiNative.connectToNetwork,WifiNative中通过 HDIL 调用到下层,来实现具体的连接过程。

//packages/modules/Wifi/service/java/com/android/server/wifi/WifiNative.java
public boolean connectToNetwork(@NonNull String ifaceName, WifiConfiguration configuration) {
    // Abort ongoing scan before connect() to unblock connection request.
    mWifiCondManager.abortScan(ifaceName);
    return mSupplicantStaIfaceHal.connectToNetwork(ifaceName, configuration);
}
//packages/modules/Wifi/service/java/com/android/server/wifi/SupplicantStaIfaceHal.java
public boolean connectToNetwork(@NonNull String ifaceName, @NonNull WifiConfiguration config) {
    synchronized (mLock) {
        logd("connectToNetwork " + config.getProfileKey());
        WifiConfiguration currentConfig = getCurrentNetworkLocalConfig(ifaceName);
        if (WifiConfigurationUtil.isSameNetwork(config, currentConfig)) {
            String networkSelectionBSSID = config.getNetworkSelectionStatus()
                    .getNetworkSelectionBSSID();
            String networkSelectionBSSIDCurrent =
                    currentConfig.getNetworkSelectionStatus().getNetworkSelectionBSSID();
            if (Objects.equals(networkSelectionBSSID, networkSelectionBSSIDCurrent)) {
                logd("Network is already saved, will not trigger remove and add operation.");
            } else {
                logd("Network is already saved, but need to update BSSID.");
                if (!setCurrentNetworkBssid(
                        ifaceName,
                        config.getNetworkSelectionStatus().getNetworkSelectionBSSID())) {
                    loge("Failed to set current network BSSID.");
                    return false;
                }
                mCurrentNetworkLocalConfigs.put(ifaceName, new WifiConfiguration(config));
            }
        } else {
            mCurrentNetworkRemoteHandles.remove(ifaceName);
            mCurrentNetworkLocalConfigs.remove(ifaceName);
            mLinkedNetworkLocalAndRemoteConfigs.remove(ifaceName);
            if (!removeAllNetworks(ifaceName)) {
                loge("Failed to remove existing networks");
                return false;
            }
            Pair<SupplicantStaNetworkHal, WifiConfiguration> pair =
                    addNetworkAndSaveConfig(ifaceName, config);
            if (pair == null) {
                loge("Failed to add/save network configuration: " + config
                        .getProfileKey());
                return false;
            }
            mCurrentNetworkRemoteHandles.put(ifaceName, pair.first);
            mCurrentNetworkLocalConfigs.put(ifaceName, pair.second);
        }
        SupplicantStaNetworkHal networkHandle =
                checkSupplicantStaNetworkAndLogFailure(ifaceName, "connectToNetwork");
        if (networkHandle == null) {
            loge("No valid remote network handle for network configuration: "
                    + config.getProfileKey());
            return false;
        }

        PmkCacheStoreData pmkData = mPmkCacheEntries.get(config.networkId);
        if (pmkData != null
                && !WifiConfigurationUtil.isConfigForPskNetwork(config)
                && pmkData.expirationTimeInSec > mClock.getElapsedSinceBootMillis() / 1000) {
            logi("Set PMK cache for config id " + config.networkId);
            if (networkHandle.setPmkCache(pmkData.data)) {
                mWifiMetrics.setConnectionPmkCache(ifaceName, true);
            }
        }

        if (!networkHandle.select()) {
            loge("Failed to select network configuration: " + config.getProfileKey());
            return false;
        }
        return true;
    }
}

首先终止扫描,接着调用 SupplicantStaIfaceHal 中的方式来实现连接。接下来是 connectToNetwork 的具体实现

  1. 通过调用getCurrentNetworkLocalConfig方法获取当前网络的本地配置。
    1. 如果传入的Wi-Fi配置和当前配置是同一个网络(使用WifiConfigurationUtil.isSameNetwork方法进行比较),则进一步比较网络选择的BSSID。
    2. 如果网络选择的BSSID相同,表示网络已保存,不需要执行删除和添加操作,打印日志进行提示。
    3. 如果网络选择的BSSID不同,表示网络已保存但需要更新BSSID,调用setCurrentNetworkBssid方法设置当前网络的BSSID,并更新当前网络的本地配置。
  2. 如果传入的Wi-Fi配置和当前配置不是同一个网络,需要执行以下操作:
    1. 从相应的映射表中移除当前网络的远程句柄、本地配置和本地与远程配置之间的映射关系。
    2. 调用removeAllNetworks方法移除所有已存在的网络。
    3. 调用addNetworkAndSaveConfig方法添加并保存网络配置,返回网络的远程句柄和本地配置。
      • 如果添加/保存网络配置失败,打印错误日志并返回失败。
    4. 将网络的远程句柄、本地配置分别存储到mCurrentNetworkRemoteHandlesmCurrentNetworkLocalConfigs映射表中。
  3. 使用checkSupplicantStaNetworkAndLogFailure方法检查并获取Wi-Fi网络的远程句柄,如果句柄无效,打印错误日志并返回失败。
  4. mPmkCacheEntries 缓存中获取与该网络ID相关的PMK缓存数据。
  5. 如果找到了PMK缓存数据,并且网络配置不是PSK网络类型,且PMK缓存未过期,则调用setPmkCache方法将PMK缓存设置到远程网络句柄中,并记录连接成功使用了PMK缓存的度量指标。
  6. 如果网络选择成功,调用远程网络句柄的select方法来选择配置的网络。如果选择失败,则打印错误日志并返回失败。
//packages/modules/Wifi/service/java/com/android/server/wifi/SupplicantStaNetworkHal.java
public boolean select() {
    synchronized (mLock) {
        final String methodStr = "select";
        if (!checkISupplicantStaNetworkAndLogFailure(methodStr)) return false;
        try {
            SupplicantStatus status = mISupplicantStaNetwork.select();
            return checkStatusAndLogFailure(status, methodStr);
        } catch (RemoteException e) {
            handleRemoteException(e, methodStr);
            return false;
        }
    }
}

//external/wpa_supplicant_8/wpa_supplicant/hidl/1.4/sta_network.cpp
Return<void> StaNetwork::select(select_cb _hidl_cb)
{
    return validateAndCall(
        this, SupplicantStatusCode::FAILURE_NETWORK_INVALID,
        &StaNetwork::selectInternal, _hidl_cb);
}

SupplicantStatus StaNetwork::selectInternal()
{
    struct wpa_ssid *wpa_ssid = retrieveNetworkPtr();
    if (wpa_ssid->disabled == 2) {
        return {SupplicantStatusCode::FAILURE_UNKNOWN, ""};
    }
    struct wpa_supplicant *wpa_s = retrieveIfacePtr();
    wpa_s->scan_min_time.sec = 0;
    wpa_s->scan_min_time.usec = 0;
    wpa_supplicant_select_network(wpa_s, wpa_ssid);
    return {SupplicantStatusCode::SUCCESS, ""};
}

SupplicantStaNetworkHal 中主要是向下调用到 HAL 层,由 wpa_supplicant 进程来实现具体的认证关联过程。接下来回到 ClientModeImpl 中,调用 WifiLastResortWatchdog 的 noteStartConnectTime 方法记录连接开始的时间,用于后续监控和故障恢复。将自动漫游标志设置为 false,表示当前连接不是自动漫游。更新 WifiInfo 的 networkId、BSSID 和 SSID。最后将当前状态转换为 L2ConnectingState。

在 HAL 层,在连接过程中,wpa_state 发生改变则会回调到 SupplicantStaIfaceCallbackImpl 的 onStateChanged。其中通过 WifiMonitor 发送了一个广播,调用 broadcastNetworkConnectionEvent 广播了 network connection event。

//packages/modules/Wifi/server/java/com/android/server/wifi/WifiMonitor.java
public void broadcastNetworkConnectionEvent(String iface, int networkId, boolean filsHlpSent,
        WifiSsid ssid, String bssid) {
    sendMessage(iface, NETWORK_CONNECTION_EVENT,
            new NetworkConnectionEventInfo(networkId, ssid, bssid, filsHlpSent));
}

WifiMonitor 向 ClientModeImpl 发送了一个 NETWORK_CONNECTION_EVENT 消息。在 L2ConnectingState 的状态下处理,但是实际上并未处理。所以交给它的父状态来处理,在父状态 ConnectingOrConnectedState 中处理。

//packages/modules/Wifi/service/java/com/android/server/wifi/ClientModeImpl.java/ConnectingOrConnectedState.class
case WifiMonitor.NETWORK_CONNECTION_EVENT: {
    if (mVerboseLoggingEnabled) log("ConnectingOrConnectedState Network connection established");
    NetworkConnectionEventInfo connectionInfo =
            (NetworkConnectionEventInfo) message.obj;
    mLastNetworkId = connectionInfo.networkId;
    mSentHLPs = connectionInfo.isFilsConnection;
    if (mSentHLPs) mWifiMetrics.incrementL2ConnectionThroughFilsAuthCount();
    mWifiConfigManager.clearRecentFailureReason(mLastNetworkId);
    mLastBssid = connectionInfo.bssid;
    // TODO: This check should not be needed after ClientModeImpl refactor.
    // Currently, the last connected network configuration is left in
    // wpa_supplicant, this may result in wpa_supplicant initiating connection
    // to it after a config store reload. Hence the old network Id lookups may not
    // work, so disconnect the network and let network selector reselect a new
    // network.
    WifiConfiguration config = getConnectedWifiConfigurationInternal();
    if (config == null) {
        logw("Connected to unknown networkId " + mLastNetworkId
                + ", disconnecting...");
        sendMessage(CMD_DISCONNECT, StaEvent.DISCONNECT_UNKNOWN_NETWORK);
        break;
    }
    mWifiInfo.setBSSID(mLastBssid);
    mWifiInfo.setNetworkId(mLastNetworkId);

    mWifiInfo.setMacAddress(mWifiNative.getMacAddress(mInterfaceName));

    ScanDetailCache scanDetailCache =
            mWifiConfigManager.getScanDetailCacheForNetwork(config.networkId);
    ScanResult scanResult = null;
    if (scanDetailCache != null && mLastBssid != null) {
        scanResult = scanDetailCache.getScanResult(mLastBssid);
        if (scanResult != null) {
            mWifiInfo.setFrequency(scanResult.frequency);
        }
    }
    if (mWifiInfo.getFrequency() == -1) {
        updateLinkLayerStatsRssiSpeedFrequencyCapabilities();
    }

    // We need to get the updated pseudonym from supplicant for EAP-SIM/AKA/AKA'
    if (config.enterpriseConfig != null
            && config.enterpriseConfig.isAuthenticationSimBased()) {
        mLastSubId = mWifiCarrierInfoManager.getBestMatchSubscriptionId(config);
        mLastSimBasedConnectionCarrierName =
                mWifiCarrierInfoManager.getCarrierNameForSubId(mLastSubId);
        String anonymousIdentity;
        if (mInterfaceName.equals("sawlan")){
            anonymousIdentity = null;
        }
        else{
            anonymousIdentity =   mWifiNative.getEapAnonymousIdentity(mInterfaceName);
        }
        if (!TextUtils.isEmpty(anonymousIdentity)
                && !WifiCarrierInfoManager
                .isAnonymousAtRealmIdentity(anonymousIdentity)) {
            String decoratedPseudonym = mWifiCarrierInfoManager
                    .decoratePseudonymWith3GppRealm(config,
                            anonymousIdentity);
            if (decoratedPseudonym != null) {
                anonymousIdentity = decoratedPseudonym;
            }
            if (mVerboseLoggingEnabled) {
                log("EAP Pseudonym: " + anonymousIdentity);
            }
            // Save the pseudonym only if it is a real one
            config.enterpriseConfig.setAnonymousIdentity(anonymousIdentity);
        } else {
            // Clear any stored pseudonyms
            config.enterpriseConfig.setAnonymousIdentity(null);
        }
        mWifiConfigManager.addOrUpdateNetwork(config, Process.WIFI_UID);
        if (config.isPasspoint()) {
            mPasspointManager.setAnonymousIdentity(config);
        } else if (config.fromWifiNetworkSuggestion) {
            mWifiNetworkSuggestionsManager.setAnonymousIdentity(config);
        }
    }
    // When connecting to Passpoint, ask for the Venue URL
    if (config.isPasspoint()) {
        mTermsAndConditionsUrl = null;
        if (scanResult == null && mLastBssid != null) {
            // The cached scan result of connected network would be null at the
            // first connection, try to check full scan result list again to look up
            // matched scan result associated to the current SSID and BSSID.
            scanResult = mScanRequestProxy.getScanResult(mLastBssid);
        }
        if (scanResult != null) {
            mPasspointManager.requestVenueUrlAnqpElement(scanResult);
        }
    }
    transitionTo(mL3ProvisioningState);
    break;
}
  • 如果启用了详细日志记录(mVerboseLoggingEnabled为true),输出日志信息。
  • 获取网络连接相关信息,包括网络ID(networkId)、是否使用Fils连接(isFilsConnection)、BSSID(bssid)等。
  • 如果使用了Fils连接(mSentHLPs为true),通过mWifiMetrics增加L2连接通过Fils认证的计数。
  • 清除最近的连接失败原因(mWifiConfigManager.clearRecentFailureReason)。
  • 设置最后连接的BSSID和网络ID(mLastBssid和mLastNetworkId)。
  • 根据当前接口名(mInterfaceName)获取MAC地址(mWifiInfo.setMacAddress)。
  • 获取与当前网络相关的扫描结果,并更新WiFi信息的频率(mWifiInfo.setFrequency)。
  • 如果频率为-1,则通过更新链路层统计的RSSI、速度、频率和能力信息来更新频率(updateLinkLayerStatsRssiSpeedFrequencyCapabilities)。
  • 如果当前网络配置是基于SIM卡认证的企业网络(EAP-SIM/AKA/AKA’),则获取更新的伪装标识(pseudonym)。
  • 如果伪装标识存在且不是匿名域标识,则使用3GPP域装饰伪装标识,并保存到企业配置中。
  • 根据网络类型的不同,更新网络配置并通知相应的管理器(mPasspointManager、mWifiNetworkSuggestionsManager)。
  • 如果连接的是Passpoint网络,通过扫描结果获取场所URL,并请求相关的ANQP元素(mPasspointManager.requestVenueUrlAnqpElement)。
  • 切换到L3ProvisioningState状态。

L3ProvisioningState 的父状态是 L2ConnectedState。

//packages/modules/Wifi/service/java/com/android/server/wifi/ClientModeImpl.java/L2ConnectedState.class
public void enter() {
    if (mVerboseLoggingEnabled) Log.v(getTag(), "enter L2ConnectedState");
    mRssiPollToken++;
    if (mEnableRssiPolling) {
        if (isPrimary()) {
            mLinkProbeManager.resetOnNewConnection();
        }
        sendMessage(CMD_RSSI_POLL, mRssiPollToken, 0);
    }
    sendNetworkChangeBroadcast(DetailedState.CONNECTING);

    // If this network was explicitly selected by the user, evaluate whether to inform
    // ConnectivityService of that fact so the system can treat it appropriately.
    final WifiConfiguration config = getConnectedWifiConfigurationInternal();

    final boolean explicitlySelected;
    // Non primary CMMs is never user selected. This prevents triggering the No Internet
    // dialog for those networks, which is difficult to handle.
    if (isPrimary() && isRecentlySelectedByTheUser(config)) {
        // If explicitlySelected is true, the network was selected by the user via Settings
        // or QuickSettings. If this network has Internet access, switch to it. Otherwise,
        // switch to it only if the user confirms that they really want to switch, or has
        // already confirmed and selected "Don't ask again".
        explicitlySelected =
                mWifiPermissionsUtil.checkNetworkSettingsPermission(config.lastConnectUid);
        if (mVerboseLoggingEnabled) {
            log("Network selected by UID " + config.lastConnectUid + " explicitlySelected="
                    + explicitlySelected);
        }
    } else {
        explicitlySelected = false;
    }

    if (mVerboseLoggingEnabled) {
        log("explicitlySelected=" + explicitlySelected + " acceptUnvalidated="
                + config.noInternetAccessExpected);
    }

    NetworkAgentConfig.Builder naConfigBuilder = new NetworkAgentConfig.Builder()
            .setLegacyType(ConnectivityManager.TYPE_WIFI)
            .setLegacyTypeName(NETWORKTYPE)
            .setExplicitlySelected(explicitlySelected)
            .setUnvalidatedConnectivityAcceptable(
                    explicitlySelected && config.noInternetAccessExpected)
            .setPartialConnectivityAcceptable(config.noInternetAccessExpected);
    if (config.carrierMerged) {
        String subscriberId = null;
        TelephonyManager subMgr = mTelephonyManager.createForSubscriptionId(
                config.subscriptionId);
        if (subMgr != null) {
            subscriberId = subMgr.getSubscriberId();
        }
        if (subscriberId != null) {
            naConfigBuilder.setSubscriberId(subscriberId);
        }
    }
    if (mVcnManager == null && SdkLevel.isAtLeastS()) {
        mVcnManager = mContext.getSystemService(VcnManager.class);
    }
    if (mVcnManager != null) {
        mVcnPolicyChangeListener = new WifiVcnNetworkPolicyChangeListener();
        mVcnManager.addVcnNetworkPolicyChangeListener(new HandlerExecutor(getHandler()),
                mVcnPolicyChangeListener);
    }
    final NetworkAgentConfig naConfig = naConfigBuilder.build();
    final NetworkCapabilities nc = getCapabilities(
            getConnectedWifiConfigurationInternal(), getConnectedBssidInternal());
    // This should never happen.
    if (mNetworkAgent != null) {
        Log.wtf(getTag(), "mNetworkAgent is not null: " + mNetworkAgent);
        mNetworkAgent.unregister();
    }
    mNetworkAgent = mWifiInjector.makeWifiNetworkAgent(nc, mLinkProperties, naConfig,
            mNetworkFactory.getProvider(), new WifiNetworkAgentCallback());
    log("mNetworkAgent = " + mNetworkAgent);
    mWifiScoreReport.setNetworkAgent(mNetworkAgent);

    // We must clear the config BSSID, as the wifi chipset may decide to roam
    // from this point on and having the BSSID specified in the network block would
    // cause the roam to faile and the device to disconnect
    clearTargetBssid("L2ConnectedState");
    mWifiMetrics.setWifiState(mInterfaceName, WifiMetricsProto.WifiLog.WIFI_ASSOCIATED);
    mWifiScoreCard.noteNetworkAgentCreated(mWifiInfo,
            mNetworkAgent.getNetwork().getNetId());
    mWifiBlocklistMonitor.handleBssidConnectionSuccess(mLastBssid, mWifiInfo.getSSID());
    // too many places to record connection failure with too many failure reasons.
    // So only record success here.
    mWifiMetrics.noteFirstL2ConnectionAfterBoot(true);
    mCmiMonitor.onL2Connected(mClientModeManager);
    mIsLinkedNetworkRoaming = false;

    if (mInterfaceName.equals("sawlan")) {
        sendMessage(CMD_IP_CONFIGURATION_SUCCESSFUL);
    }
}

检查是否启用了 RSSI 轮询并判断当前网络是不是主要网络,是的话则重置链接探测管理器。调用 sendMessage 方法发送一个消息 CMD_RSSI_POLL,带有 RssiPollToken参数。调用 sendNetworkChangeBroadcast 方法发送一个网络状态改变的广播,状态为 CONNECTING。获取当前连接的 WifiConfiguration对象。判断网络是否是用户显式选择的网络,并将结果赋值给 explicitlySelected。创建一个 NetworkAgentConfig.Builder 对象,并设置与网络代理相关的配置信息。接下来是针对合并运营商网络的处理逻辑。如果网络配置的 carrierMerged 属性为真,则获取订阅 ID 对应的运营商 ID,并将其设置到 naConfigBuilder 中。使用 naConfigBuilder 构建 NetworkAgentConfig 对象。调用getCapabilities 方法获取当前连接的NetworkCapabilities。创建一个 WifiNetworkAgent 对象。设置 WifiScoreReport的网络代理为新创建的 NetworkAgent。清除目标 BSSID,这样设备就可以在漫游时自由选择最佳的接入点,而不会受限于特定的 BSSID。接下来是各种记录事件。最后根据 InterfaceName 是否为"sawlan"来发送CMD_IP_CONFIGURATION_SUCCESSFUL 消息。我们配置的 Station 的 InterfaceName 就是 sawlan。

//packages/modules/Wifi/service/java/com/android/server/wifi/ClientModeImpl.java/L3ProvisioningState.class
public void enter() {
    if (mVerboseLoggingEnabled) Log.v(getTag(), "enter L3ProvisioningState");
    WifiConfiguration currentConfig = getConnectedWifiConfigurationInternal();
    if (mIpClientWithPreConnection && mIpClient != null) {
        mIpClient.notifyPreconnectionComplete(mSentHLPs);
        mIpClientWithPreConnection = false;
        mSentHLPs = false;
    } else {
        startIpClient(currentConfig, false);
    }
    // Get Link layer stats so as we get fresh tx packet counters
    getWifiLinkLayerStats();
}

mIpClientWithPreConnection = false;接下来 startIpClient,来获取相关的 L3 层配置。

在 L2ConnectedState 中的 enter 方法中,发送了一个 CMD_IP_CONFIGURATION_SUCCESSFUL 消息。

//packages/modules/Wifi/service/java/com/android/server/wifi/ClientModeImpl.java/L2ConnectedState.class
case CMD_IP_CONFIGURATION_SUCCESSFUL: {
    if (getConnectedWifiConfigurationInternal() == null || mNetworkAgent == null) {
        // The current config may have been removed while we were connecting,
        // trigger a disconnect to clear up state.
        reportConnectionAttemptEnd(
                WifiMetrics.ConnectionEvent.FAILURE_NETWORK_DISCONNECTION,
                WifiMetricsProto.ConnectionEvent.HLF_NONE,
                WifiMetricsProto.ConnectionEvent.FAILURE_REASON_UNKNOWN);
        mWifiNative.disconnect(mInterfaceName);
    } else {
        handleSuccessfulIpConfiguration();
        sendConnectedState();
        transitionTo(mL3ConnectedState);
    }
    break;
}

同样在 L2ConnectedState 处理这条消息,成功获取到相关 IP 配置。接着转换到 L3ConnectedState。

packages/modules/Wifi/service/java/com/android/server/wifi/ClientModeImpl.java/L3ConnectedState.class
public void enter() {
    if (mVerboseLoggingEnabled) {
        log("enter L3ConnectedState  mScreenOn=" + mScreenOn);
    }

    reportConnectionAttemptEnd(
            WifiMetrics.ConnectionEvent.FAILURE_NONE,
            WifiMetricsProto.ConnectionEvent.HLF_NONE,
            WifiMetricsProto.ConnectionEvent.FAILURE_REASON_UNKNOWN);
    mWifiConnectivityManager.handleConnectionStateChanged(
            mClientModeManager,
            WifiConnectivityManager.WIFI_STATE_CONNECTED);
    registerConnected();
    mTargetWifiConfiguration = null;
    mWifiScoreReport.reset();
    mLastSignalLevel = -1;

    // Not roaming anymore
    mIsAutoRoaming = false;

    mTargetNetworkId = WifiConfiguration.INVALID_NETWORK_ID;
    mWifiLastResortWatchdog.connectedStateTransition(true);
    mWifiStateTracker.updateState(mInterfaceName, WifiStateTracker.CONNECTED);
    // Inform WifiLockManager
    mWifiLockManager.updateWifiClientConnected(mClientModeManager, true);
    WifiConfiguration config = getConnectedWifiConfigurationInternal();
    mWifiScoreReport.startConnectedNetworkScorer(
            mNetworkAgent.getNetwork().getNetId(), isRecentlySelectedByTheUser(config));
    updateLinkLayerStatsRssiAndScoreReport();
    mWifiScoreCard.noteIpConfiguration(mWifiInfo);
    // too many places to record L3 failure with too many failure reasons.
    // So only record success here.
    mWifiMetrics.noteFirstL3ConnectionAfterBoot(true);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值