android p wifi一直在扫描_最全面的Android Wifi扫描分析

使用

开始wifi扫描的代码很简单:

val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager

val success = wifiManager.startScan()

if (!success) {

// scan failure handling

scanFailure()

}

然后定义一个receiver接收结果

val wifiScanReceiver = object : BroadcastReceiver() {

override fun onReceive(context: Context, intent: Intent) {

val success = intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false)

if (success) {

val results = wifiManager.scanResults

} else {

scanFailure()

}

}

}

val intentFilter = IntentFilter()

intentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)

context.registerReceiver(wifiScanReceiver, intentFilter)

注意:scanFailure时,wifiManager.scanResults的数据未上一次的扫描结果

版本差异

Android 8以下:

未限制

Android 8.0 和 Android 8.1:

每个后台应用可以在 30 分钟内扫描一次。

需要申明以下任意一项权限即可:

ACCESS_FINE_LOCATION

ACCESS_COARSE_LOCATION

CHANGE_WIFI_STATE

Android 9:

每个前台应用可以在 2 分钟内扫描四次。这样便可在短时间内进行多次扫描。

所有后台应用组合可以在 30 分钟内扫描一次。

需要申明以下所有权限:

ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION

CHANGE_WIFI_STATE

设备已启用位置服务 (Settings > Location)。

Android 10 及更高版本:

用 Android 9 的节流限制。新增一个开发者选项,用户可以关闭节流功能以便进行本地测试(Developer Options > Networking > Wi-Fi scan throttling)

target>=29,必须有 ACCESS_FINE_LOCATION

target<29,ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION都可以

CHANGE_WIFI_STATE

设备已启用位置服务 (Settings > Location)。

源码解析

startScan

WifiManager类中的startScan方法:

/** @hide */

@SystemApi

@RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)

public boolean startScan(WorkSource workSource) {

try {

String packageName = mContext.getOpPackageName();

mService.startScan(null, workSource, packageName);

return true;

} catch (RemoteException e) {

throw e.rethrowFromSystemServer();

}

}

最终通过IWifiManager.aidl,调用的是WifiServiceImpl类

不同系统版本有不同实现

1. Android 6.0,7.0系统:

public void startScan(ScanSettings settings, WorkSource workSource) {

enforceChangePermission();

synchronized (this) {

if (mInIdleMode) {

// Need to send an immediate scan result broadcast in case the

// caller is waiting for a result ..

// clear calling identity to send broadcast

long callingIdentity = Binder.clearCallingIdentity();

try {

mWifiStateMachine.sendScanResultsAvailableBroadcast(/* scanSucceeded = */ false);

} finally {

// restore calling identity

Binder.restoreCallingIdentity(callingIdentity);

}

mScanPending = true;

return;

}

}

...

mWifiStateMachine.startScan(Binder.getCallingUid(), scanRequestCounter++,

settings, workSource);

}

enforceChangePermission是检查是否有CHANGE_WIFI_STATE的权限

mInIdleMode由powermanager判定设备是否处于空闲状态

如果处于空闲,则不再真正扫描,而是调用WifiStateMachine发送最近可用的扫描结果

我们看下WifiStateMachine的代码:

/**

* Track the state of Wifi connectivity. All event handling is done here,

* and all changes in connectivity state are initiated here.

*

* Wi-Fi now supports three modes of operation: Client, SoftAp and p2p

* In the current implementation, we support concurrent wifi p2p and wifi operation.

* The WifiStateMachine handles SoftAp and Client operations while WifiP2pService

* handles p2p operation.

*

* @hide

*/

public class WifiStateMachine extends StateMachine implements WifiNative.WifiRssiEventHandler {

...

public void startScan(int callingUid, int scanCounter,

ScanSettings settings, WorkSource workSource) {

Bundle bundle = new Bundle();

bundle.putParcelable(CUSTOMIZED_SCAN_SETTING, settings);

bundle.putParcelable(CUSTOMIZED_SCAN_WORKSOURCE, workSource);

bundle.putLong(SCAN_REQUEST_TIME, System.currentTimeMillis());

sendMessage(CMD_START_SCAN, callingUid, scanCounter, bundle);

}

...

}

这个类主要维护Wifi连接的各种状态,以及所有事件的处理

其中维护了ScanModeState,DriverStartedState,DriverStartingState,ConnectModeState等等

在ScanModeState中的processMessage方法调用了handleScanRequest方法:

class ScanModeState extends State {

...

@Override

public boolean processMessage(Message message) {

// Handle scan. All the connection related commands are

// handled only in ConnectModeState

case CMD_START_SCAN:

handleScanRequest(message);

break;

}

}

private void handleScanRequest(Message message) {

...

// call wifi native to start the scan

if (startScanNative(freqs, hiddenNetworkIds, workSource)) {

// a full scan covers everything, clearing scan request buffer

if (freqs == null)

mBufferedScanMsg.clear();

messageHandlingStatus = MESSAGE_HANDLING_STATUS_OK;

if (workSource != null) {

// External worksource was passed along the scan request,

// hence always send a broadcast

mSendScanResultsBroadcast = true;

}

return;

}

....

}

private boolean startScanNative(final Set freqs, Set hiddenNetworkIds,

WorkSource workSource) {

...

WifiScanner.ScanListener nativeScanListener = new WifiScanner.ScanListener() {

// ignore all events since WifiStateMachine is registered for the supplicant events

public void onSuccess() {

}

public void onFailure(int reason, String description) {

mIsScanOngoing = false;

mIsFullScanOngoing = false;

}

public void onResults(WifiScanner.ScanData[] results) {

}

public void onFullResult(ScanResult fullScanResult) {

}

public void onPeriodChanged(int periodInMs) {

}

};

mWifiScanner.startScan(settings, nativeScanListener, workSource);

...

}

WifiScanner中startScan方法,通过AsyncChannel中的Messenger将message发送到WifiScanningServiceImpl中

mWifiScanner.startScan最终调用的是WifiScanningServiceImpl中:

public class WifiScanningServiceImpl extends IWifiScanner.Stub {

...

class DriverStartedState extends State {

@Override

public boolean processMessage(Message msg) {

case WifiScanner.CMD_START_SINGLE_SCAN:

if (validateScanRequest(ci, handler, scanSettings)) {

...

replySucceeded(msg);

// If there is an active scan that will fulfill the scan request then

// mark this request as an active scan, otherwise mark it pending.

// If were not currently scanning then try to start a scan. Otherwise

// this scan will be scheduled when transitioning back to IdleState

// after finishing the current scan.

if (getCurrentState() == mScanningState) {

if (activeScanSatisfies(scanSettings)) {

mActiveScans.addRequest(ci, handler, workSource, scanSettings);

} else {

mPendingScans.addRequest(ci, handler, workSource, scanSettings);

}

} else {

mPendingScans.addRequest(ci, handler, workSource, scanSettings);

tryToStartNewScan();

}

} else {

...

}

}

}

void tryToStartNewScan() {

...

if (mScannerImpl.startSingleScan(settings, this)) {

...

} else {

....

}

}

...

}

如果在DefaultState状态下接受到scan请求,该次扫描失败。

如果在ScanningState状态下接受到scan请求: 如果当前正在进行的扫描能满足需求,将请求加入active队列,否则加入挂起队列

如果是其他状态直接加入挂起队列,并立即调用tryToStartNewScan()

mScannerImpl通过工厂方法生成的实例为WificondScannerImpl,在WificondScannerImpl中startSingleScan:

@Override

public boolean startSingleScan(WifiNative.ScanSettings settings,

WifiNative.ScanEventHandler eventHandler) {

synchronized (mSettingsLock) {

...

if (!allFreqs.isEmpty()) {

freqs = allFreqs.getScanFreqs();

success = mWifiNative.scan(

mIfaceName, settings.scanType, freqs, hiddenNetworkSSIDSet);

if (!success) {

Log.e(TAG, "Failed to start scan, freqs=" + freqs);

}

} else {

// There is a scan request but no available channels could be scanned for.

// We regard it as a scan failure in this case.

Log.e(TAG, "Failed to start scan because there is no available channel to scan");

}

if (success) {

mScanTimeoutListener = new AlarmManager.OnAlarmListener() {

@Override public void onAlarm() {

handleScanTimeout();

}

};

mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,

mClock.getElapsedSinceBootMillis() + SCAN_TIMEOUT_MS,

TIMEOUT_ALARM_TAG, mScanTimeoutListener, mEventHandler);

} else {

...

}

return true;

}

}

可见在调用了mWifiNative.scan后,还设置了timeout机制,交给AlarmManager去执行

WifiNative中的scan调用的是WificondControl中的scan方法,我们看下WificondControl中:

public boolean scan(@NonNull String ifaceName,

int scanType,

Set freqs,

List hiddenNetworkSSIDs) {

IWifiScannerImpl scannerImpl = getScannerImpl(ifaceName);

...

try {

return scannerImpl.scan(settings);

} catch (RemoteException e1) {

Log.e(TAG, "Failed to request scan due to remote exception");

}

return false;

}

private IWifiScannerImpl getScannerImpl(@NonNull String ifaceName) {

return mWificondScanners.get(ifaceName);

}

mWificondScanners是个hashmap,数据在 setupInterfaceForClientMode方法中put进去:

public IClientInterface setupInterfaceForClientMode(@NonNull String ifaceName) {

IClientInterface clientInterface = null;

try {

clientInterface = mWificond.createClientInterface(ifaceName);

} catch (RemoteException e1) {

Log.e(TAG, "Failed to get IClientInterface due to remote exception");

return null;

}

...

try {

IWifiScannerImpl wificondScanner = clientInterface.getWifiScannerImpl();

if (wificondScanner == null) {

Log.e(TAG, "Failed to get WificondScannerImpl");

return null;

}

mWificondScanners.put(ifaceName, wificondScanner);

...

} catch (RemoteException e) {

Log.e(TAG, "Failed to refresh wificond scanner due to remote exception");

}

return clientInterface;

}

mWificond的类型为IWificond,clientInterface的类型为IClientInterface,这两个都是aidl生成的接口,具体的实现在IWificond.cpp中,cpp中的内容此处就不做深入

2. Android 8.0,8.1系统:

WifiServiceImpl类中:

@Override

public void startScan(ScanSettings settings, WorkSource workSource, String packageName) {

enforceChangePermission();

mLog.trace("startScan uid=%").c(Binder.getCallingUid()).flush();

// Check and throttle background apps for wifi scan.

if (isRequestFromBackground(packageName)) {

long lastScanMs = mLastScanTimestamps.getOrDefault(packageName, 0L);

long elapsedRealtime = mClock.getElapsedSinceBootMillis();

if (lastScanMs != 0 && (elapsedRealtime - lastScanMs) < mBackgroundThrottleInterval) {

sendFailedScanBroadcast();

return;

}

// Proceed with the scan request and record the time.

mLastScanTimestamps.put(packageName, elapsedRealtime);

}

synchronized (this) {

if (mWifiScanner == null) {

mWifiScanner = mWifiInjector.getWifiScanner();

}

if (mInIdleMode) {

// Need to send an immediate scan result broadcast in case the

// caller is waiting for a result ..

// TODO: investigate if the logic to cancel scans when idle can move to

// WifiScanningServiceImpl. This will 1 - clean up WifiServiceImpl and 2 -

// avoid plumbing an awkward path to report a cancelled/failed scan. This will

// be sent directly until b/31398592 is fixed.

sendFailedScanBroadcast();

mScanPending = true;

return;

}

}

...

mWifiStateMachine.startScan(Binder.getCallingUid(), scanRequestCounter++,

settings, workSource);

}

可见多了一个isRequestFromBackground操作,如果是background进程的调用,距上次调用< mBackgroundThrottleInterval,则sendFailedScanBroadcast

mBackgroundThrottleInterval参数是通过读取framework中的设置,这个值默认为30分钟

private void updateBackgroundThrottleInterval() {

mBackgroundThrottleInterval = mFrameworkFacade.getLongSetting(

mContext,

Settings.Global.WIFI_SCAN_BACKGROUND_THROTTLE_INTERVAL_MS,

DEFAULT_SCAN_BACKGROUND_THROTTLE_INTERVAL_MS);

}

3. Android 9.0系统:

WifiServiceImpl类中:

@Override

public boolean startScan(String packageName) {

if (enforceChangePermission(packageName) != MODE_ALLOWED) {

return false;

}

int callingUid = Binder.getCallingUid();

long ident = Binder.clearCallingIdentity();

mLog.info("startScan uid=%").c(callingUid).flush();

synchronized (this) {

if (mInIdleMode) {

// Need to send an immediate scan result broadcast in case the

// caller is waiting for a result ..

// TODO: investigate if the logic to cancel scans when idle can move to

// WifiScanningServiceImpl. This will 1 - clean up WifiServiceImpl and 2 -

// avoid plumbing an awkward path to report a cancelled/failed scan. This will

// be sent directly until b/31398592 is fixed.

sendFailedScanBroadcast();

mScanPending = true;

return false;

}

}

try {

mWifiPermissionsUtil.enforceCanAccessScanResults(packageName, callingUid);

Mutable scanSuccess = new Mutable<>();

boolean runWithScissorsSuccess = mWifiInjector.getWifiStateMachineHandler()

.runWithScissors(() -> {

scanSuccess.value = mScanRequestProxy.startScan(callingUid, packageName);

}, RUN_WITH_SCISSORS_TIMEOUT_MILLIS);

if (!runWithScissorsSuccess) {

Log.e(TAG, "Failed to post runnable to start scan");

sendFailedScanBroadcast();

return false;

}

if (!scanSuccess.value) {

Log.e(TAG, "Failed to start scan");

return false;

}

} catch (SecurityException e) {

return false;

} finally {

Binder.restoreCallingIdentity(ident);

}

return true;

}

mWifiPermissionsUtil.enforceCanAccessScanResults ,permission判断对比8.0有更新,WifiPermissionsUtil会判断各种所需权限

scan交给mScanRequestProxy去做:

public boolean startScan(int callingUid, String packageName) {

if (!retrieveWifiScannerIfNecessary()) {

Log.e(TAG, "Failed to retrieve wifiscanner");

sendScanResultFailureBroadcastToPackage(packageName);

return false;

}

boolean fromSettingsOrSetupWizard =

mWifiPermissionsUtil.checkNetworkSettingsPermission(callingUid)

|| mWifiPermissionsUtil.checkNetworkSetupWizardPermission(callingUid);

// Check and throttle scan request from apps without NETWORK_SETTINGS permission.

if (!fromSettingsOrSetupWizard

&& shouldScanRequestBeThrottledForApp(callingUid, packageName)) {

Log.i(TAG, "Scan request from " + packageName + " throttled");

sendScanResultFailureBroadcastToPackage(packageName);

return false;

}

// Create a worksource using the caller's UID.

WorkSource workSource = new WorkSource(callingUid);

// Create the scan settings.

WifiScanner.ScanSettings settings = new WifiScanner.ScanSettings();

// Scan requests from apps with network settings will be of high accuracy type.

if (fromSettingsOrSetupWizard) {

settings.type = WifiScanner.TYPE_HIGH_ACCURACY;

}

// always do full scans

settings.band = WifiScanner.WIFI_BAND_BOTH_WITH_DFS;

settings.reportEvents = WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN

| WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT;

if (mScanningForHiddenNetworksEnabled) {

// retrieve the list of hidden network SSIDs to scan for, if enabled.

List hiddenNetworkList =

mWifiConfigManager.retrieveHiddenNetworkList();

settings.hiddenNetworks = hiddenNetworkList.toArray(

new WifiScanner.ScanSettings.HiddenNetwork[hiddenNetworkList.size()]);

}

mWifiScanner.startScan(settings, new ScanRequestProxyScanListener(), workSource);

mIsScanProcessingComplete = false;

return true;

return true;

}

这里直接交给WifiScanner去执行,相比于之前版本省去了WifiStateMachine状态管理

如果一个应用拥有networksetting权限(就是android中的设置才有的权限,一般应用不可能有)则可以不受任何限制地扫描,如果没有这个权限,这Wi-Fi扫描将会执行shouldScanRequestBeThrottledForApp,如果返回为ture,则sendScanResultFailureBroadcastToPackage

看下shouldScanRequestBeThrottledForApp做了什么:

private boolean shouldScanRequestBeThrottledForApp(int callingUid, String packageName) {

boolean isThrottled;

if (isRequestFromBackground(callingUid, packageName)) {

isThrottled = shouldScanRequestBeThrottledForBackgroundApp();

if (isThrottled) {

if (mVerboseLoggingEnabled) {

Log.v(TAG, "Background scan app request [" + callingUid + ", "

+ packageName + "]");

}

mWifiMetrics.incrementExternalBackgroundAppOneshotScanRequestsThrottledCount();

}

} else {

isThrottled = shouldScanRequestBeThrottledForForegroundApp(callingUid, packageName);

if (isThrottled) {

if (mVerboseLoggingEnabled) {

Log.v(TAG, "Foreground scan app request [" + callingUid + ", "

+ packageName + "]");

}

mWifiMetrics.incrementExternalForegroundAppOneshotScanRequestsThrottledCount();

}

}

mWifiMetrics.incrementExternalAppOneshotScanRequestsCount();

return isThrottled;

}

private boolean shouldScanRequestBeThrottledForBackgroundApp() {

long lastScanMs = mLastScanTimestampForBgApps;

long elapsedRealtime = mClock.getElapsedSinceBootMillis();

if (lastScanMs != 0

&& (elapsedRealtime - lastScanMs) < SCAN_REQUEST_THROTTLE_INTERVAL_BG_APPS_MS) {

return true;

}

// Proceed with the scan request and record the time.

mLastScanTimestampForBgApps = elapsedRealtime;

return false;

}

private boolean shouldScanRequestBeThrottledForForegroundApp(

int callingUid, String packageName) {

LinkedList scanRequestTimestamps =

getOrCreateScanRequestTimestampsForForegroundApp(callingUid, packageName);

long currentTimeMillis = mClock.getElapsedSinceBootMillis();

// First evict old entries from the list.

trimPastScanRequestTimesForForegroundApp(scanRequestTimestamps, currentTimeMillis);

if (scanRequestTimestamps.size() >= SCAN_REQUEST_THROTTLE_MAX_IN_TIME_WINDOW_FG_APPS) {

return true;

}

// Proceed with the scan request and record the time.

scanRequestTimestamps.addLast(currentTimeMillis);

return false;

}

判断应用为前台应该还是后台应用(前后台限制在这里)

后台应用限制SCAN_REQUEST_THROTTLE_INTERVAL_BG_APPS_MS未30分钟

前台应用方法获取了一个保存每次扫描时间戳的链表scanRequestTimestamps ,每个应用又以不同的链表uid为键将自己的链表保存在一个设备全局的map中,每次调用这个方法将移除这个链表2分钟以前的时间戳,如果移除以后,这个链表仍然大于4,则取消本次扫描,否则将当前时间戳加入链表。

3. Android 10.0系统:

和Android 9相比,Android10 在enforceCanAccessScanResults 中作了更多判断:

public class WifiPermissionsUtil {

public void enforceCanAccessScanResults(String pkgName, int uid) throws SecurityException {

checkPackage(uid, pkgName);

// Apps with NETWORK_SETTINGS, NETWORK_SETUP_WIZARD, NETWORK_MANAGED_PROVISIONING,

// NETWORK_STACK & MAINLINE_NETWORK_STACK are granted a bypass.

if (checkNetworkSettingsPermission(uid) || checkNetworkSetupWizardPermission(uid)

|| checkNetworkManagedProvisioningPermission(uid)

|| checkNetworkStackPermission(uid) || checkMainlineNetworkStackPermission(uid)) {

return;

}

// Location mode must be enabled

if (!isLocationModeEnabled()) {

// Location mode is disabled, scan results cannot be returned

throw new SecurityException("Location mode is disabled for the device");

}

// Check if the calling Uid has CAN_READ_PEER_MAC_ADDRESS permission.

boolean canCallingUidAccessLocation = checkCallerHasPeersMacAddressPermission(uid);

// LocationAccess by App: caller must have Coarse/Fine Location permission to have access to

// location information.

boolean canAppPackageUseLocation = checkCallersLocationPermission(pkgName,

uid, /* coarseForTargetSdkLessThanQ */ true);

// If neither caller or app has location access, there is no need to check

// any other permissions. Deny access to scan results.

if (!canCallingUidAccessLocation && !canAppPackageUseLocation) {

throw new SecurityException("UID " + uid + " has no location permission");

}

// Check if Wifi Scan request is an operation allowed for this App.

if (!isScanAllowedbyApps(pkgName, uid)) {

throw new SecurityException("UID " + uid + " has no wifi scan permission");

}

// If the User or profile is current, permission is granted

// Otherwise, uid must have INTERACT_ACROSS_USERS_FULL permission.

if (!isCurrentProfile(uid) && !checkInteractAcrossUsersFull(uid)) {

throw new SecurityException("UID " + uid + " profile not permitted");

}

}

}

多了checkNetworkManagedProvisioningPermission,checkNetworkStackPermission和checkMainlineNetworkStackPermission

总结

WifiManager通过aidl调用WifiServiceImpl,由app进程发送到系统进程

在scan之前会检查permission

9.0以下通过WifiStateMachine管理状态,9.0以上通过ScanRequestProxy代理,并交于WifiScanner

WifiScanner通过AsyncChannel中的Messenger将message发送到WifiScanningServiceImpl中

WifiScanningServiceImpl自身也有状态管理,mScannerImpl调用scan后,启动了超时机制

最终由IWificond.aidl实现者IWificond.cpp实现scan操作

整理了下流程图:

sequence_diagram.png

getScanResults

WifiServiceImpl中:

@Override

public List getScanResults(String callingPackage) {

enforceAccessPermission();

...

try {

mWifiPermissionsUtil.enforceCanAccessScanResults(callingPackage, uid);

final List scanResults = new ArrayList<>();

boolean success = mWifiInjector.getClientModeImplHandler().runWithScissors(() -> {

scanResults.addAll(mScanRequestProxy.getScanResults());

}, RUN_WITH_SCISSORS_TIMEOUT_MILLIS);

if (!success) {

Log.e(TAG, "Failed to post runnable to fetch scan results");

return new ArrayList();

}

return scanResults;

} catch (SecurityException e) {

Slog.e(TAG, "Permission violation - getScanResults not allowed for uid="

+ uid + ", packageName=" + callingPackage + ", reason=" + e);

return new ArrayList();

} finally {

Binder.restoreCallingIdentity(ident);

}

}

也做了permisson的判断

ScanRequestProxy中的mLastScanResults是如何set的呢:

private class GlobalScanListener implements WifiScanner.ScanListener {

@Override

public void onResults(WifiScanner.ScanData[] scanDatas) {

...

if (scanData.getBandScanned() == WifiScanner.WIFI_BAND_BOTH_WITH_DFS) {

// Store the last scan results & send out the scan completion broadcast.

mLastScanResults.clear();

mLastScanResults.addAll(Arrays.asList(scanResults));

sendScanResultBroadcast(true);

}

}

}

private boolean retrieveWifiScannerIfNecessary() {

if (mWifiScanner == null) {

mWifiScanner = mWifiInjector.getWifiScanner();

// Start listening for throttle settings change after we retrieve scanner instance.

mThrottleEnabledSettingObserver.initialize();

// Register the global scan listener.

if (mWifiScanner != null) {

mWifiScanner.registerScanListener(new GlobalScanListener());

}

}

return mWifiScanner != null;

}

在WifiScanningServiceImpl 里注册了一个监听器

并将此接口加入到mSingleScanListeners中:

case WifiScanner.CMD_REGISTER_SCAN_LISTENER:

logScanRequest("registerScanListener", ci, msg.arg2, null, null, null);

mSingleScanListeners.addRequest(ci, msg.arg2, null, null);

replySucceeded(msg);

break;

在方法reportScanResults中会遍历mSingleScanListeners:

void reportScanResults(ScanData results) {

...

for (RequestInfo entry : mSingleScanListeners) {

logCallback("singleScanResults", entry.clientInfo, entry.handlerId,

describeForLog(allResults));

entry.reportEvent(WifiScanner.CMD_SCAN_RESULT, 0, parcelableAllResults);

}

...

}

当调用WifiScanner.getScanResults时会发送WifiScanner.CMD_GET_SCAN_RESULTS的message,当接收到WifiScanner.CMD_GET_SCAN_RESULTS的message时,会调用reportScanResults方法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值