1. PNO扫描的发起
在前面介绍WiFi打开流程的时候有提到过,ClientModeImpl被创建后,进入初始状态DisconnectedState,会立即发起Connectivity Scan.
// packages/modules/Wifi/service/java/com/android/server/wifi/WifiConnectivityManager.java
startConnectivityScan(SCAN_IMMEDIATELY);
private void startConnectivityScan(boolean scanImmediately) {
// ... ...
if (mScreenOn) {
startPeriodicScan(scanImmediately);
} else {
if (mWifiState == WIFI_STATE_DISCONNECTED && !mPnoScanStarted) {
startDisconnectedPnoScan();
}
}
}
如果是亮屏状态,那么发起周期性扫描;如果是灭屏状态,并且WiFi未连接,就发起PNO扫描。这里我们只关注PNO扫描。
// packages/modules/Wifi/service/java/com/android/server/wifi/WifiConnectivityManager.java
// Start a DisconnectedPNO scan when screen is off and Wifi is disconnected
private void startDisconnectedPnoScan() {
// Initialize PNO settings
PnoSettings pnoSettings = new PnoSettings();
// 1. 获取preferred network list.
List<PnoSettings.PnoNetwork> pnoNetworkList = retrievePnoNetworkList();
int listSize = pnoNetworkList.size();
if (listSize == 0) {
// No saved network
localLog("No saved network for starting disconnected PNO.");
return;
}
// 2. pno设置
pnoSettings.networkList = new PnoSettings.PnoNetwork[listSize];
pnoSettings.networkList = pnoNetworkList.toArray(pnoSettings.networkList);
// 设置rssi的阈值,低于阈值的网络不会上报
pnoSettings.min6GHzRssi = mScoringParams.getEntryRssi(ScanResult.BAND_6_GHZ_START_FREQ_MHZ);
pnoSettings.min5GHzRssi = mScoringParams.getEntryRssi(ScanResult.BAND_5_GHZ_START_FREQ_MHZ);
pnoSettings.min24GHzRssi = mScoringParams.getEntryRssi(
ScanResult.BAND_24_GHZ_START_FREQ_MHZ);
// 3.扫描设置
// Initialize scan settings
ScanSettings scanSettings = new ScanSettings();
scanSettings.band = getScanBand();
scanSettings.reportEvents = WifiScanner.REPORT_EVENT_NO_BATCH;
scanSettings.numBssidsPerScan = 0;
scanSettings.periodInMs = deviceMobilityStateToPnoScanIntervalMs(mDeviceMobilityState);
mScanner.startDisconnectedPnoScan(
scanSettings, pnoSettings, new HandlerExecutor(mEventHandler), mPnoScanListener);
mPnoScanStarted = true;
mWifiMetrics.logPnoScanStart();
}
这个函数首先进行PNO扫描的参数设置,然后交给WifiScanner进行处理
// packages/modules/Wifi/framework/java/android/net/wifi/WifiScanner.java
public void startDisconnectedPnoScan(ScanSettings scanSettings, PnoSettings pnoSettings,
@NonNull @CallbackExecutor Executor executor, PnoScanListener listener) {
Objects.requireNonNull(listener, "listener cannot be null");
Objects.requireNonNull(pnoSettings, "pnoSettings cannot be null");
int key = addListener(listener, executor);
if (key == INVALID_KEY) return;
validateChannel();
pnoSettings.isConnected = false;
startPnoScan(scanSettings, pnoSettings, key);
}
private void startPnoScan(ScanSettings scanSettings, PnoSettings pnoSettings, int key) {
// Bundle up both the settings and send it across.
Bundle pnoParams = new Bundle();
// Set the PNO scan flag.
scanSettings.isPnoScan = true;
pnoParams.putParcelable(PNO_PARAMS_SCAN_SETTINGS_KEY, scanSettings);
pnoParams.putParcelable(PNO_PARAMS_PNO_SETTINGS_KEY, pnoSettings);
mAsyncChannel.sendMessage(CMD_START_PNO_SCAN, 0, key, pnoParams);
}
WifiScanner实际上只是提供了接口文件,最终还是会发消息CMD_START_PNO_SCAN
给WifiScanningServiceImpl去处理.
这里WifiScanner和WifiScanningServiceImpl是通过AsyncChannel通信的。
WifiScanningServiceImpl的处理如下
// packages/modules/Wifi/service/java/com/android/server/wifi/scanner/WifiScanningServiceImpl.java
private class ClientHandler extends Handler {
public void handleMessage(Message msg) {
switch (msg.what) {
// ... ...
case WifiScanner.CMD_START_PNO_SCAN:
case WifiScanner.CMD_STOP_PNO_SCAN:
mPnoScanStateMachine.sendMessage(Message.obtain(msg));
break;
// ... ...
}
}
}
PnoScanStateMachine的StartedState收到消息后不做什么处理,只是从msg中取出参数并检查是否支持PNO扫描,如果支持的就会进入HwPnoScanState,并且消息交给HwPnoScanState继续处理。
// HwPnoScanState.processMessage.CMD_START_PNO_SCAN
addHwPnoScanRequest(ci, msg.arg2, scanSettings, pnoSettings);
addHwPnoScanRequest的实现如下:
private boolean addHwPnoScanRequest(ClientInfo ci, int handler, ScanSettings scanSettings,
PnoSettings pnoSettings) {
// ... ...
WifiNative.PnoSettings nativePnoSettings =
convertSettingsToPnoNative(scanSettings, pnoSettings);
if (!mScannerImplsTracker.setHwPnoList(nativePnoSettings)) {
return false;
}
logScanRequest("addHwPnoScanRequest", ci, handler, null, scanSettings, pnoSettings);
addPnoScanRequest(ci, handler, scanSettings, pnoSettings);
return true;
}
首先将scanSettings和pnoSettings转化成nativePnoSettings;接着调用mScannerImplsTracker.setHwPnoList往下层继续调用;最后记录pno扫描请求。
setHwPnoList
的实现如下
public boolean setHwPnoList(WifiNative.PnoSettings pnoSettings) {
mStatusPerImpl.clear();
boolean anySuccess = false;
// 遍历所有的scannerimpl(每个interface对应一个)
for (Map.Entry<String, WifiScannerImpl> entry : mScannerImpls.entrySet()) {
String ifaceName = entry.getKey();
WifiScannerImpl impl = entry.getValue();
boolean success = impl.setHwPnoList(
pnoSettings, new PnoEventHandler(ifaceName));
if (!success) {
Log.e(TAG, "Failed to start pno on " + ifaceName);
continue;
}
mStatusPerImpl.put(ifaceName, STATUS_PENDING);
anySuccess = true;
}
return anySuccess;
}
mScannerImpls中的WifiScannerImpl都是在WifiScanningServiceImpl 的 setupScannerImpls()中创建的,每个iface对应一个
private void setupScannerImpls() {
// ... ...
Set<String> ifaceNamesOfImplsToSetup = new ArraySet<>(ifaceNames);
ifaceNamesOfImplsToSetup.removeAll(ifaceNamesOfImplsAlreadySetup);
// ... ...
for (String ifaceName : ifaceNamesOfImplsToSetup) {
WifiScannerImpl impl = mScannerImplFactory.create(mContext, mLooper, mClock, ifaceName);
// If this new scanner impl does not offer any new bands to scan, then we should
// ignore it.
if (!doesAnyExistingImplSatisfy(impl)) {
mScannerImpls.put(ifaceName, impl);
}
// ... ...
}
}
WifiScannerImpl的创建由WifiScannerImpl.DEFAULT_FACTORY负责:
public static final WifiScannerImplFactory DEFAULT_FACTORY = new WifiScannerImplFactory() {
public WifiScannerImpl create(Context context, Looper looper, Clock clock,
@NonNull String ifaceName) {
WifiNative wifiNative = WifiInjector.getInstance().getWifiNative();
WifiMonitor wifiMonitor = WifiInjector.getInstance().getWifiMonitor();
if (TextUtils.isEmpty(ifaceName)) {
return null;
}
// Background Scan
if (wifiNative.getBgScanCapabilities(
ifaceName, new WifiNative.ScanCapabilities())) {
return new HalWifiScannerImpl(context, ifaceName, wifiNative, wifiMonitor,
looper, clock);
} else {
return new WificondScannerImpl(context, ifaceName, wifiNative, wifiMonitor,
new WificondChannelHelper(wifiNative), looper, clock);
}
}
};
如果底层支持getBgScanCapabilities,那么就返回HalWifiScannerImpl实例,否则,返回WificondScannerImpl实例。但其实对于singlescan和pnoscan,都还是调用的WificondScannerImpl的接口,只有BatchedScan才会通过HAL去请求。
所以,对于PNO扫描,我们继续分析WificondScannerImpl
// packages/modules/Wifi/service/java/com/android/server/wifi/scanner/WificondScannerImpl.java
public boolean setHwPnoList(WifiNative.PnoSettings settings,
WifiNative.PnoEventHandler eventHandler) {
synchronized (mSettingsLock) {
if (mLastPnoScanSettings != null) {
Log.w(TAG, "Already running a PNO scan");
return false;
}
if (!isHwPnoScanRequired(settings.isConnected)) {
return false;
}
mLastPnoScanSettings = new LastPnoScanSettings(
mClock.getElapsedSinceBootNanos(),
settings.networkList, eventHandler);
if (!startHwPnoScan(settings)) {
Log.e(TAG, "Failed to start PNO scan");
reportPnoScanFailure();
}
return true;
}
}
-
PNO扫描只能发起一次,如果已经存在,那么不能重复发起,必须先stop
-
startHwPnoScan(settings)
调用
WifiNative
的startPnoScan()
方法
public boolean startPnoScan(@NonNull String ifaceName, PnoSettings pnoSettings) {
return mWifiCondManager.startPnoScan(ifaceName, pnoSettings.toNativePnoSettings(),
Runnable::run,
new WifiNl80211Manager.PnoScanRequestCallback() {
@Override
public void onPnoRequestSucceeded() {
// ... ...
}
@Override
public void onPnoRequestFailed() {
// ... ...
}
});
}
这里又回到了熟悉的WifiNl80211Manager,在之前WiFi打开的流程中我们也介绍过。它负责与wificond交互,而wificond负责通过nl80211接口与驱动交互。
public boolean startPnoScan(@NonNull String ifaceName, @NonNull PnoSettings pnoSettings,
@NonNull @CallbackExecutor Executor executor,
@NonNull PnoScanRequestCallback callback) {
IWifiScannerImpl scannerImpl = getScannerImpl(ifaceName);
// ... ...
try {
boolean success = scannerImpl.startPnoScan(pnoSettings);
if (success) {
executor.execute(callback::onPnoRequestSucceeded);
} else {
executor.execute(callback::onPnoRequestFailed);
}
return success;
} catch (RemoteException e1) {
Log.e(TAG, "Failed to start pno scan due to remote exception");
}
return false;
}
下面就交给wificond去处理了
pno scan的wificond的入口在文件system/connectivity/wificond/scanning/scanner_impl.cpp
中
// system/connectivity/wificond/scanning/scanner_impl.cpp
Status ScannerImpl::startPnoScan(const PnoSettings& pno_settings,
bool* out_success) {
pno_settings_ = pno_settings;
LOG(VERBOSE) << "startPnoScan";
*out_success = StartPnoScanDefault(pno_settings);
return Status::ok();
}
StartPnoScanDefault()方法理解起来比较容易:首先是参数转换,然后添加一些必要的参数,最后交给scan_utils_->StartScheduledScan处理
bool ScannerImpl::StartPnoScanDefault(const PnoSettings& pno_settings) {
// ... ...
// 解析扫描参数
ParsePnoSettings(pno_settings, &scan_ssids, &match_ssids, &freqs, &unused);
// 随机MAC地址
bool request_random_mac = wiphy_features_.supports_random_mac_sched_scan && !client_interface_->IsAssociated();
// 低功耗模式,如果驱动支持,那么就是必选的
bool request_low_power = wiphy_features_.supports_low_power_oneshot_scan;
bool request_sched_scan_relative_rssi = wiphy_features_.supports_ext_sched_scan_relative_rssi;
// 启动扫描
struct SchedScanReqFlags req_flags = {};
req_flags.request_random_mac = request_random_mac;
req_flags.request_low_power = request_low_power;
req_flags.request_sched_scan_relative_rssi = request_sched_scan_relative_rssi;
scan_utils_->StartScheduledScan(interface_index_,
GenerateIntervalSetting(pno_settings),
pno_settings.min_2g_rssi_,
pno_settings.min_5g_rssi_,
pno_settings.min_6g_rssi_,
req_flags,
scan_ssids,
match_ssids,
freqs,
&error_code)
}
StartScheduledScan 通过NL80211接口向驱动发起周期性扫描接口。简要介绍一下设置的参数(SchedScanIntervalSetting)
Scheduled Scan 是一种机制,允许Wi-Fi设备按计划定期执行扫描操作,以搜索附近的Wi-Fi网络
Scheduled Scan 是一种机制,允许Wi-Fi设备按计划定期执行扫描操作,以搜索附近的Wi-Fi网络
- interface_index
interface编号- SchedScanIntervalSetting
- ScanPlan
- interval_ms
扫描间隔时间- n_iterations
扫描次数- plans
ScanPlan的集合,依次执行。- final_interval_ms
当所有plans结束后,以final_interval_ms的频率继续扫描,默认为0(不扫描)- rssi_threshold_2g
2G网络的RSSI阈值,低于rssi_threshold_2g的2.4G网络不会上报- rssi_threshold_5g
与rssi_threshold_2g类似- rssi_threshold_6g
与rssi_threshold_2g类似- SchedScanReqFlags
计划扫描/周期扫描(SchedScan)参数
- request_random_mac
如果是true,要求设备在扫描时使用随机MAC地址。- request_low_power
低功耗扫描。PNO扫描默认设置为true。- request_sched_scan_relative_rssi
该参数用于获取与当前已连接BSS相比具有更好信号强度(RSSI)的BSS。
假设已连接到A(rssi=-60),但有其他网络B(-50)/C(-59)/D(-70)在附近;此时如果设置了NL80211_ATTR_SCHED_SCAN_RSSI_ADJUST后,并且delta=3,那么只会扫描到B,C和D 不会包含在扫描结果里。
pno扫描默认设置为true。前提是驱动要支持>supports_ext_sched_scan_relative_rssi
.
android只设置了2.4G,5G可能驱动有默认值。if (req_flags.request_sched_scan_relative_rssi) { struct nl80211_bss_select_rssi_adjust rssi_adjust; rssi_adjust.band = NL80211_BAND_2GHZ; // RSSI 差值 rssi_adjust.delta = static_cast<int8_t>(rssi_threshold_2g - rssi_threshold_5g); NL80211Attr<vector<uint8_t>> rssi_adjust_attr( NL80211_ATTR_SCHED_SCAN_RSSI_ADJUST, vector<uint8_t>( reinterpret_cast<uint8_t*>(&rssi_adjust), reinterpret_cast<uint8_t*>(&rssi_adjust) + sizeof(rssi_adjust))); start_sched_scan.AddAttribute(rssi_adjust_attr); }
- scan_ssids
可以设置扫描的ssid。扫描时设置ssid主要是为了扫描隐藏热点。
只有在network是hidden时,才会将其ssid加入到集合里面。
一般情况下,隐藏的无线访问点(AP)在受到probe request时,有两种选择:要么忽略不响应,要么响应但是不广播自己的ssid。
如果我们设置了scan_ssids,那么设备发送probe request帧时,会附带上ssid,以请求符合该ssid的网络信息;AP收到后,会比较帧内的ssid列表,如果跟自己的ssid相符,那么就会响应probe response,并且带上自己的网络信息(ssid等)。- match_ssids
只扫描列表中的网络。NL80211_ATTR_SCHED_SCAN_MATCH- freqs
扫描哪些信道,对于PNO扫描,通常只会在少数的几个信道上扫描。如果是singlescan,那么是全信道扫描。
到这里,PNO扫描相关的参数已经设置到驱动了。那么扫描结果是如何上报的呢,我们从下往上看
2. 扫描结果上报
wificond的ScannerImpl
在构造时,会注册两个listener: OnScanResultsReady
和 OnSchedScanResultsReady
// system/connectivity/wificond/scanning/scanner_impl.cpp
ScannerImpl::ScannerImpl(uint32_t interface_index,
const ScanCapabilities& scan_capabilities,
const WiphyFeatures& wiphy_features,
ClientInterfaceImpl* client_interface,
ScanUtils* scan_utils) {
// ... ...
scan_utils_->SubscribeScanResultNotification(
interface_index_,
std::bind(&ScannerImpl::OnScanResultsReady, this, _1, _2, _3, _4));
// Subscribe scheduled scan result notification from kernel.
scan_utils_->SubscribeSchedScanResultNotification(
interface_index_,
std::bind(&ScannerImpl::OnSchedScanResultsReady,
this,
_1, _2));
}
// system/connectivity/wificond/scanning/scan_utils.cpp
void ScanUtils::SubscribeScanResultNotification(
uint32_t interface_index,
OnScanResultsReadyHandler handler) {
netlink_manager_->SubscribeScanResultNotification(interface_index, handler);
}
// system/connectivity/wificond/scanning/scan_utils.cpp
void ScanUtils::SubscribeSchedScanResultNotification(
uint32_t interface_index,
OnSchedScanResultsReadyHandler handler) {
netlink_manager_->SubscribeSchedScanResultNotification(interface_index,
handler);
}
向netlink_manager注册了两个handler,分别是 扫描 和 计划扫描。其实PNO扫描和普通扫描流程是一样的。
netlink_manager.cpp中的ReceivePacketAndRunHandler()
方法阻塞读取netlink消息并解些
// system/connectivity/wificond/net/netlink_manager.cpp
void NetlinkManager::ReceivePacketAndRunHandler(int fd) {
ssize_t len = read(fd, ReceiveBuffer, kReceiveBufferSize);
// ... ...
uint8_t* ptr = ReceiveBuffer;
while (ptr < ReceiveBuffer + len) {
// ... ...
// Handle multicasts.
if (sequence_number == kBroadcastSequenceNumber) {
BroadcastHandler(std::move(packet));
continue;
}
// ... ...
}
}
void NetlinkManager::BroadcastHandler(unique_ptr<const NL80211Packet> packet) {
uint32_t command = packet->GetCommand();
if (command == NL80211_CMD_NEW_SCAN_RESULTS ||
// Scan was aborted, for unspecified reasons.partial scan results may be
// available.
command == NL80211_CMD_SCAN_ABORTED) {
OnScanResultsReady(std::move(packet));
return;
}
if (command == NL80211_CMD_SCHED_SCAN_RESULTS ||
command == NL80211_CMD_SCHED_SCAN_STOPPED) {
// 计划扫描结果
OnSchedScanResultsReady(std::move(packet));
return;
}
// ... ...
}
接着就调用回到了ScannerImpl了
void ScannerImpl::OnSchedScanResultsReady(uint32_t interface_index,
bool scan_stopped) {
if (pno_scan_event_handler_ != nullptr) {
if (scan_stopped) {
// If |pno_scan_started_| is false.
// This stop notification might result from our own request.
// See the document for NL80211_CMD_SCHED_SCAN_STOPPED in nl80211.h.
if (pno_scan_started_) {
LOG(WARNING) << "Unexpected pno scan stopped event";
pno_scan_event_handler_->OnPnoScanFailed();
}
pno_scan_started_ = false;
} else {
LOG(INFO) << "Pno scan result ready event";
pno_scan_event_handler_->OnPnoNetworkFound();
}
}
}
继续回到java层
在wifi打开时,setupInterfaceForClientMode
方法会向wificond注册scanresults的回调函数
// frameworks/base/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java
public boolean setupInterfaceForClientMode(/*.. ...*/) {
// ... ...
PnoScanEventHandler pnoScanEventHandler = new PnoScanEventHandler(executor,
pnoScanCallback);
mPnoScanEventHandlers.put(ifaceName, pnoScanEventHandler);
wificondScanner.subscribePnoScanEvents(pnoScanEventHandler);
}
PnoScanEventHandler
收到回调后,继续往上传
private class PnoScanEventHandler extends IPnoScanEvent.Stub {
@Override
public void OnPnoNetworkFound() {
Log.d(TAG, "Pno scan result event");
final long token = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> mCallback.onScanResultReady());
} finally {
Binder.restoreCallingIdentity(token);
}
}
}
到了WifiNative,现看WifiNative是什么时候注册的
// packages/modules/Wifi/service/java/com/android/server/wifi/WifiNative.java
public String setupInterfaceForClientInConnectivityMode(/* ... ... */) {
mWifiCondManager.setupInterfaceForClientMode(iface.name, Runnable::run,
new NormalScanEventCallback(iface.name),
new PnoScanEventCallback(iface.name))
}
private class PnoScanEventCallback implements WifiNl80211Manager.ScanEventCallback {
public void onScanResultReady() {
Log.d(TAG, "Pno scan result event");
mWifiMonitor.broadcastPnoScanResultEvent(mIfaceName);
mWifiMetrics.incrementPnoFoundNetworkEventCount();
}
}
WifiMonitor
public void broadcastPnoScanResultEvent(String iface) {
sendMessage(iface, PNO_SCAN_RESULTS_EVENT);
}
WifiCondScannerImpl进行处理,它再通过WifiNative -> WifiNl80211Manager -> wificond 从驱动获取扫描结果。
public boolean handleMessage(Message msg) {
case WifiMonitor.PNO_SCAN_RESULTS_EVENT:
pollLatestScanDataForPno();
break;
}
private void pollLatestScanDataForPno() {
synchronized (mSettingsLock) {
if (mLastPnoScanSettings == null) {
// got a scan before we started scanning or after scan was canceled
return;
}
mNativePnoScanResults = mWifiNative.getPnoScanResults(getIfaceName());
List<ScanResult> hwPnoScanResults = new ArrayList<>();
int numFilteredScanResults = 0;
for (int i = 0; i < mNativePnoScanResults.size(); ++i) {
ScanResult result = mNativePnoScanResults.get(i).getScanResult();
// nanoseconds -> microseconds
if (result.timestamp >= mLastPnoScanSettings.startTimeNanos / 1_000) {
hwPnoScanResults.add(result);
} else {
numFilteredScanResults++;
}
}
if (numFilteredScanResults != 0) {
Log.d(TAG, "Filtering out " + numFilteredScanResults + " pno scan results.");
}
if (mLastPnoScanSettings.pnoScanEventHandler != null) {
ScanResult[] pnoScanResultsArray =
hwPnoScanResults.toArray(new ScanResult[hwPnoScanResults.size()]);
mLastPnoScanSettings.pnoScanEventHandler.onPnoNetworkFound(pnoScanResultsArray);
}
}
}
从驱动获取到扫描结果列表后,回调给WifiScanningServiceImpl
的pnoScanEventHandler
,回调方法为onPnoNetworkFound
.
WifiScanningServiceImpl
再发消息CMD_PNO_NETWORK_FOUND
给WifiScanner
WifiScanner收到后,将把结果传递给WifiConnectivityManager
case CMD_PNO_NETWORK_FOUND: {
PnoScanListener pnoScanListener = (PnoScanListener) listener;
ParcelableScanResults parcelableScanResults = (ParcelableScanResults) msg.obj;
Binder.clearCallingIdentity();
executor.execute(() ->
pnoScanListener.onPnoNetworkFound(parcelableScanResults.getResults()));
} break;
最终WifiConnectivityManager拿着扫描结果去选择合适的网络进行连接。
下面是我手机上的一段日志,可以对理解流程有一定帮助
09-18 15:56:12.659 1296 1296 V wificond: startPnoScan
09-18 15:56:12.661 1296 1296 I wificond: Pno scan started for frequencies: 2412, 2437, 2462, 5180, 5220, 5260, 5300, 5745, 5785,
09-18 15:56:13.355 1296 1296 I wificond: Pno scan result ready event
09-18 15:56:13.356 1259 2580 D WifiNl80211Manager: Pno scan result event
09-18 15:56:13.356 1259 2580 D WifiNative: Pno scan result event
09-18 15:56:13.377 1259 2250 V WifiConnectivityManager: PnoScanListener onResults: start network selection
09-18 15:56:13.410 1259 2250 V WifiConnectivityManager: PnoScanListener: WNS candidate-"mobile"
3. 总结
PNO扫描由WifiConnectivityManager负责发起(Disconnected并且Screen OFF),将请求传递给wificond
,wificond
通过netlink设置相关扫描参数后请求驱动进行周期扫描(ScheduledScan)。驱动扫描结束后,现上报扫描结束事件NL80211_CMD_SCHED_SCAN_RESULTS
,Framework层的WificondScannerImpl
收到通知后再主动通过wificond
从驱动获取扫描结果列表并继续上报,直到WifiConnectivityManager
收到扫描结果的回调并处理。
下面是一个完整的流程图: 包括PNO扫描的发起,回调函数的注册,扫描结果的上报。