夜雨聆风学习资料网

ARTICLE · 1351446

Android源码第十二章:Wi-Fi——扫描结果怎样回到应用

Android源码第十二章:Wi-Fi——扫描结果怎样回到应用

点了扫描,startScan() 已经返回 true,为什么热点列表还没刷新?这时 App 只完成了请求提交。系统更新全频段扫描结果后会发通知,App 再调用 getScanResults() 读取共享缓存。 收到通知却没看到新条目时,可以按 12.3 查授权和条目的观测时间。

找到热点后还要建立连接、取得 IP,再看互联网验证。这个顺序可以用来区分“列表没更新”和“连上 Wi-Fi 但上不了网”两类问题。

文章目录

• 12.1 用 WifiManager 提交扫描并读取结果

• 12.2 分清扫描、连接和数据

• 12.3 在 Android Studio 模拟器上观察扫描

• 12.4 总图:请求下行,通知与数据分两趟回来

• 12.5 回看扫描服务和接口对象怎样建立

• 12.6 入口如何变成一个待处理扫描

• 12.7 从扫描器到内核:数据逐次换载体

• 12.8 cfg80211 如何把请求交给选定的 hwsim

• 12.9 回来的是通知,结果还要另取

• 12.10 失败与资源收尾要按层看

• 12.11 从“看见热点”接到“建立连接”

• 12.12 回看、排障与自测

• 参考资料

12.1 用 WifiManager 提交扫描并读取结果

假设你正在做一个诊断页,用户点「扫描附近 Wi-Fi」后,页面要显示系统缓存里有多少条结果。Activity 先通过 getApplicationContext().getSystemService(WifiManager.class) 取得公开 SDK 入口。WifiManager 把请求交给系统 Wi-Fi 服务,App 通过同一个管理对象登记通知和读取缓存。

本例在前台运行,使用 Android 11(API 30)及以上的 ScanResultsCallback。Manifest 声明 ACCESS_WIFI_STATE、CHANGE_WIFI_STATE、ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION。尚未获精确位置授权时,点击按钮会同时申请精确和粗略位置权限,确认精确位置已获授权、系统定位服务及 Wi-Fi 已开启后,再执行下面的代码。

    if (observer == null) {        WifiManager.ScanResultsCallback callback = new WifiManager.ScanResultsCallback() {            @Override            public void onScanResultsAvailable() {                if (observer == this) readResults();            }        };        wifi.registerScanResultsCallback(getMainExecutor(), callback);        observer = callback;    }    requestedAtUs = SystemClock.elapsedRealtimeNanos() / 1_000;    boolean accepted = wifi.startScan();

registerScanResultsCallback() 指定主线程 Executor,startScan() 返回的是请求是否受理。 收到 onScanResultsAvailable() 时,readResults() 调用 wifi.getScanResults() 读取系统共享缓存。App 退到后台时,在 onStop() 中调用 wifi.unregisterScanResultsCallback(callback)。完整示例还会先清除本地登记,并在回调开始时核对回调对象,防止已排队的旧通知被下一次观察消费。

startScan() 已标记为废弃,本例用它演示主动触发。只需观察平台或其他应用完成的全频段扫描时,可以登记通知、按需读取结果。需要区分扫描成功或失败时,另行监听 SCAN_RESULTS_AVAILABLE_ACTION,读取 EXTRA_RESULTS_UPDATED,并在观察结束时注销 Receiver。它们都可能反映平台发起的扫描。位置授权细节及扫描限制见文末官方资料。

12.2 分清扫描、连接和数据

扫描回答“附近有哪些基本服务集”。一个 BSS 可粗略理解为一个接入点提供的无线服务,BSSID 通常是其标识地址;SSID 是网络名称。相同 SSID 可以有多个 BSSID。ScanResult 包含名称、BSSID、频率、安全能力、信号和观测时间,不包含“已经完成认证”的保证。

连接需要选择网络配置、认证、关联及按安全类型完成密钥协商。wpa_supplicant 负责相关控制与安全状态;获得二层连接后,还需通过 IpClient 配置 IP、路由和 DNS。网络是否能访问互联网由 Connectivity 的网络验证另行判断。

名称
本章职责
一个容易混淆的点
WifiManager
应用入口,提交扫描、登记结果通知、读取结果
boolean 不是扫描结果列表
WifiServiceImpl / ScanRequestProxy
权限、节流、调度入口与应用可见缓存
缓存可以由别的请求更新
WifiScanningServiceImpl
合并请求、状态机、多接口扫描与监听者分发
多个请求不一定对应多次物理扫描
WifiNative / Nl80211Native
组织接口操作,选择 nl80211 实现
类名 Native 不意味着每步都有 JNI
wificond
一条实现中的独立 Native 进程,Binder 接口转 nl80211
当前版本也有绕过它的直接实现
Supplicant HAL / wpa_supplicant
网络配置、认证和连接状态
与扫描路径分工,不能固定串在扫描后面
Vendor Wifi HAL
接口/芯片能力、厂商功能及支持时的批扫描
单次扫描并非必经其扫描接口
nl80211 / cfg80211
用户空间无线协议 / 内核无线配置框架
前者是命令协议,后者管理请求及 BSS
mac80211 / Driver
SoftMAC 公共 MAC 实现 / 具体设备或模拟实现
FullMAC 不要求经过 mac80211

nl80211 承载扫描等控制请求。联网后的 TCP/IP 数据走第十一章讲过的 socket/netdev 路径。

12.3 在 Android Studio 模拟器上观察扫描

1. 用 Android Studio 新建 Empty Views Activity 项目,语言选 Java,包名用 com.example.wifiscandemo,Minimum SDK 选 API 30。保留模板生成的 Gradle 配置,确认 app 模块的 compileSdk 不低于 30,并已安装该版本的 SDK Platform。把文末可复制的 Wi-Fi 扫描示例中的两个文件覆盖到对应路径,再同步 Gradle。

2. 在 Device Manager 创建一台 Phone AVD,例如 Pixel 设备配置加 Android 14(API 34)Google APIs 镜像。按电脑架构选镜像。启动后打开系统设置里的 Wi-Fi 和位置服务。在工具栏选中该 AVD,点击 Run,由 Android Studio 构建、安装并启动 App。

3. 打开 Android Studio 的 Logcat,选中同一台 AVD,并用 package:com.example.wifiscandemo tag:WifiChapter12 过滤。点「扫描附近 Wi-Fi」。首次尚未授权时,先允许在使用应用时访问位置,Android 12 及以上还要选「精确」,再点一次按钮发起扫描。已获精确位置授权时,首次点击即提交。只给了大致位置或系统不再弹窗时,到系统的应用权限设置调整后重试。让 App 保持前台,先观察一轮再决定是否重试。

一次正常观察可能先出现 REQUEST accepted=true,随后出现 CALLBACK received 和 RESULT count=…。有缓存条目时会另有 ENTRY。结果如下表解读,日志名均来自附录代码。

观察
能判断什么
REQUEST accepted=true
调用通过入口并提交给扫描调度。驱动是否已经开始,需要后续证据。
REQUEST accepted=false
 或 REQUEST_DENIED
这次调用未确认受理。先看授权、Wi-Fi、设备状态或节流,失败原因见 12.10。已登记的监听还可能收到其他来源的通知。
CALLBACK received
 与 RESULT count=0
App 收到了通知,本次读取没有返回条目。授权回退、服务等待超时和当前缓存为空都需结合系统状态判断。显式抛出权限异常时,示例记录 READ_DENIED。
ENTRY seenAtOrAfterRequest=false
条目观测时间早于最近一次按钮请求,可能来自旧缓存。为 true 时也只能判断时间先后,无法证明条目由本 App 的请求产生。
没有 CALLBACK received
记录「本轮未观察到回调」。先看 AVD 是否选对、日志过滤、App 是否留在前台,再检查系统状态。示例没有为回调设定完成时限。

ScanResult.timestamp 的 SDK 契约是开机以来的观测时间,单位为微秒。示例用同一时间基准计算条目年龄,只记频率和年龄,不输出 SSID 或 BSSID。每次实际调用 startScan() 前,App 都会更新本地时间基准。离开页面后若已登记监听,可看到 OBSERVER stopped。重新进入并点击按钮会新建监听,退出页面不会取消整个系统的扫描。

这些日志用于观察公开 API 的受理、通知和读取。模拟器是否产生新条目取决于镜像与虚拟无线环境。缺少新条目时可保留观察记录,或在允许调试的 Android 11 及以上真机重复步骤。要确认下文的 wificond、hwsim 或宿主无线后端,可从 12.10 的进阶验证开始,继续采集与目标分支匹配的证据。本章的底层解释以固定源码为依据。

12.4 总图:请求下行,通知与数据分两趟回来

先沿架构图找入口、扫描服务和内核之间的边界。两条 netlink 实现共用内核接口。本节的时序选择 wificond 分支,直接实现的区别见 12.7。

整体流程把请求受理、命令确认、驱动完成和应用读取拆开。先看失败停在哪一步,再决定该看权限、排队状态、驱动事件还是共享缓存。

时序里有两次取数。扫描服务先用 GET_SCAN 取内核 BSS 条目,Wifi 服务的 GlobalScanListener 接着更新共享缓存。应用收到全频段通知后,再用 getScanResults() 取自己能看到的列表。

主路径的次序是:

应用 WifiManager --Binder--> WifiServiceImpl  --WifiThreadRunner.call--> WifiHandlerThread / ScanRequestProxy  --本地 LocalService + post--> WifiScanningService 线程 / 状态机  --> WificondScannerImpl --> WifiNative --> Nl80211Native  ├─ useWificond() true:WifiNl80211Manager --Binder--> wificond  │   --> ScanUtils --nl80211--> Kernel  └─ false:Nl80211Utils / Nl80211Proxy --nl80211--> KernelKernel:nl80211 --> cfg80211 --> mac80211 --> 选定 hwsim Driver完成:Kernel 多播 --> 对应实现的 scan callback --> WifiMonitor 消息取数:WificondScannerImpl --> GET_SCAN --> BSS 列表 --> ScanData分发:扫描状态机 --> GlobalScanListener --> 缓存 --> App 通知读取:应用 getScanResults --Binder--> 缓存列表 --> 应用

相同进程内的 WifiScannerInternal 不额外跨 Binder;它把监听者包为 IWifiScannerListener.Stub 不等于发生远程事务。回调到 App 则是独立的反向 Binder 接口,不是原 startScan() 调用栈继续返回。

执行域
创建与线程来源
本章交接
应用
系统服务注册器提供 WifiManager,构造时保存 IWifiManager;示例指定 main executor
同步请求返回;Binder 回调再投递主线程
system_server Wifi 服务
WifiService 创建 WifiInjector/WifiServiceImpl;Injector 建立 WifiHandlerThread
Binder 入口借 WifiThreadRunner 同步取一次“受理结果”
system_server 扫描服务
WifiScanningService 创建独立 HandlerThread,构造 WifiScanningServiceImpl
LocalService.startScan 进入服务后 post,再投递状态机消息
wificond
main 创建 LooperBackedEventLoop、NetlinkManager、ScanUtils、Server
Binder polling 与 netlink FD 由该事件循环处理;不能套普通 Binder 多线程池叙述
Kernel
netlink 命令处理、wiphy work 与 hwsim delayed work
多播通知是另一事件;不要求与用户线程同时运行

12.5 回看扫描服务和接口对象怎样建立

12.4 的时序从 App 登记开始。这里回看系统怎样提前准备各个接收者,后续源码使用 12.7 列出的固定版本。

应用通过 getSystemService(WifiManager.class) 取得管理对象;WifiManager 保存的是 WifiService 的 Binder 代理。WifiService 在构造中创建 WifiInjector 和 WifiServiceImpl;Injector 创建长期使用的 WifiNative、Nl80211Native、ScanRequestProxy,后者保存缓存和 RemoteCallbackList。

扫描服务构造时创建自己的状态机与线程执行器,startService() 把 new LocalService() 登记到 WifiLocalServices。ScanRequestProxy.retrieveWifiScannerIfNecessary() 延迟取得它,并登记 GlobalScanListener,因为扫描服务启动较晚。

        if (mWifiScanner == null) {            mWifiScanner = WifiLocalServices.getService(WifiScannerInternal.class);            updateThrottleEnabled();            // Register the global scan listener.            if (mWifiScanner != null) {                mWifiScanner.registerScanListener(                        new WifiScannerInternal.ScanListener(new GlobalScanListener(),                                mWifiThreadRunner));            }        }        return mWifiScanner != null;

来源:Wifi,ScanRequestProxy.java,retrieveWifiScannerIfNecessary()。这一字段指向扫描服务的本地实现,GlobalScanListener 负责全部相关单次扫描结果,不仅是本 App 发出的那一轮。WifiScannerInternal.ScanListener 保存目标监听者与 WifiThreadRunner,其 onResults() 把 ScanData 投回 WifiHandlerThread。

主路径还需要接口已经准备好。WifiNative.setupInterfaceForClientInScanMode() 创建/登记接口状态,再调用 Nl80211Native.setupInterfaceForClientMode()。在 wificond 分支,WifiNl80211Manager 从服务管理器取得 wificond 并登记死亡监听,调用 createClientInterface(ifaceName)。

Native Server::createClientInterface() 用接口名查询 wiphy/ifindex,创建 ClientInterfaceImpl,保存进 client_interfaces_。ClientInterfaceImpl 创建 ClientInterfaceBinder 和 ScannerImpl;ScannerImpl 构造时向 ScanUtils 订阅 ifindex 对应的扫描事件。

  LOG(INFO) << "create scanner for interface with index: "            << (int)interface_index_;  scanner_ = new ScannerImpl(interface_index_,                             scan_capabilities_,                             wiphy_features_,                             this,                             scan_utils_);

来源:wificond,client_interface_impl.cpp,ClientInterfaceImpl 构造函数。scanner_ 与客户端接口绑定,ifindex 识别内核接口;不是每调用一次 startScan 就新建一个 ScannerImpl。 Java 侧取得 getWifiScannerImpl(),把代理按 ifaceName 存入 map,同时调用 subscribeScanEvents() 登记自己的 ScanEventHandler。接口名用于用户层查找,内核消息用 ifindex。

Vendor HAL 可以参与建立接口、能力查询;scan-only 模式不要求启动 Supplicant。WifiScannerImpl.DEFAULT_FACTORY 按背景扫描能力选 HalWifiScannerImpl 或 WificondScannerImpl。即使选到前者,其 startSingleScan() 仍调用内部 WificondScannerImpl;它的 startBatchedScan() 才委托 WifiNative.startBgScan()。不能凭类名把单次扫描改画成 gscan。

12.6 入口如何变成一个待处理扫描

WifiManager.startScan() 委托带 WorkSource 的重载,实际发送包名与 attributionTag:

    public boolean startScan(WorkSource workSource) {        try {            String packageName = mContext.getOpPackageName();            String attributionTag = mContext.getAttributionTag();            return mService.startScan(packageName, attributionTag);        } catch (RemoteException e) {            throw e.rethrowFromSystemServer();        }    }

来源:Wifi,framework/java/android/net/wifi/WifiManager.java,startScan(WorkSource)。同步 Binder 返回的 boolean 描述这次提交,不包含 ScanResult。 RemoteException 会转为系统服务异常;普通失败还可能由服务返回 false。

WifiServiceImpl 先校验修改 Wifi 权限、包名归属、idle 状态和访问扫描结果的权限。当前内部位置校验抛 SecurityException 时,startScan() 捕获后返回 false;入口外的某些权限校验仍可能抛异常。getScanResults() 也有独立权限检查,不要把官方通用异常说明当成每个实现分支的唯一返回形式。

            Boolean scanSuccess = mWifiThreadRunner.call(() ->                    mScanRequestProxy.startScan(callingUid, packageName), null,                    TAG + "#startScan");            if (scanSuccess == null) {                sendFailedScanBroadcast();                return false;            }

来源:Wifi,WifiServiceImpl.java,startScan(String,String)。mWifiThreadRunner.call() 把工作交到 WifiHandlerThread 并等待这一次结果;等待超时得到 null 时返回 false。 该 false 不保证已投递任务没有后续动作,不能把超时当作业务撤销。

ScanRequestProxy 检查扫描可用性和节流。普通前台 App 的阈值为 120 秒内 4 次,后台 App 共同使用 30 分钟间隔;有系统权限的请求、例外列表及关闭节流等分支另处理。它创建 WorkSource(uid,packageName) 表示耗电归因,并组织频段、报告标志、隐藏 SSID 等参数。

        mWifiScanner.startScan(settings,                new WifiScannerInternal.ScanListener(new ScanRequestProxyScanListener(),                        mWifiThreadRunner),                workSource);        return true;

来源:Wifi,ScanRequestProxy.java,startScan(int,String)。这里调用的 startScan 返回 void,随后立即 return true;它无法证明稍后的扫描器调用或 Driver 请求成功。ScanRequestProxyScanListener.onSuccess() 的注释也要求继续等结果。

LocalService.startScan 把 settings/listener/workSource 传给 WifiScanningServiceImpl;服务权限处理后 post 到自身 runner,创建/复用 ClientInfo,发送 CMD_START_SINGLE_SCAN。状态机校验请求,可能把可覆盖的新请求加入正在进行的扫描,或放入 pending;tryToStartNewScan() 合并频段、隐藏网络等为 WifiNative.ScanSettings,交各接口扫描器。至少一个接口受理后,状态机才把这一批 pending 移到 active。请求数、接口数与底层扫描次数不是一一对应。

沿图看入口提交返回时,扫描状态机仍可继续处理请求。

12.7 从扫描器到内核:数据逐次换载体

上一节合并的扫描设置先交给 WificondScannerImpl。这里沿 useWificond() == true 的单次扫描,看它怎样把设置转换成内核能识别的命令。完整的源码版本和实现选择见本节末。

扫描器把接口参数交给 nl80211

WificondScannerImpl 在扫描服务线程中,使用 settings 锁保护本地状态;同一实例已有 mLastScanSettings 时拒绝重叠单次扫描。开始时记录单调时钟和频段,保存扫描状态回调:

            mLastScanSettings = new LastScanSettings(                    mClock.getElapsedSinceBootNanos(),                    reportFullResults, allFreqs, eventHandler);            int scanStatus = WifiScanner.REASON_UNSPECIFIED;            Set<Integer> freqs = Collections.emptySet();            if (!allFreqs.isEmpty()) {                freqs = allFreqs.getScanFreqs();                scanStatus = mWifiNative.scan(                        getIfaceName(), settings.scanType, freqs, hiddenNetworkSSIDSet,                        settings.enable6GhzRnr, settings.vendorIes);

来源:Wifi,scanner/WificondScannerImpl.java,startSingleScan()。LastScanSettings 留住本轮起点、频率与回调,下一次完成/失败依此归属。WifiNative.scan() 把隐藏 SSID 解码成字节数组,放入额外的 6 GHz/vendor 参数;Android U 及以上走返回状态码的 Nl80211Native.startScan(),旧分支用 startScanPreU() 再转码。

        if (useWificond()) {            return mWificondManager.startScan2(                    ifaceName, scanType, freqs, hiddenNetworkSSIDs, extraScanningParams);        }

来源:Wifi,nl80211/Nl80211Native.java,startScan()。只有 useWificond() 为 true 才进入下一段 Binder 路径。 false 分支由 Java 实现组织 ifindex、扫描标志、通配 SSID/隐藏 SSID、频率,然后调用 Nl80211Utils.triggerScan。Nl80211Proxy 使用 AF_NETLINK/NETLINK_GENERIC socket 发请求;JNI/系统调用包装不等于另有一个固定 Wifi JNI 中介。

主情景的 WifiNl80211Manager 用 ifaceName 查找 IWifiScannerImpl,构造 SingleScanSettings,把频率包装为 ChannelSettings、隐藏网络包装为 HiddenNetwork;startScan2() 调用 AIDL scanRequest(settings) 并转换状态码。不要误摘历史 startScan() 中返回 boolean 的 scannerImpl.scan() 当当前 U+ 路径。

Native ScannerImpl::scanRequest 检查对象有效性、能力和随机 MAC 条件,准备空 SSID(通配扫描)、隐藏 SSID 和频率列表,调用 ScanUtils::Scan。它用 status 返回扫描请求状态,Binder Status::ok() 只说明事务处理本身成功;两者不是同一成功。

  NL80211Packet trigger_scan(      netlink_manager_->GetFamilyId(),      NL80211_CMD_TRIGGER_SCAN,      netlink_manager_->GetSequenceNumber(),      getpid());  // If we do not use NLM_F_ACK, we only receive a unicast repsonse  // when there is an error. If everything is good, scan results notification  // will only be sent through multicast.  // If NLM_F_ACK is set, there will always be an unicast repsonse, either an  // ERROR or an ACK message. The handler will always be called and removed by  // NetlinkManager.  trigger_scan.AddFlag(NLM_F_ACK);  NL80211Attr<uint32_t> if_index_attr(NL80211_ATTR_IFINDEX, interface_index);

来源:wificond,scanning/scan_utils.cpp,ScanUtils::Scan()。NL80211_CMD_TRIGGER_SCAN 携带 ifindex、SSID、频率和扫描标志,NLM_F_ACK 要求内核确认命令。SendMessageAndGetAckOrError() 等的是 ACK/错误,不是无线扫描结束。成功后 ScannerImpl 设置 scan_started_=true 并返回 SCAN_STATUS_SUCCESS。

迁移直接分支的 Nl80211Utils.triggerScan() 同样构造 TRIGGER_SCAN 和 NLM_F_ACK,再由 Nl80211Proxy 收应答;事件经 Nl80211BroadcastMonitor/Nl80211Native 回到 WifiMonitor,后半段结果处理与本章共同。它没有 wificond Binder 边和 ScannerImpl 对象,不可把两条线首尾串起来。

本章使用的源码版本与实现分支

下列提交用于复查调用关系。运行镜像、迁移开关和驱动配置需要在目标设备上核对。

项目
固定提交
本章使用范围
packages/modules/Wifi
67beff9acf0e91046d2a9a36a68cb06ef263d1f1
API、系统服务、扫描器、Nl80211Native、连接封装
frameworks/base
94b4c163b7dfe5ce3607f7bb8456f9573f7de57d
WifiNl80211Manager、客户端服务取得入口
system/connectivity/wificond
fefad78738af9d9753bb0acbafcf458691f8627c
ScannerImpl、ScanUtils、netlink、事件循环
external/wpa_supplicant_8
3396e8b8365817b3a24b78b534b04bb9dcea330a
AIDL StaNetwork、nl80211 Driver 接口
hardware/interfaces
0162af698935100a590b7359581ac8b1b80693e5
Wifi / Supplicant 的 AIDL 与历史 HIDL 契约
device/generic/goldfish
296e55aa0244e8929e393e00e34471fef2a5d662
Emulator 虚拟无线配置线索
kernel/common
ccafb60de224d35a8bc14de33e1cd47caaaa786b
本章下载并核对的 nl80211、cfg80211、mac80211、mac80211_hwsim

文末有固定的 Kernel 源码入口。本文取得了下文用到的源码文件,实际镜像的构建清单和补丁仍需在对应环境核验。

主阅读情景:useWificond() 为 true,单次扫描,未启用 mock/fake;底层选 mac80211_hwsim 的 use_chanctx=true、mlo=false 分支。 这是源码可读场景,不声称本机当前正使用此组合。已有第一章 Emulator 实测只能证明当时的镜像与 Kernel 信息,未证明本章 Wifi 分支、radio 参数或 host 无线后端。

WifiInjector 中迁移开关的真实条件是:

        boolean isWificondMigrationEnabled = Environment.isSdkAtLeastC()                && mFeatureFlags.wificondToNl80211Migration()                && mContext.getResources().getBoolean(R.bool.config_wificondMigrationEnabled);        // Force enable the wificond migration if wificond is already disabled.        if (!isWificondMigrationEnabled                && SystemProperties.get("init.svc.wificond").isEmpty()) {            Log.i(TAG, "Force enabling wificond migration due to wificond disabled.");            isWificondMigrationEnabled = true;        }

来源:Wifi 项目,service/java/com/android/server/wifi/WifiInjector.java,构造函数。SDK 条件、feature flag、资源开关共同选择迁移;若 init.svc.wificond 为空还会强制启用迁移。Nl80211Native 保存 useWificond = !isWificondMigrationEnabled,实际判断还要求 !mUseNl80211Override。不能仅凭设备版本或源码中存在 wificond 就认定使用它。

Goldfish 的 init/init.net.ranchu.sh 仅在 ro.boot.qemu.virtiowifi=1 且有 MAC 前缀时调用 mac80211_create_radios。这提供调查入口,未取得本轮属性、模块、radio 创建参数与 host 证据前,不能把下文 hwsim 分支标为当前运行实现。

12.8 cfg80211 如何把请求交给选定的 hwsim

固定 Kernel 的 net/wireless/nl80211.c::nl80211_trigger_scan() 解析属性、验证能力、监管域允许的信道和参数,为 cfg80211_scan_request 分配空间;已有 scan_req/scan_msg 时返回 -EBUSY。之后保存请求并启动:

rdev->scan_req = request; err = cfg80211_scan(rdev); if (err) goto out_free; nl80211_send_scan_start(rdev, wdev); dev_hold(wdev->netdev); return 0;

来源:Kernel,net/wireless/nl80211.c,nl80211_trigger_scan()。rdev->scan_req 持有当前请求;cfg80211_scan() 才进入所注册的无线操作。 失败会回到 out_free 清空指针并 kfree;成功时 dev_hold,稍后完成路径配对 dev_put。

cfg80211 是内核无线配置与 BSS 管理框架。cfg80211_scan() 在没有 WIPHY_FLAG_SPLIT_SCAN_6GHZ 时调用 rdev_scan;有分段扫描标志时可能拆分非 6 GHz/6 GHz 请求。rdev_scan() 调用 rdev->ops->scan(),对 SoftMAC/mac80211,注册表 mac80211_config_ops.scan = ieee80211_scan,后者检查接口模式并调用 ieee80211_request_scan → __ieee80211_start_scan。

FullMAC Driver 可以注册自己的 cfg80211_ops.scan,交设备固件处理 MAC 工作;本章选择 hwsim/SoftMAC,实际经过 mac80211。“支持 hw_scan 回调”和“FullMAC”是两个不同分类。 mac80211 也能把扫描具体执行委托给驱动的 hw_scan。

hwsim 在 mac80211_hwsim_new_radio() 根据参数选 ieee80211_ops:mlo 优先;否则 use_chanctx 为 true 选 mac80211_hwsim_mchan_ops(含 hw_scan),普通 mac80211_hwsim_ops 只有 sw_scan_start/sw_scan_complete。本文为了明确展示 Driver 的异步接收/完成,固定讨论 use_chanctx=true,mlo=false;这不是所有 hwsim 的通用路径。

mac80211 若选择 hw_scan,先准备 ieee80211_scan_request,drv_hw_scan() 调用所选 ops 的 hw_scan。在本情景进入:

mutex_lock(&hwsim->mutex); if (WARN_ON(hwsim->tmp_chan || hwsim->hw_scan_request)) { mutex_unlock(&hwsim->mutex); return -EBUSY; } hwsim->hw_scan_request = req; hwsim->hw_scan_vif = vif; hwsim->scan_chan_idx = 0;

来源:Kernel,drivers/net/wireless/virtual/mac80211_hwsim.c,mac80211_hwsim_hw_scan()。hwsim 保存请求与 vif,随后排 delayed work;返回 0 只代表安排执行。hw_scan_work() 按信道逐步处理,按条件构造 probe request 并交 mac80211_hwsim_tx_frame,再等待接收路径收集 beacon/probe response。

这里没有物理芯片固件。hwsim 帧发送进一步分支:有 wmediumd portid 或启用 VirtIO 时交 mac80211_hwsim_tx_frame_nl;否则使用内核中的模拟介质。使用外部模拟介质时,真正结果取决于相应后端;源码阅读不保证会找到热点。本文 Driver 终点已定位,host 预编译模拟器的精确无线实现未建立运行证据。不能把它改写成“射频芯片完成扫描”。

当本次下发给 hwsim 的子请求信道处理完,hwsim 调用 ieee80211_scan_completed,清空它保存的请求/vif/tmp_chan。它不一定代表顶层全频段请求结束:本提交 hwsim 未设置 SINGLE_SCAN_ON_ALL_BANDS,mac80211 可能继续下一个频段。普通 hwsim 软件扫描则由 mac80211 的扫描工作推进,并回调 sw_scan_complete,不能套用此处 hw_scan_work。

每个频段子请求结束后,mac80211 还要检查是否有下一批信道。

12.9 回来的是通知,结果还要另取

Driver 完成先回到 mac80211:

set_bit(SCAN_COMPLETED, &local->scanning); if (info->aborted) set_bit(SCAN_ABORTED, &local->scanning); memcpy(&local->scan_info, info, sizeof(*info)); wiphy_delayed_work_queue(local->hw.wiphy, &local->scan_work, 0);

来源:Kernel,net/mac80211/scan.c,ieee80211_scan_completed()。这一步设置完成/中止标志,再投递 scan_work;不是直接运行 Android 的回调。 工作处理后先进入 __ieee80211_scan_completed 的多频段判断:

if (hw_scan && !aborted &&    !ieee80211_hw_check(&local->hw, SINGLE_SCAN_ON_ALL_BANDS) &&    ieee80211_prep_hw_scan(scan_sdata)) { int rc; rc = drv_hw_scan(local, rcu_dereference_protected(local->scan_sdata,  lockdep_is_held(&local->hw.wiphy->mtx)), local->hw_scan_req); if (rc == 0) return;

来源:Kernel,net/mac80211/scan.c,__ieee80211_scan_completed()。还有频段时再次 drv_hw_scan,成功即 return;没有后续工作或中止/失败才进入最终清理与 cfg80211_scan_done。 这是 mac80211 按频段下发的机制,与 cfg80211 的 SPLIT_SCAN_6GHZ 分段分别判断。

cfg80211_scan_done 保存 info 并排 scan_done_wk;最终 ___cfg80211_scan_done 构造完成/中止消息,归还 netdev 引用,释放 int_scan_req/scan_req,再发送 nl80211 多播。BSS 条目来自接收路径:mac80211 的 ieee80211_bss_info_update 调用 cfg80211_inform_bss_frame_data;扫描完成消息本身不携带整个热点列表。

wificond 的 NetlinkManager 在启动时订阅 scan 组,收到 NEW_SCAN_RESULTS 或 SCAN_ABORTED 后按 ifindex 查已登记 handler。ScannerImpl::OnScanResultsReady 清掉 scan_started_,正常回调 OnScanResultReady,中止回调 OnScanRequestFailed(SCAN_STATUS_FAILED_ABORT)。

  if (scan_event_handler_ != nullptr) {    // TODO: Pass other parameters back once we find framework needs them.    if (aborted) {      LOG(WARNING) << "Scan aborted";      scan_event_handler_->OnScanRequestFailed(IWifiScannerImpl::SCAN_STATUS_FAILED_ABORT);    } else {      scan_event_handler_->OnScanResultReady();    }

来源:wificond,scanning/scanner_impl.cpp,OnScanResultsReady()。这里即使不是自己标记正在扫描,也可能处理外部扫描完成通知。 Java ScanEventHandler 的 Binder 接收方法按已登记 executor 调用 WifiNative.ScanEventHandler;主情景 setup 时传 Runnable::run,因此当下执行随 Binder 接收线程,随后 WifiMonitor 发送 Message,才在 WificondScannerImpl 的 Handler 处理。不要把所有回调写成 WifiHandlerThread 直接执行。

WificondScannerImpl 收 SCAN_RESULTS_EVENT 时先取消超时,再 pollLatestScanData,主动调用 WifiNative.getScanResults → Nl80211Native → WifiNl80211Manager → ScannerImpl::getScanResults → ScanUtils::GetScanResult。后者发 NL80211_CMD_GET_SCAN dump;内核 nl80211_dump_scan 遍历 BSS 缓存,返回逐条属性。wificond 的 dump 若失败会记录日志,Binder 事务仍可成功返回,因此空列表也需要检查取数阶段。

NativeScanResult 保存 SSID 字节、BSSID、频率、信号、信息元素及观测时间;Java 将其转换为 ScanDetail/ScanResult。pollLatestScanData() 做本轮筛选:

                ScanResult result = mNativeScanResults.get(i).getScanResult();                // nanoseconds -> microseconds                if (result.timestamp >= mLastScanSettings.startTimeNanos / 1_000) {                    // Allow even not explicitly requested 6Ghz results because they could be found                    // via 6Ghz RNR.                    if (mLastScanSettings.singleScanFreqs.containsChannel(                                    result.frequency) || ScanResult.is6GHz(result.frequency)) {                        singleScanResults.add(result);

来源:Wifi,scanner/WificondScannerImpl.java,pollLatestScanData()。先按扫描开始时间过滤旧条目,再按所请求信道或 6 GHz 结果条件过滤。 代码将纳秒起点除 1000 与微秒 timestamp 比较。wificond 优先使用 NL80211_BSS_LAST_SEEN_BOOTTIME,缺失时有 TSF 等兼容处理;不能无条件把所有旧驱动时间戳当成同一时钟的实测。

扫描器排序并创建 WifiScanner.ScanData,通知 onScanStatus(WIFI_SCAN_RESULTS_AVAILABLE),最后 mLastScanSettings=null。扫描服务汇合各接口状态,向 active 请求和全局监听者分发 ScanData;每个请求还按其 settings 过滤,完成后清理 active 列表。

GlobalScanListener 在 WifiHandlerThread 处理 ScanData:全频段结果先清 full/partial 缓存,更新 BSSID 对应条目;部分频段结果走合并缓存。只有判定为 full band 的这一分支才发送成功广播并通知应用 ScanResultsCallback。 不能把每一次 partial scan 都画成应用一定收到一次通知。这里的 full band 由 WifiScanner.isFullBandScan(bands, false) 判断,按兼容规则可忽略缺少 6 GHz 或 60 GHz 的情况。

应用反向 Binder callback 使用 oneway 接口,最终由 ScanResultsCallbackProxy 接收:

                synchronized (mLock) {                    executor = mExecutor;                    callback = mCallback;                }                if (callback == null || executor == null) {                    return;                }                Binder.clearCallingIdentity();                executor.execute(callback::onScanResultsAvailable);

来源:Wifi,WifiManager.java,ScanResultsCallbackProxy.onScanResultsAvailable()。proxy 保存的 executor 决定回调的消费线程;我们的 getMainExecutor() 让结果读取代码在主线程运行。 这次通知没有列表。App 再发 getScanResults() Binder 请求,WifiServiceImpl 从 ScanRequestProxy 取 full/partial 合并缓存,以 ParceledListSlice 返回,WifiManager 解出 List。

如果把列表附在通知里,订阅者能直接处理随通知到达的快照,但系统需要为每个接收者传递列表,还要在发送数据时处理相应的访问权限。现有接口允许 App 按需读取,WifiServiceImpl 在读取时再校验权限。App 需要列表时会再走一次 Binder,读取发生时共享缓存可能已被更新。对应到 12.3 的日志,CALLBACK 只能标出通知到达,缓存内容要看后面的 RESULT 与 ENTRY。

这张图把完成通知、内核 GET_SCAN 和应用读取缓存放在同一个画面。

12.10 失败与资源收尾要按层看

条件
真实处理与可观察边界
不应得出的结论
入口权限/idle/节流/服务线程超时
false 或部分入口异常;可能有失败广播
false 不能统一解释为射频失败
没有可用信道、请求错误/忙
WifiNative 状态码;扫描器 post reportScanFailure
startSingleScan 返回 true 仍可能只是异步报告失败
Kernel -EBUSY/-EINVAL
ACK/错误转为 SCAN_STATUS / WifiScanner REASON
Binder Status::ok 不等于请求成功
Driver 中止
SCAN_ABORTED → OnScanRequestFailed → WifiMonitor 失败事件
有旧缓存不意味着中止没发生
扫描未完成
WificondScannerImpl 15000 ms 定时器 → REASON_TIMEOUT
15 秒是本版本超时配置,不是扫描 SLA,也不是动画时长
wificond 累计 ENODEV
同一 ScannerImpl 初始化或上次成功发起单次/PNO扫描以来累计计数;其他失败不清零,第4次ENODEV触发 CHECK 并终止 wificond;后续恢复需观察服务重启
不应只摘日志而遗漏 fatal 终点
宿主/接口退出
Binder death、接口拆除、订阅注销、状态清理
Java callback 注销不等于取消 Kernel 扫描

WificondScannerImpl 正常结果/失败事件取消 Alarm;reportScanFailure 通知本轮 eventHandler 后置空 mLastScanSettings。cleanup 取消超时、报告当前失败、注销 WifiMonitor 监听并清理本地状态;不能仅因示例退出就声称它直接取消了所有系统扫描。

wificond Server 拆接口时 erase ClientInterfaceImpl;析构让 Binder 实现失效并调用 scanner_->Invalidate,后者退订单次/计划扫描结果并标 invalid。Java WifiNl80211Manager 也会退订 scan/pno 事件、tearDownClientInterface 并移除 map;wificond 死亡时清理代理状态,通知上层恢复。

应用 unregisterScanResultsCallback 经 WifiService 到 RemoteCallbackList.unregister;WifiManager 在 finally cleanUpProxy 清空 executor/callback。已经投递到应用 executor 的 Runnable 可能仍在队列里,所以示例在回调里核对当前登记对象,防止页面退出后仍消费已排队的旧通知。远端死亡由 RemoteCallbackList 处理,不是正常生命周期可省略注销的理由。

进阶验证:采集系统 Wi-Fi 状态与日志

需要进一步定位扫描服务或底层实现时,再采集系统属性和服务日志。以下命令用于已有访问权限的调试设备。先在电脑的终端 A 设置实际设备序列号和输出目录。

export ANDROID_SERIAL='替换为实际设备序列号'WIFI12_RUN_DIR=$(mktemp -d "${TMPDIR:-/tmp}/wifi12-observation.XXXXXXXX") && (    set -e    printf '%s\n' "$WIFI12_RUN_DIR"    adb shell getprop ro.build.fingerprint > "$WIFI12_RUN_DIR/fingerprint.txt"    adb shell uname -a > "$WIFI12_RUN_DIR/kernel.txt"    adb shell dumpsys wifi > "$WIFI12_RUN_DIR/wifi-before.txt"    adb shell getprop init.svc.wificond > "$WIFI12_RUN_DIR/wificond.txt"    adb shell cmd wifi help > "$WIFI12_RUN_DIR/wifi-help.txt")

终端 B 设为同一个 ANDROID_SERIAL,并把 WIFI12_RUN_DIR 设为终端 A 打印的目录。观察器保持运行且没有 adb 错误时,回到 12.3 的 App,点一次「扫描附近 Wi-Fi」。筛选过的日志在扫描前可能一直静默:

export ANDROID_SERIAL='替换为实际设备序列号'WIFI12_RUN_DIR='/替换为终端A打印的目录'(    set -o pipefail    test -d "$WIFI12_RUN_DIR" || exit 1    adb logcat -v threadtime WifiChapter12:I WifiService:I ScanRequestProxy:D WificondScannerImpl:D wificond:V '*:S' 2>&1 | tee "$WIFI12_RUN_DIR/scan.log")

只筛选标签可能漏掉未启用的调试日志。没有看到某条日志不证明某层没运行。保留时间、PID/TID,并在终端 A 再采 dumpsys wifi 作对照。输出文件存在只说明采集命令有产物。还需检查内容和退出码。

可选 shell 入口是 adb shell cmd wifi start-scan、adb shell cmd wifi list-scan-results。当前 WifiShellCommand 的 start-scan 忽略 mWifiService.startScan() 返回值并直接 return 0,所以 shell 退出码 0 不证明扫描受理,更不证明完成。 shell 权限身份也不同于普通 App,不能用它验证 App 节流或位置授权。

12.11 从“看见热点”接到“建立连接”

扫描结果是候选输入,不携带认证凭据,也不会自动把每个结果连上。系统保存配置、用户选择、网络建议或网络请求策略决定候选。普通 App 在新版本受网络配置 API 限制,应使用 WifiNetworkSpecifier/WifiNetworkSuggestion 等相应入口;不教普通 App 绕过系统策略直接调用内部 connectToNetwork。

本节选择“已有合法配置,经系统连接策略进入 ClientModeImpl”作为衔接边界。startConnectToNetwork(networkId,uid,bssid) 发送 CMD_START_CONNECT;状态机查 WifiConfiguration、选择 BSSID/安全参数,connectToNetwork(config) 调 WifiNative.connectToNetwork,再由 SupplicantStaIfaceHal 处理。

当前封装优先选择 AIDL Mainline 服务,然后 AIDL Vendor,再回退已声明的 HIDL;没有服务则失败。它们是可选实现,不是依次经过的三级 HAL。 本节摘读 AIDL 基类与 external/wpa_supplicant_8 的 AIDL Vendor 实现作可读连接路径,不把 Mainline/HIDL 符号混入同一条远端调用。

AIDL 基类 connectToNetwork 比较已有配置:同网络可复用 remote handle,需要时更新 BSSID;新网络则清旧句柄/配置、removeAllNetworks,addNetworkAndSaveConfig,保存新的远端 ISupplicantStaNetwork 包装与本地配置。最后:

            if (!networkHandle.select()) {                Log.e(TAG, "Failed to select network configuration: " + config.getProfileKey());                return false;            }            mCurrentNetworkConnectTimestamp.put(ifaceName, mClock.getElapsedSinceBootMillis());            return true;

来源:Wifi,SupplicantStaIfaceHalAidlBase.java,私有 connectToNetwork(String,WifiConfiguration,WifiSsid)。这个 select() 是配置启动请求;返回 true 不等于已关联或已拿到 IP。 网络包装通过 AIDL 调远端 StaNetwork::select,selectInternal 调 wpa_supplicant_select_network。

Supplicant 根据网络、安全能力和状态可能再扫描/选 BSS,再经 SME/driver_ops 认证关联。driver_nl80211.c::wpa_driver_nl80211_associate() 按 WPA_DRIVER_FLAGS_SME 分支:支持用户态 SME 时走 NL80211_CMD_ASSOCIATE(认证另有 authenticate),否则调用 connect 路径发送 NL80211_CMD_CONNECT。这同样不能画成 AUTHENTICATE→ASSOCIATE→CONNECT 固定串行;hwsim/mac80211 常用用户态 SME,但实际能力仍需目标核验。本章不逐行展开 WPA/EAP 密钥状态机。

反向状态通过 Supplicant AIDL 回调到 SupplicantStaIfaceCallbackAidlImpl。当状态 COMPLETED 时发 WifiMonitor.NETWORK_CONNECTION_EVENT,再发状态变化事件;ClientModeImpl 接收事件,核对配置并推进二层状态。随后常规连接进入 IpClient 的 IP 配置;FILS 可提前准备,不能写成所有连接严格等到这一刻才创建 IpClient。

startIpClient() 组织 ProvisioningConfiguration 后调用 mIpClient.startProvisioning;onProvisioningSuccess(LinkProperties) 发送 CMD_IP_CONFIGURATION_SUCCESSFUL。它与 Supplicant COMPLETED 分别表示 IP 配置和二层完成。ClientModeImpl 进入 L2ConnectedState 时注册 WifiNetworkAgent。IP 配置成功后,已有 Agent 向 Connectivity 更新 LinkProperties,进入 L3ConnectedState 时再标记连接,互联网验证另行处理。认证失败/关联拒绝、DHCP 失败、网络无互联网要分别看状态和失败理由,不能都称“扫描失败”。

在连接链上分开看二层状态、LinkProperties 与互联网验证。

12.12 回看、排障与自测

按钮提示「请求已提交」却没看到新条目时,先按 12.3 检查 CALLBACK、RESULT 和 ENTRY。需要定位系统原因时,再沿 12.4 的流程图检查请求是否在排队、底层是否失败,以及这次缓存更新是否会通知 App。已经连上 Wi-Fi 但不能上网时,回到 12.11 分别看二层状态、IP 配置和互联网验证。

现象
首查入口
判据
startScan=false
WifiServiceImpl、ScanRequestProxy
区分权限、idle、节流和线程超时
shell 退出 0 但没结果
WifiShellCommand、系统日志
本版本 shell 没传出受理结果
callback 无结果或旧条目
GlobalScanListener、缓存、timestamp
外部全扫描可触发;GET_SCAN 可能无新 BSS
频繁 REASON_BUSY
Kernel scan_req、接口并发
找并发扫描和未完成请求,不盲目无限重试
有 COMPLETED 但不能上网
IpClient、LinkProperties、Connectivity 验证
二层、IP 和互联网验证分层确认

自测与参考答案:

1. 为什么 ACK 不等于完成?——ACK 确认 TRIGGER_SCAN 命令,完成另外多播,列表还需 GET_SCAN。

2. 为什么扫描图不固定包含 Supplicant?——本章单次扫描由 wificond 或直接 nl80211 执行,连接/安全控制另走 Supplicant;它自己也可能因连接而扫描。

3. 为什么 hw_scan 不能证明 FullMAC?——mac80211 的 SoftMAC Driver 也可实现这个回调;本章 hwsim 即有条件示例。

4. callback 为什么不能证明“我的请求产生这批结果”?——通知与共享缓存没有应用专属扫描 ID,平台/其他请求可触发。

5. App 离开页面就会取消全系统扫描吗?——注销只撤销自身通知;扫描调度、接口和 Kernel 请求由各层生命周期管理。

6. 怎样确认设备走 hwsim?——先核对接口、驱动与 radio 配置,再结合目标设备的扫描日志或 Trace。本章的源码分支提供核对入口。

附录:可复制的 Wi-Fi 扫描示例

按 12.3 创建项目并保留模板生成的 Gradle 文件,替换下列两个文件。Activity 用代码创建一个按钮,无需再添加布局或依赖。模板遗留的布局可以保留,它不会被此 Activity 加载。

app/src/main/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android">    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />    <application android:label="Wi-Fi 扫描观察"        android:theme="@android:style/Theme.Material.Light.NoActionBar">        <activity android:name="com.example.wifiscandemo.MainActivity" android:exported="true">            <intent-filter>                <action android:name="android.intent.action.MAIN" />                <category android:name="android.intent.category.LAUNCHER" />            </intent-filter>        </activity>    </application></manifest>

app/src/main/java/com/example/wifiscandemo/MainActivity.java

package com.example.wifiscandemo;import android.Manifest;import android.app.Activity;import android.content.pm.PackageManager;import android.location.LocationManager;import android.net.wifi.ScanResult;import android.net.wifi.WifiManager;import android.os.Bundle;import android.os.SystemClock;import android.util.Log;import android.view.Gravity;import android.widget.Button;import android.widget.FrameLayout;import android.widget.Toast;import java.util.List;public class MainActivity extends Activity {    private static final String TAG = "WifiChapter12";    private static final int REQUEST_LOCATION = 1;    private WifiManager wifi;    private WifiManager.ScanResultsCallback observer;    private long requestedAtUs;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        wifi = getApplicationContext().getSystemService(WifiManager.class);        Button scanButton = new Button(this);        scanButton.setText("扫描附近 Wi-Fi");        scanButton.setOnClickListener(view -> observeAndRequest());        FrameLayout screen = new FrameLayout(this);        screen.addView(scanButton, new FrameLayout.LayoutParams(                FrameLayout.LayoutParams.WRAP_CONTENT,                FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER));        setContentView(screen);    }    @SuppressWarnings("deprecation") // 本章演示公开 SDK 中仍可调用的 startScan。    private void observeAndRequest() {        if (wifi == null) {            tell("WIFI_UNAVAILABLE", "设备未提供 WifiManager");            return;        }        if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)                != PackageManager.PERMISSION_GRANTED) {            tell("LOCATION_PERMISSION_REQUIRED", "授予精确位置后再点扫描");            requestPermissions(new String[]{                    Manifest.permission.ACCESS_FINE_LOCATION,                    Manifest.permission.ACCESS_COARSE_LOCATION            }, REQUEST_LOCATION);            return;        }        LocationManager location = getSystemService(LocationManager.class);        if (location == null || !location.isLocationEnabled()) {            tell("LOCATION_DISABLED", "请在系统设置中开启位置服务");            return;        }        if (!wifi.isWifiEnabled()) {            tell("WIFI_DISABLED", "请在系统设置中开启 Wi-Fi");            return;        }        try {            if (observer == null) {                WifiManager.ScanResultsCallback callback = new WifiManager.ScanResultsCallback() {                    @Override                    public void onScanResultsAvailable() {                        if (observer == this) readResults();                    }                };                wifi.registerScanResultsCallback(getMainExecutor(), callback);                observer = callback;            }            requestedAtUs = SystemClock.elapsedRealtimeNanos() / 1_000;            boolean accepted = wifi.startScan();            tell("REQUEST accepted=" + accepted, accepted ? "请求已提交" : "请求未受理");        } catch (SecurityException e) {            Log.w(TAG, "REQUEST_DENIED", e);            tell("REQUEST_DENIED", "系统拒绝扫描操作");            stopObserving();        }    }    private void readResults() {        Log.i(TAG, "CALLBACK received");        try {            List<ScanResult> results = wifi.getScanResults();            tell("RESULT count=" + results.size(), "缓存条目数:" + results.size());            for (ScanResult result : results) {                long ageMs = Math.max(0, SystemClock.elapsedRealtime() - result.timestamp / 1_000);                Log.i(TAG, "ENTRY ageMs=" + ageMs                        + " seenAtOrAfterRequest=" + (result.timestamp >= requestedAtUs)                        + " frequency=" + result.frequency);            }        } catch (SecurityException e) {            Log.w(TAG, "READ_DENIED", e);            tell("READ_DENIED", "系统拒绝读取扫描缓存");        }    }    private void stopObserving() {        WifiManager.ScanResultsCallback callback = observer;        observer = null;        if (callback != null) {            wifi.unregisterScanResultsCallback(callback);            Log.i(TAG, "OBSERVER stopped");        }    }    @Override    protected void onStop() {        stopObserving();        super.onStop();    }    private void tell(String log, String message) {        Log.i(TAG, log);        Toast.makeText(this, message, Toast.LENGTH_SHORT).show();    }}

参考资料

• 位置运行时权限官方说明。

• Wi-Fi 扫描过程与限制。

• 创建与管理 Android 虚拟设备。

• WifiManager.ScanResultsCallback API。

• Wifi 固定提交,主要符号见 12.5~12.11。

• WifiNl80211Manager 固定文件。

• wificond 固定提交。

• Supplicant 固定提交。

• Wifi HAL 契约固定源码。

• Goldfish 配置固定源码。

• Kernel 固定提交。

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料