/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.wifi.scanner; import static android.content.pm.PackageManager.PERMISSION_DENIED; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AlarmManager; import android.content.Context; import android.net.wifi.IWifiScanner; import android.net.wifi.ScanResult; import android.net.wifi.WifiAnnotations; import android.net.wifi.WifiManager; import android.net.wifi.WifiScanner; import android.net.wifi.WifiScanner.ChannelSpec; import android.net.wifi.WifiScanner.PnoSettings; import android.net.wifi.WifiScanner.ScanData; import android.net.wifi.WifiScanner.ScanSettings; import android.net.wifi.WifiScanner.WifiBand; import android.os.BadParcelableException; import android.os.BatteryStatsManager; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; import android.os.WorkSource; import android.util.ArrayMap; import android.util.ArraySet; import android.util.LocalLog; import android.util.Log; import android.util.Pair; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.AsyncChannel; import com.android.internal.util.Protocol; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import com.android.modules.utils.build.SdkLevel; import com.android.server.wifi.ClientModeImpl; import com.android.server.wifi.Clock; import com.android.server.wifi.FrameworkFacade; import com.android.server.wifi.WifiInjector; import com.android.server.wifi.WifiLog; import com.android.server.wifi.WifiMetrics; import com.android.server.wifi.WifiNative; import com.android.server.wifi.WifiThreadRunner; import com.android.server.wifi.proto.WifiStatsLog; import com.android.server.wifi.proto.nano.WifiMetricsProto; import com.android.server.wifi.scanner.ChannelHelper.ChannelCollection; import com.android.server.wifi.util.ArrayUtils; import com.android.server.wifi.util.LastCallerInfoManager; import com.android.server.wifi.util.ScanResultUtil; import com.android.server.wifi.util.WifiHandler; import com.android.server.wifi.util.WifiPermissionsUtil; import com.android.server.wifi.util.WorkSourceUtil; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; public class WifiScanningServiceImpl extends IWifiScanner.Stub { private static final String TAG = WifiScanningService.TAG; private static final boolean DBG = false; private static final int UNKNOWN_PID = -1; private final LocalLog mLocalLog = new LocalLog(512); private WifiLog mLog; private void localLog(String message) { mLocalLog.log(message); } private void logw(String message) { Log.w(TAG, message); mLocalLog.log(message); } private void loge(String message) { Log.e(TAG, message); mLocalLog.log(message); } @Override public Messenger getMessenger() { if (mClientHandler != null) { mLog.trace("getMessenger() uid=%").c(Binder.getCallingUid()).flush(); return new Messenger(mClientHandler); } loge("WifiScanningServiceImpl trying to get messenger w/o initialization"); return null; } @Override public Bundle getAvailableChannels(@WifiBand int band, String packageName, @Nullable String attributionTag) { int uid = Binder.getCallingUid(); long ident = Binder.clearCallingIdentity(); try { enforcePermission(uid, packageName, attributionTag, false, false, false); } finally { Binder.restoreCallingIdentity(ident); } ChannelSpec[][] channelSpecs = mWifiThreadRunner.call(() -> { if (mChannelHelper == null) return new ChannelSpec[0][0]; mChannelHelper.updateChannels(); return mChannelHelper.getAvailableScanChannels(band); }, new ChannelSpec[0][0]); ArrayList list = new ArrayList<>(); for (int i = 0; i < channelSpecs.length; i++) { for (ChannelSpec channelSpec : channelSpecs[i]) { list.add(channelSpec.frequency); } } Bundle b = new Bundle(); b.putIntegerArrayList(WifiScanner.GET_AVAILABLE_CHANNELS_EXTRA, list); mLog.trace("getAvailableChannels uid=%").c(Binder.getCallingUid()).flush(); return b; } private void enforceNetworkStack(int uid) { mContext.enforcePermission( Manifest.permission.NETWORK_STACK, UNKNOWN_PID, uid, "NetworkStack"); } // Helper method to check if the incoming message is for a privileged request. private boolean isPrivilegedMessage(int msgWhat) { return (msgWhat == WifiScanner.CMD_ENABLE || msgWhat == WifiScanner.CMD_DISABLE || msgWhat == WifiScanner.CMD_START_PNO_SCAN || msgWhat == WifiScanner.CMD_STOP_PNO_SCAN || msgWhat == WifiScanner.CMD_REGISTER_SCAN_LISTENER); } // For non-privileged requests, retrieve the bundled package name for app-op & permission // checks. private String getPackageName(Message msg) { if (!(msg.obj instanceof Bundle)) { return null; } Bundle bundle = (Bundle) msg.obj; return bundle.getString(WifiScanner.REQUEST_PACKAGE_NAME_KEY); } // For non-privileged requests, retrieve the bundled attributionTag name for app-op & permission // checks. private String getAttributionTag(Message msg) { if (!(msg.obj instanceof Bundle)) { return null; } Bundle bundle = (Bundle) msg.obj; return bundle.getString(WifiScanner.REQUEST_FEATURE_ID_KEY); } // Check if we should ignore location settings if this is a single scan request. private boolean shouldIgnoreLocationSettingsForSingleScan(Message msg) { if (msg.what != WifiScanner.CMD_START_SINGLE_SCAN) return false; if (!(msg.obj instanceof Bundle)) return false; Bundle bundle = (Bundle) msg.obj; try { ScanSettings scanSettings = bundle.getParcelable(WifiScanner.SCAN_PARAMS_SCAN_SETTINGS_KEY); if (scanSettings == null) return false; return scanSettings.ignoreLocationSettings; } catch (BadParcelableException e) { Log.wtf(TAG, "Failed to get parcelable params", e); return false; } } // Check if we should hide this request from app-ops if this is a single scan request. private boolean shouldHideFromAppsForSingleScan(Message msg) { if (msg.what != WifiScanner.CMD_START_SINGLE_SCAN) return false; if (!(msg.obj instanceof Bundle)) return false; Bundle bundle = (Bundle) msg.obj; ScanSettings scanSettings = bundle.getParcelable(WifiScanner.SCAN_PARAMS_SCAN_SETTINGS_KEY); return scanSettings.hideFromAppOps; } /** * @see #enforcePermission(int, String, String, boolean, boolean, boolean) */ private void enforcePermission(int uid, Message msg) throws SecurityException { enforcePermission(uid, getPackageName(msg), getAttributionTag(msg), isPrivilegedMessage(msg.what), shouldIgnoreLocationSettingsForSingleScan(msg), shouldHideFromAppsForSingleScan(msg)); } /** * Enforce the necessary client permissions for WifiScanner. * If the client has NETWORK_STACK permission, then it can "always" send "any" request. * If the client has only LOCATION_HARDWARE permission, then it can * a) Only make scan related requests when location is turned on. * b) Can never make one of the privileged requests. * @param uid uid of the client * @param packageName package name of the client * @param attributionTag The feature in the package of the client * @param isPrivilegedRequest whether we are checking for a privileged request * @param shouldIgnoreLocationSettings override to ignore location settings * @param shouldHideFromApps override to hide request from AppOps */ private void enforcePermission(int uid, String packageName, @Nullable String attributionTag, boolean isPrivilegedRequest, boolean shouldIgnoreLocationSettings, boolean shouldHideFromApps) { try { /** Wifi stack issued requests.*/ enforceNetworkStack(uid); } catch (SecurityException e) { // System-app issued requests if (isPrivilegedRequest) { // Privileged message, only requests from clients with NETWORK_STACK allowed! throw e; } mWifiPermissionsUtil.enforceCanAccessScanResultsForWifiScanner(packageName, attributionTag, uid, shouldIgnoreLocationSettings, shouldHideFromApps); } } private class ClientHandler extends WifiHandler { ClientHandler(String tag, Looper looper) { super(tag, looper); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION: { if (msg.replyTo == null) { logw("msg.replyTo is null"); return; } ExternalClientInfo client = (ExternalClientInfo) mClients.get(msg.replyTo); if (client != null) { logw("duplicate client connection: " + msg.sendingUid + ", messenger=" + msg.replyTo); client.mChannel.replyToMessage(msg, AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED, AsyncChannel.STATUS_FULL_CONNECTION_REFUSED_ALREADY_CONNECTED); return; } AsyncChannel ac = mFrameworkFacade.makeWifiAsyncChannel(TAG); ac.connected(mContext, this, msg.replyTo); client = new ExternalClientInfo(msg.sendingUid, msg.replyTo, ac); client.register(); ac.replyToMessage(msg, AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED, AsyncChannel.STATUS_SUCCESSFUL); localLog("client connected: " + client); return; } case AsyncChannel.CMD_CHANNEL_DISCONNECT: { ExternalClientInfo client = (ExternalClientInfo) mClients.get(msg.replyTo); if (client != null) { client.mChannel.disconnect(); } return; } case AsyncChannel.CMD_CHANNEL_DISCONNECTED: { ExternalClientInfo client = (ExternalClientInfo) mClients.get(msg.replyTo); if (client != null && msg.arg1 != AsyncChannel.STATUS_SEND_UNSUCCESSFUL && msg.arg1 != AsyncChannel.STATUS_FULL_CONNECTION_REFUSED_ALREADY_CONNECTED) { localLog("client disconnected: " + client + ", reason: " + msg.arg1); client.cleanup(); } return; } } try { enforcePermission(msg.sendingUid, msg); } catch (SecurityException e) { localLog("failed to authorize app: " + e); replyFailed(msg, WifiScanner.REASON_NOT_AUTHORIZED, "Not authorized"); return; } // Since the CMD_GET_SCAN_RESULTS and CMD_GET_SINGLE_SCAN_RESULTS messages are // sent from WifiScanner using |sendMessageSynchronously|, handle separately since // the |msg.replyTo| field does not actually correspond to the Messenger that is // registered for that client. if (msg.what == WifiScanner.CMD_GET_SCAN_RESULTS) { mBackgroundScanStateMachine.sendMessage(Message.obtain(msg)); return; } if (msg.what == WifiScanner.CMD_GET_SINGLE_SCAN_RESULTS) { mSingleScanStateMachine.sendMessage(Message.obtain(msg)); return; } ClientInfo ci = mClients.get(msg.replyTo); if (ci == null) { loge("Could not find client info for message " + msg.replyTo + ", msg=" + msg); replyFailed(msg, WifiScanner.REASON_INVALID_LISTENER, "Could not find listener"); return; } switch (msg.what) { case WifiScanner.CMD_ENABLE: Log.i(TAG, "Received a request to enable scanning, UID = " + msg.sendingUid); setupScannerImpls(); mBackgroundScanStateMachine.sendMessage(Message.obtain(msg)); mSingleScanStateMachine.sendMessage(Message.obtain(msg)); mPnoScanStateMachine.sendMessage(Message.obtain(msg)); mLastCallerInfoManager.put(LastCallerInfoManager.SCANNING_ENABLED, msg.arg1, msg.sendingUid, msg.arg2, (String) msg.obj, true); break; case WifiScanner.CMD_DISABLE: Log.i(TAG, "Received a request to disable scanning, UID = " + msg.sendingUid); teardownScannerImpls(); mBackgroundScanStateMachine.sendMessage(Message.obtain(msg)); mSingleScanStateMachine.sendMessage(Message.obtain(msg)); mPnoScanStateMachine.sendMessage(Message.obtain(msg)); mLastCallerInfoManager.put(LastCallerInfoManager.SCANNING_ENABLED, msg.arg1, msg.sendingUid, msg.arg2, (String) msg.obj, false); break; case WifiScanner.CMD_START_BACKGROUND_SCAN: case WifiScanner.CMD_STOP_BACKGROUND_SCAN: mBackgroundScanStateMachine.sendMessage(Message.obtain(msg)); break; case WifiScanner.CMD_START_PNO_SCAN: case WifiScanner.CMD_STOP_PNO_SCAN: mPnoScanStateMachine.sendMessage(Message.obtain(msg)); break; case WifiScanner.CMD_START_SINGLE_SCAN: case WifiScanner.CMD_STOP_SINGLE_SCAN: mSingleScanStateMachine.sendMessage(Message.obtain(msg)); break; case WifiScanner.CMD_REGISTER_SCAN_LISTENER: logScanRequest("registerScanListener", ci, msg.arg2, null, null, null); mSingleScanListeners.addRequest(ci, msg.arg2, null, null); replySucceeded(msg); break; case WifiScanner.CMD_DEREGISTER_SCAN_LISTENER: logScanRequest("deregisterScanListener", ci, msg.arg2, null, null, null); mSingleScanListeners.removeRequest(ci, msg.arg2); break; default: replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "Invalid request"); break; } } } private static final int BASE = Protocol.BASE_WIFI_SCANNER_SERVICE; private static final int CMD_SCAN_RESULTS_AVAILABLE = BASE + 0; private static final int CMD_FULL_SCAN_RESULTS = BASE + 1; private static final int CMD_SCAN_PAUSED = BASE + 8; private static final int CMD_SCAN_RESTARTED = BASE + 9; private static final int CMD_SCAN_FAILED = BASE + 10; private static final int CMD_PNO_NETWORK_FOUND = BASE + 11; private static final int CMD_PNO_SCAN_FAILED = BASE + 12; private final Context mContext; private final Looper mLooper; private final WifiThreadRunner mWifiThreadRunner; private final WifiScannerImpl.WifiScannerImplFactory mScannerImplFactory; private final ArrayMap mClients; private final Map mScannerImpls; private final RequestList mSingleScanListeners = new RequestList<>(); private ChannelHelper mChannelHelper; private BackgroundScanScheduler mBackgroundScheduler; private WifiNative.ScanSettings mPreviousSchedule; private WifiBackgroundScanStateMachine mBackgroundScanStateMachine; private WifiSingleScanStateMachine mSingleScanStateMachine; private WifiPnoScanStateMachine mPnoScanStateMachine; private ClientHandler mClientHandler; private final BatteryStatsManager mBatteryStats; private final AlarmManager mAlarmManager; private final WifiMetrics mWifiMetrics; private final Clock mClock; private final FrameworkFacade mFrameworkFacade; private final WifiPermissionsUtil mWifiPermissionsUtil; private final WifiNative mWifiNative; private final WifiManager mWifiManager; private final LastCallerInfoManager mLastCallerInfoManager; WifiScanningServiceImpl(Context context, Looper looper, WifiScannerImpl.WifiScannerImplFactory scannerImplFactory, BatteryStatsManager batteryStats, WifiInjector wifiInjector) { mContext = context; mLooper = looper; mWifiThreadRunner = new WifiThreadRunner(new Handler(looper)); mScannerImplFactory = scannerImplFactory; mBatteryStats = batteryStats; mClients = new ArrayMap<>(); mScannerImpls = new ArrayMap<>(); mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); mWifiMetrics = wifiInjector.getWifiMetrics(); mClock = wifiInjector.getClock(); mLog = wifiInjector.makeLog(TAG); mFrameworkFacade = wifiInjector.getFrameworkFacade(); mWifiPermissionsUtil = wifiInjector.getWifiPermissionsUtil(); mWifiNative = wifiInjector.getWifiNative(); // Wifi service is always started before other wifi services. So, there is no problem // obtaining WifiManager in the constructor here. mWifiManager = mContext.getSystemService(WifiManager.class); mPreviousSchedule = null; mLastCallerInfoManager = wifiInjector.getLastCallerInfoManager(); } public void startService() { mWifiThreadRunner.post(() -> { mBackgroundScanStateMachine = new WifiBackgroundScanStateMachine(mLooper); mSingleScanStateMachine = new WifiSingleScanStateMachine(mLooper); mPnoScanStateMachine = new WifiPnoScanStateMachine(mLooper); mBackgroundScanStateMachine.start(); mSingleScanStateMachine.start(); mPnoScanStateMachine.start(); // Create client handler only after StateMachines are ready. mClientHandler = new ClientHandler(TAG, mLooper); }); } /** * Checks if all the channels provided by the new impl is already satisfied by an existing impl. * * Note: This only handles the cases where the 2 ifaces are on different chips with * distinctly different bands supported on both. If there are cases where * the 2 ifaces support overlapping bands, then we probably need to rework this. * For example: wlan0 supports 2.4G only, wlan1 supports 2.4G + 5G + DFS. * In the above example, we should teardown wlan0 impl when wlan1 impl is created * because wlan1 impl can already handle all the supported bands. * Ignoring this for now since we don't foresee this requirement in the near future. */ private boolean doesAnyExistingImplSatisfy(WifiScannerImpl newImpl) { for (WifiScannerImpl existingImpl : mScannerImpls.values()) { if (existingImpl.getChannelHelper().satisfies(newImpl.getChannelHelper())) { return true; } } return false; } private void setupScannerImpls() { Set ifaceNames = mWifiNative.getClientInterfaceNames(); if (ArrayUtils.isEmpty(ifaceNames)) { loge("Failed to retrieve client interface names"); return; } Set ifaceNamesOfImplsAlreadySetup = mScannerImpls.keySet(); if (ifaceNames.equals(ifaceNamesOfImplsAlreadySetup)) { // Scanner Impls already exist for all ifaces (back to back CMD_ENABLE sent?). Log.i(TAG, "scanner impls already exists"); return; } // set of impls to teardown. Set ifaceNamesOfImplsToTeardown = new ArraySet<>(ifaceNamesOfImplsAlreadySetup); ifaceNamesOfImplsToTeardown.removeAll(ifaceNames); // set of impls to be considered for setup. Set ifaceNamesOfImplsToSetup = new ArraySet<>(ifaceNames); ifaceNamesOfImplsToSetup.removeAll(ifaceNamesOfImplsAlreadySetup); for (String ifaceName : ifaceNamesOfImplsToTeardown) { WifiScannerImpl impl = mScannerImpls.remove(ifaceName); if (impl == null) continue; // should never happen impl.cleanup(); Log.i(TAG, "Removed an impl for " + ifaceName); } for (String ifaceName : ifaceNamesOfImplsToSetup) { WifiScannerImpl impl = mScannerImplFactory.create(mContext, mLooper, mClock, ifaceName); if (impl == null) { loge("Failed to create scanner impl for " + ifaceName); continue; } // 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); Log.i(TAG, "Created a new impl for " + ifaceName); } else { Log.i(TAG, "All the channels on the new impl for iface " + ifaceName + " are already satisfied by an existing impl. Skipping.."); impl.cleanup(); // cleanup the impl before discarding. } } } private void teardownScannerImpls() { for (Map.Entry entry : mScannerImpls.entrySet()) { WifiScannerImpl impl = entry.getValue(); String ifaceName = entry.getKey(); if (impl == null) continue; // should never happen impl.cleanup(); Log.i(TAG, "Removed an impl for " + ifaceName); } mScannerImpls.clear(); } /** * Provide a way for unit tests to set valid log object in the WifiHandler * @param log WifiLog object to assign to the clientHandler */ @VisibleForTesting public void setWifiHandlerLogForTest(WifiLog log) { mClientHandler.setWifiLog(log); } private WorkSource computeWorkSource(ClientInfo ci, WorkSource requestedWorkSource) { if (requestedWorkSource != null && !requestedWorkSource.isEmpty()) { return requestedWorkSource.withoutNames(); } if (ci.getUid() > 0) { return new WorkSource(ci.getUid()); } // We can't construct a sensible WorkSource because the one supplied to us was empty and // we don't have a valid UID for the given client. loge("Unable to compute workSource for client: " + ci + ", requested: " + requestedWorkSource); return new WorkSource(); } private class RequestInfo { final ClientInfo clientInfo; final int handlerId; final WorkSource workSource; final T settings; RequestInfo(ClientInfo clientInfo, int handlerId, WorkSource requestedWorkSource, T settings) { this.clientInfo = clientInfo; this.handlerId = handlerId; this.settings = settings; this.workSource = computeWorkSource(clientInfo, requestedWorkSource); } void reportEvent(int what, int arg1, Object obj) { clientInfo.reportEvent(what, arg1, handlerId, obj); } } private class RequestList extends ArrayList> { void addRequest(ClientInfo ci, int handler, WorkSource reqworkSource, T settings) { add(new RequestInfo(ci, handler, reqworkSource, settings)); } T removeRequest(ClientInfo ci, int handlerId) { T removed = null; Iterator> iter = iterator(); while (iter.hasNext()) { RequestInfo entry = iter.next(); if (entry.clientInfo == ci && entry.handlerId == handlerId) { removed = entry.settings; iter.remove(); } } return removed; } Collection getAllSettings() { ArrayList settingsList = new ArrayList<>(); Iterator> iter = iterator(); while (iter.hasNext()) { RequestInfo entry = iter.next(); settingsList.add(entry.settings); } return settingsList; } Collection getAllSettingsForClient(ClientInfo ci) { ArrayList settingsList = new ArrayList<>(); Iterator> iter = iterator(); while (iter.hasNext()) { RequestInfo entry = iter.next(); if (entry.clientInfo == ci) { settingsList.add(entry.settings); } } return settingsList; } void removeAllForClient(ClientInfo ci) { Iterator> iter = iterator(); while (iter.hasNext()) { RequestInfo entry = iter.next(); if (entry.clientInfo == ci) { iter.remove(); } } } WorkSource createMergedWorkSource() { WorkSource mergedSource = new WorkSource(); for (RequestInfo entry : this) { mergedSource.add(entry.workSource); } return mergedSource; } } /** * State machine that holds the state of single scans. Scans should only be active in the * ScanningState. The pending scans and active scans maps are swapped when entering * ScanningState. Any requests queued while scanning will be placed in the pending queue and * executed after transitioning back to IdleState. */ class WifiSingleScanStateMachine extends StateMachine { /** * Maximum age of results that we return from our cache via * {@link WifiScanner#getScanResults()}. * This is currently set to 3 minutes to restore parity with the wpa_supplicant's scan * result cache expiration policy. (See b/62253332 for details) */ @VisibleForTesting public static final int CACHED_SCAN_RESULTS_MAX_AGE_IN_MILLIS = 180 * 1000; /** * Alarm Tag to use for the delayed indication of emergency scan end. */ @VisibleForTesting public static final String EMERGENCY_SCAN_END_INDICATION_ALARM_TAG = TAG + "EmergencyScanEnd"; /** * Alarm timeout to use for the delayed indication of emergency scan end. */ private static final int EMERGENCY_SCAN_END_INDICATION_DELAY_MILLIS = 15_000; /** * Alarm listener to use for the delayed indication of emergency scan end. */ private final AlarmManager.OnAlarmListener mEmergencyScanEndIndicationListener = () -> mWifiManager.setEmergencyScanRequestInProgress(false); private final DefaultState mDefaultState = new DefaultState(); private final DriverStartedState mDriverStartedState = new DriverStartedState(); private final IdleState mIdleState = new IdleState(); private final ScanningState mScanningState = new ScanningState(); private WifiNative.ScanSettings mActiveScanSettings = null; private RequestList mActiveScans = new RequestList<>(); private RequestList mPendingScans = new RequestList<>(); // Scan results cached from the last full single scan request. private final List mCachedScanResults = new ArrayList<>(); // Tracks scan requests across multiple scanner impls. private final ScannerImplsTracker mScannerImplsTracker; WifiSingleScanStateMachine(Looper looper) { super("WifiSingleScanStateMachine", looper); mScannerImplsTracker = new ScannerImplsTracker(); setLogRecSize(128); setLogOnlyTransitions(false); // CHECKSTYLE:OFF IndentationCheck addState(mDefaultState); addState(mDriverStartedState, mDefaultState); addState(mIdleState, mDriverStartedState); addState(mScanningState, mDriverStartedState); // CHECKSTYLE:ON IndentationCheck setInitialState(mDefaultState); } /** * Tracks a single scan request across all the available scanner impls. * * a) Initiates the scan using the same ScanSettings across all the available impls. * b) Waits for all the impls to report the status of the scan request (success or failure). * c) Calculates a consolidated scan status and sends the results if successful. * Note: If there are failures on some of the scanner impls, we ignore them since we will * get some scan results from the other successful impls. We don't declare total scan * failures, unless all the scanner impls fail. */ private final class ScannerImplsTracker { private final class ScanEventHandler implements WifiNative.ScanEventHandler { private final String mImplIfaceName; ScanEventHandler(@NonNull String implIfaceName) { mImplIfaceName = implIfaceName; } /** * Called to indicate a change in state for the current scan. * Will dispatch a corresponding event to the state machine */ @Override public void onScanStatus(int event) { if (DBG) { localLog("onScanStatus event received, event=" + event + ", iface=" + mImplIfaceName); } switch (event) { case WifiNative.WIFI_SCAN_RESULTS_AVAILABLE: case WifiNative.WIFI_SCAN_THRESHOLD_NUM_SCANS: case WifiNative.WIFI_SCAN_THRESHOLD_PERCENT: reportScanStatusForImpl(mImplIfaceName, STATUS_SUCCEEDED); break; case WifiNative.WIFI_SCAN_FAILED: reportScanStatusForImpl(mImplIfaceName, STATUS_FAILED); break; default: Log.e(TAG, "Unknown scan status event: " + event); break; } } /** * Called for each full scan result if requested */ @Override public void onFullScanResult(ScanResult fullScanResult, int bucketsScanned) { if (DBG) localLog("onFullScanResult received on iface " + mImplIfaceName); reportFullScanResultForImpl(mImplIfaceName, fullScanResult, bucketsScanned); } @Override public void onScanPaused(ScanData[] scanData) { // should not happen for single scan Log.e(TAG, "Got scan paused for single scan"); } @Override public void onScanRestarted() { // should not happen for single scan Log.e(TAG, "Got scan restarted for single scan"); } } private static final int STATUS_PENDING = 0; private static final int STATUS_SUCCEEDED = 1; private static final int STATUS_FAILED = 2; // Tracks scan status per impl. Map mStatusPerImpl = new ArrayMap<>(); /** * Triggers a new scan on all the available scanner impls. * @return true if the scan succeeded on any of the impl, false otherwise. */ public boolean startSingleScan(WifiNative.ScanSettings scanSettings) { mStatusPerImpl.clear(); boolean anySuccess = false; for (Map.Entry entry : mScannerImpls.entrySet()) { String ifaceName = entry.getKey(); WifiScannerImpl impl = entry.getValue(); boolean success = impl.startSingleScan( scanSettings, new ScanEventHandler(ifaceName)); if (!success) { Log.e(TAG, "Failed to start single scan on " + ifaceName); mStatusPerImpl.put(ifaceName, STATUS_FAILED); continue; } mStatusPerImpl.put(ifaceName, STATUS_PENDING); anySuccess = true; } return anySuccess; } /** * Returns the latest scan results from all the available scanner impls. * @return Consolidated list of scan results from all the impl. */ public @Nullable ScanData getLatestSingleScanResults() { ScanData consolidatedScanData = null; for (WifiScannerImpl impl : mScannerImpls.values()) { Integer ifaceStatus = mStatusPerImpl.get(impl.getIfaceName()); if (ifaceStatus == null || ifaceStatus != STATUS_SUCCEEDED) { continue; } ScanData scanData = impl.getLatestSingleScanResults(); if (consolidatedScanData == null) { consolidatedScanData = new ScanData(scanData); } else { consolidatedScanData.addResults(scanData.getResults()); } } return consolidatedScanData; } private void reportFullScanResultForImpl(@NonNull String implIfaceName, ScanResult fullScanResult, int bucketsScanned) { Integer status = mStatusPerImpl.get(implIfaceName); if (status != null && status == STATUS_PENDING) { sendMessage(CMD_FULL_SCAN_RESULTS, 0, bucketsScanned, fullScanResult); } } private int getConsolidatedStatus() { boolean anyPending = mStatusPerImpl.values().stream() .anyMatch(status -> status == STATUS_PENDING); // at-least one impl status is still pending. if (anyPending) return STATUS_PENDING; boolean anySuccess = mStatusPerImpl.values().stream() .anyMatch(status -> status == STATUS_SUCCEEDED); // one success is good enough to declare consolidated success. if (anySuccess) { return STATUS_SUCCEEDED; } else { // all failed. return STATUS_FAILED; } } private void reportScanStatusForImpl(@NonNull String implIfaceName, int newStatus) { Integer currentStatus = mStatusPerImpl.get(implIfaceName); if (currentStatus != null && currentStatus == STATUS_PENDING) { mStatusPerImpl.put(implIfaceName, newStatus); } // Now check if all the scanner impls scan status is available. int consolidatedStatus = getConsolidatedStatus(); if (consolidatedStatus == STATUS_SUCCEEDED) { sendMessage(CMD_SCAN_RESULTS_AVAILABLE); } else if (consolidatedStatus == STATUS_FAILED) { sendMessage(CMD_SCAN_FAILED); } } } /** * Helper method to handle the scan start message. */ private void handleScanStartMessage(ClientInfo ci, Message msg) { int handler = msg.arg2; Bundle scanParams = (Bundle) msg.obj; if (scanParams == null) { logCallback("singleScanInvalidRequest", ci, handler, "null params"); replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "params null"); return; } ScanSettings scanSettings = null; WorkSource workSource = null; try { scanSettings = scanParams.getParcelable( WifiScanner.SCAN_PARAMS_SCAN_SETTINGS_KEY); workSource = scanParams.getParcelable( WifiScanner.SCAN_PARAMS_WORK_SOURCE_KEY); } catch (BadParcelableException e) { Log.wtf(TAG, "Failed to get parcelable params", e); logCallback("singleScanInvalidRequest", ci, handler, "bad parcel params"); replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "bad parcel params"); return; } if (validateScanRequest(ci, handler, scanSettings)) { if (getCurrentState() == mDefaultState && !scanSettings.ignoreLocationSettings) { // Reject regular scan requests if scanning is disabled. replyFailed(msg, WifiScanner.REASON_UNSPECIFIED, "not available"); return; } mWifiMetrics.incrementOneshotScanCount(); if ((scanSettings.band & WifiScanner.WIFI_BAND_5_GHZ_DFS_ONLY) != 0) { mWifiMetrics.incrementOneshotScanWithDfsCount(); } logScanRequest("addSingleScanRequest", ci, handler, workSource, scanSettings, null); replySucceeded(msg); if (scanSettings.ignoreLocationSettings) { // Inform wifi manager that an emergency scan is in progress (regardless of // whether scanning is currently enabled or not). This ensures that // the wifi chip remains on for the duration of this scan. mWifiManager.setEmergencyScanRequestInProgress(true); } if (getCurrentState() == mScanningState) { // 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 (activeScanSatisfies(scanSettings)) { mActiveScans.addRequest(ci, handler, workSource, scanSettings); } else { mPendingScans.addRequest(ci, handler, workSource, scanSettings); } } else if (getCurrentState() == mIdleState) { // 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. mPendingScans.addRequest(ci, handler, workSource, scanSettings); tryToStartNewScan(); } else if (getCurrentState() == mDefaultState) { // If scanning is disabled and the request is for emergency purposes // (checked above), add to pending list. this scan will be scheduled when // transitioning to IdleState when wifi manager enables scanning as a part of // processing WifiManager.setEmergencyScanRequestInProgress(true) mPendingScans.addRequest(ci, handler, workSource, scanSettings); } } else { logCallback("singleScanInvalidRequest", ci, handler, "bad request"); replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "bad request"); mWifiMetrics.incrementScanReturnEntry( WifiMetricsProto.WifiLog.SCAN_FAILURE_INVALID_CONFIGURATION, 1); } } class DefaultState extends State { @Override public void enter() { mActiveScans.clear(); mPendingScans.clear(); } @Override public boolean processMessage(Message msg) { ClientInfo ci = mClients.get(msg.replyTo); switch (msg.what) { case WifiScanner.CMD_ENABLE: if (mScannerImpls.isEmpty()) { loge("Failed to start single scan state machine because scanner impl" + " is null"); return HANDLED; } transitionTo(mIdleState); return HANDLED; case WifiScanner.CMD_DISABLE: transitionTo(mDefaultState); return HANDLED; case WifiScanner.CMD_START_SINGLE_SCAN: handleScanStartMessage(ci, msg); return HANDLED; case WifiScanner.CMD_STOP_SINGLE_SCAN: removeSingleScanRequest(ci, msg.arg2); return HANDLED; case CMD_SCAN_RESULTS_AVAILABLE: if (DBG) localLog("ignored scan results available event"); return HANDLED; case CMD_FULL_SCAN_RESULTS: if (DBG) localLog("ignored full scan result event"); return HANDLED; case WifiScanner.CMD_GET_SINGLE_SCAN_RESULTS: msg.obj = new WifiScanner.ParcelableScanResults( filterCachedScanResultsByAge()); replySucceeded(msg); return HANDLED; default: return NOT_HANDLED; } } /** * Filter out any scan results that are older than * {@link #CACHED_SCAN_RESULTS_MAX_AGE_IN_MILLIS}. * * @return Filtered list of scan results. */ private ScanResult[] filterCachedScanResultsByAge() { // Using ScanResult.timestamp here to ensure that we use the same fields as // WificondScannerImpl for filtering stale results. long currentTimeInMillis = mClock.getElapsedSinceBootMillis(); return mCachedScanResults.stream() .filter(scanResult -> ((currentTimeInMillis - (scanResult.timestamp / 1000)) < CACHED_SCAN_RESULTS_MAX_AGE_IN_MILLIS)) .toArray(ScanResult[]::new); } } /** * State representing when the driver is running. This state is not meant to be transitioned * directly, but is instead intended as a parent state of ScanningState and IdleState * to hold common functionality and handle cleaning up scans when the driver is shut down. */ class DriverStartedState extends State { @Override public void exit() { // clear scan results when scan mode is not active mCachedScanResults.clear(); mWifiMetrics.incrementScanReturnEntry( WifiMetricsProto.WifiLog.SCAN_FAILURE_INTERRUPTED, mPendingScans.size()); sendOpFailedToAllAndClear(mPendingScans, WifiScanner.REASON_UNSPECIFIED, "Scan was interrupted"); } @Override public boolean processMessage(Message msg) { switch (msg.what) { case WifiScanner.CMD_ENABLE: // Ignore if we're already in driver loaded state. return HANDLED; default: return NOT_HANDLED; } } } class IdleState extends State { @Override public void enter() { tryToStartNewScan(); } @Override public boolean processMessage(Message msg) { return NOT_HANDLED; } } class ScanningState extends State { private WorkSource mScanWorkSource; @Override public void enter() { mScanWorkSource = mActiveScans.createMergedWorkSource(); mBatteryStats.reportWifiScanStartedFromSource(mScanWorkSource); Pair uidsAndTags = WorkSourceUtil.getUidsAndTagsForWs(mScanWorkSource); WifiStatsLog.write(WifiStatsLog.WIFI_SCAN_STATE_CHANGED, uidsAndTags.first, uidsAndTags.second, WifiStatsLog.WIFI_SCAN_STATE_CHANGED__STATE__ON); } @Override public void exit() { mActiveScanSettings = null; mBatteryStats.reportWifiScanStoppedFromSource(mScanWorkSource); Pair uidsAndTags = WorkSourceUtil.getUidsAndTagsForWs(mScanWorkSource); WifiStatsLog.write(WifiStatsLog.WIFI_SCAN_STATE_CHANGED, uidsAndTags.first, uidsAndTags.second, WifiStatsLog.WIFI_SCAN_STATE_CHANGED__STATE__OFF); // if any scans are still active (never got results available then indicate failure) mWifiMetrics.incrementScanReturnEntry( WifiMetricsProto.WifiLog.SCAN_UNKNOWN, mActiveScans.size()); sendOpFailedToAllAndClear(mActiveScans, WifiScanner.REASON_UNSPECIFIED, "Scan was interrupted"); } @Override public boolean processMessage(Message msg) { switch (msg.what) { case CMD_SCAN_RESULTS_AVAILABLE: ScanData latestScanResults = mScannerImplsTracker.getLatestSingleScanResults(); if (latestScanResults != null) { handleScanResults(latestScanResults); } else { Log.e(TAG, "latest scan results null unexpectedly"); } transitionTo(mIdleState); return HANDLED; case CMD_FULL_SCAN_RESULTS: reportFullScanResult((ScanResult) msg.obj, /* bucketsScanned */ msg.arg2); return HANDLED; case CMD_SCAN_FAILED: mWifiMetrics.incrementScanReturnEntry( WifiMetricsProto.WifiLog.SCAN_UNKNOWN, mActiveScans.size()); mWifiMetrics.getScanMetrics().logScanFailed( WifiMetrics.ScanMetrics.SCAN_TYPE_SINGLE); sendOpFailedToAllAndClear(mActiveScans, WifiScanner.REASON_UNSPECIFIED, "Scan failed"); transitionTo(mIdleState); return HANDLED; default: return NOT_HANDLED; } } } boolean validateScanType(@WifiAnnotations.ScanType int type) { return (type == WifiScanner.SCAN_TYPE_LOW_LATENCY || type == WifiScanner.SCAN_TYPE_LOW_POWER || type == WifiScanner.SCAN_TYPE_HIGH_ACCURACY); } boolean validateScanRequest(ClientInfo ci, int handler, ScanSettings settings) { if (ci == null) { Log.d(TAG, "Failing single scan request ClientInfo not found " + handler); return false; } if (settings.band == WifiScanner.WIFI_BAND_UNSPECIFIED) { if (settings.channels == null || settings.channels.length == 0) { Log.d(TAG, "Failing single scan because channel list was empty"); return false; } } if (!validateScanType(settings.type)) { Log.e(TAG, "Invalid scan type " + settings.type); return false; } if (mContext.checkPermission( Manifest.permission.NETWORK_STACK, UNKNOWN_PID, ci.getUid()) == PERMISSION_DENIED) { if (!ArrayUtils.isEmpty(settings.hiddenNetworks)) { Log.e(TAG, "Failing single scan because app " + ci.getUid() + " does not have permission to set hidden networks"); return false; } if (settings.type != WifiScanner.SCAN_TYPE_LOW_LATENCY) { Log.e(TAG, "Failing single scan because app " + ci.getUid() + " does not have permission to set type"); return false; } } return true; } // We can coalesce a LOW_POWER/LOW_LATENCY scan request into an ongoing HIGH_ACCURACY // scan request. But, we can't coalesce a HIGH_ACCURACY scan request into an ongoing // LOW_POWER/LOW_LATENCY scan request. boolean activeScanTypeSatisfies(int requestScanType) { switch(mActiveScanSettings.scanType) { case WifiScanner.SCAN_TYPE_LOW_LATENCY: case WifiScanner.SCAN_TYPE_LOW_POWER: return requestScanType != WifiScanner.SCAN_TYPE_HIGH_ACCURACY; case WifiScanner.SCAN_TYPE_HIGH_ACCURACY: return true; default: // This should never happen because we've validated the incoming type in // |validateScanType|. throw new IllegalArgumentException("Invalid scan type " + mActiveScanSettings.scanType); } } // If there is a HIGH_ACCURACY scan request among the requests being merged, the merged // scan type should be HIGH_ACCURACY. int mergeScanTypes(int existingScanType, int newScanType) { switch(existingScanType) { case WifiScanner.SCAN_TYPE_LOW_LATENCY: case WifiScanner.SCAN_TYPE_LOW_POWER: return newScanType; case WifiScanner.SCAN_TYPE_HIGH_ACCURACY: return existingScanType; default: // This should never happen because we've validated the incoming type in // |validateScanType|. throw new IllegalArgumentException("Invalid scan type " + existingScanType); } } private boolean mergeRnrSetting(boolean enable6GhzRnr, ScanSettings scanSettings) { if (!SdkLevel.isAtLeastS()) { return false; } if (enable6GhzRnr) { return true; } int rnrSetting = scanSettings.getRnrSetting(); if (rnrSetting == WifiScanner.WIFI_RNR_ENABLED) { return true; } if (rnrSetting == WifiScanner.WIFI_RNR_ENABLED_IF_WIFI_BAND_6_GHZ_SCANNED) { return ChannelHelper.is6GhzBandIncluded(scanSettings.band); } return false; } boolean activeScanSatisfies(ScanSettings settings) { if (mActiveScanSettings == null) { return false; } if (!activeScanTypeSatisfies(settings.type)) { return false; } // there is always one bucket for a single scan WifiNative.BucketSettings activeBucket = mActiveScanSettings.buckets[0]; // validate that all requested channels are being scanned ChannelCollection activeChannels = mChannelHelper.createChannelCollection(); activeChannels.addChannels(activeBucket); if (!activeChannels.containsSettings(settings)) { return false; } // if the request is for a full scan, but there is no ongoing full scan if ((settings.reportEvents & WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT) != 0 && (activeBucket.report_events & WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT) == 0) { return false; } if (!ArrayUtils.isEmpty(settings.hiddenNetworks)) { if (ArrayUtils.isEmpty(mActiveScanSettings.hiddenNetworks)) { return false; } List activeHiddenNetworks = new ArrayList<>(); for (WifiNative.HiddenNetwork hiddenNetwork : mActiveScanSettings.hiddenNetworks) { activeHiddenNetworks.add(hiddenNetwork); } for (ScanSettings.HiddenNetwork hiddenNetwork : settings.hiddenNetworks) { WifiNative.HiddenNetwork nativeHiddenNetwork = new WifiNative.HiddenNetwork(); nativeHiddenNetwork.ssid = hiddenNetwork.ssid; if (!activeHiddenNetworks.contains(nativeHiddenNetwork)) { return false; } } } return true; } void removeSingleScanRequest(ClientInfo ci, int handler) { if (ci != null) { logScanRequest("removeSingleScanRequest", ci, handler, null, null, null); mPendingScans.removeRequest(ci, handler); mActiveScans.removeRequest(ci, handler); } } void removeSingleScanRequests(ClientInfo ci) { if (ci != null) { logScanRequest("removeSingleScanRequests", ci, -1, null, null, null); mPendingScans.removeAllForClient(ci); mActiveScans.removeAllForClient(ci); } } void tryToStartNewScan() { if (mPendingScans.size() == 0) { // no pending requests return; } mChannelHelper.updateChannels(); // TODO move merging logic to a scheduler WifiNative.ScanSettings settings = new WifiNative.ScanSettings(); settings.num_buckets = 1; WifiNative.BucketSettings bucketSettings = new WifiNative.BucketSettings(); bucketSettings.bucket = 0; bucketSettings.period_ms = 0; bucketSettings.report_events = WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN; ChannelCollection channels = mChannelHelper.createChannelCollection(); List hiddenNetworkList = new ArrayList<>(); for (RequestInfo entry : mPendingScans) { settings.scanType = mergeScanTypes(settings.scanType, entry.settings.type); settings.enable6GhzRnr = mergeRnrSetting(settings.enable6GhzRnr, entry.settings); channels.addChannels(entry.settings); for (ScanSettings.HiddenNetwork srcNetwork : entry.settings.hiddenNetworks) { WifiNative.HiddenNetwork hiddenNetwork = new WifiNative.HiddenNetwork(); hiddenNetwork.ssid = srcNetwork.ssid; hiddenNetworkList.add(hiddenNetwork); } if ((entry.settings.reportEvents & WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT) != 0) { bucketSettings.report_events |= WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT; } if (entry.clientInfo != null) { mWifiMetrics.getScanMetrics().setClientUid(entry.clientInfo.mUid); } mWifiMetrics.getScanMetrics().setWorkSource(entry.workSource); } if (hiddenNetworkList.size() > 0) { settings.hiddenNetworks = new WifiNative.HiddenNetwork[hiddenNetworkList.size()]; int numHiddenNetworks = 0; for (WifiNative.HiddenNetwork hiddenNetwork : hiddenNetworkList) { settings.hiddenNetworks[numHiddenNetworks++] = hiddenNetwork; } } channels.fillBucketSettings(bucketSettings, Integer.MAX_VALUE); settings.buckets = new WifiNative.BucketSettings[] {bucketSettings}; if (mScannerImplsTracker.startSingleScan(settings)) { mWifiMetrics.getScanMetrics().logScanStarted( WifiMetrics.ScanMetrics.SCAN_TYPE_SINGLE); // store the active scan settings mActiveScanSettings = settings; // swap pending and active scan requests RequestList tmp = mActiveScans; mActiveScans = mPendingScans; mPendingScans = tmp; // make sure that the pending list is clear mPendingScans.clear(); transitionTo(mScanningState); } else { mWifiMetrics.incrementScanReturnEntry( WifiMetricsProto.WifiLog.SCAN_UNKNOWN, mPendingScans.size()); mWifiMetrics.getScanMetrics().logScanFailedToStart( WifiMetrics.ScanMetrics.SCAN_TYPE_SINGLE); // notify and cancel failed scans sendOpFailedToAllAndClear(mPendingScans, WifiScanner.REASON_UNSPECIFIED, "Failed to start single scan"); } } void sendOpFailedToAllAndClear(RequestList clientHandlers, int reason, String description) { for (RequestInfo entry : clientHandlers) { logCallback("singleScanFailed", entry.clientInfo, entry.handlerId, "reason=" + reason + ", " + description); entry.reportEvent(WifiScanner.CMD_OP_FAILED, 0, new WifiScanner.OperationResult(reason, description)); } clientHandlers.clear(); } void reportFullScanResult(@NonNull ScanResult result, int bucketsScanned) { for (RequestInfo entry : mActiveScans) { if (ScanScheduleUtil.shouldReportFullScanResultForSettings(mChannelHelper, result, bucketsScanned, entry.settings, -1)) { entry.reportEvent(WifiScanner.CMD_FULL_SCAN_RESULT, 0, result); } } for (RequestInfo entry : mSingleScanListeners) { entry.reportEvent(WifiScanner.CMD_FULL_SCAN_RESULT, 0, result); } } void reportScanResults(@NonNull ScanData results) { if (results != null && results.getResults() != null) { if (results.getResults().length > 0) { mWifiMetrics.incrementNonEmptyScanResultCount(); } else { mWifiMetrics.incrementEmptyScanResultCount(); } } ScanData[] allResults = new ScanData[] {results}; for (RequestInfo entry : mActiveScans) { ScanData[] resultsToDeliver = ScanScheduleUtil.filterResultsForSettings( mChannelHelper, allResults, entry.settings, -1); WifiScanner.ParcelableScanData parcelableResultsToDeliver = new WifiScanner.ParcelableScanData(resultsToDeliver); logCallback("singleScanResults", entry.clientInfo, entry.handlerId, describeForLog(resultsToDeliver)); entry.reportEvent(WifiScanner.CMD_SCAN_RESULT, 0, parcelableResultsToDeliver); // make sure the handler is removed entry.reportEvent(WifiScanner.CMD_SINGLE_SCAN_COMPLETED, 0, null); } WifiScanner.ParcelableScanData parcelableAllResults = new WifiScanner.ParcelableScanData(allResults); for (RequestInfo entry : mSingleScanListeners) { logCallback("singleScanResults", entry.clientInfo, entry.handlerId, describeForLog(allResults)); entry.reportEvent(WifiScanner.CMD_SCAN_RESULT, 0, parcelableAllResults); } } void handleScanResults(@NonNull ScanData results) { mWifiMetrics.getScanMetrics().logScanSucceeded( WifiMetrics.ScanMetrics.SCAN_TYPE_SINGLE, results.getResults().length); mWifiMetrics.incrementScanReturnEntry( WifiMetricsProto.WifiLog.SCAN_SUCCESS, mActiveScans.size()); reportScanResults(results); // Cache full band (with DFS or not) scan results. if (WifiScanner.isFullBandScan(results.getScannedBandsInternal(), true)) { mCachedScanResults.clear(); mCachedScanResults.addAll(Arrays.asList(results.getResults())); } if (mActiveScans.stream().anyMatch(rI -> rI.settings.ignoreLocationSettings)) { // We were processing an emergency scan, post an alarm to inform WifiManager the // end of that scan processing. If another scan is processed before the alarm fires, // this timer is restarted (AlarmManager.set() using the same listener resets the // timer). This delayed indication of emergency scan end prevents // quick wifi toggle on/off if there is a burst of emergency scans when wifi is off. mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, mClock.getElapsedSinceBootMillis() + EMERGENCY_SCAN_END_INDICATION_DELAY_MILLIS, EMERGENCY_SCAN_END_INDICATION_ALARM_TAG, mEmergencyScanEndIndicationListener, getHandler()); } mActiveScans.clear(); } List getCachedScanResultsAsList() { return mCachedScanResults; } } // TODO(b/71855918): Remove this bg scan state machine and its dependencies. // Note: bgscan will not support multiple scanner impls (will pick any). class WifiBackgroundScanStateMachine extends StateMachine { private final DefaultState mDefaultState = new DefaultState(); private final StartedState mStartedState = new StartedState(); private final PausedState mPausedState = new PausedState(); private final RequestList mActiveBackgroundScans = new RequestList<>(); private WifiScannerImpl mScannerImpl; WifiBackgroundScanStateMachine(Looper looper) { super("WifiBackgroundScanStateMachine", looper); setLogRecSize(512); setLogOnlyTransitions(false); // CHECKSTYLE:OFF IndentationCheck addState(mDefaultState); addState(mStartedState, mDefaultState); addState(mPausedState, mDefaultState); // CHECKSTYLE:ON IndentationCheck setInitialState(mDefaultState); } public Collection getBackgroundScanSettings(ClientInfo ci) { return mActiveBackgroundScans.getAllSettingsForClient(ci); } public void removeBackgroundScanSettings(ClientInfo ci) { mActiveBackgroundScans.removeAllForClient(ci); updateSchedule(); } private final class ScanEventHandler implements WifiNative.ScanEventHandler { private final String mImplIfaceName; ScanEventHandler(@NonNull String implIfaceName) { mImplIfaceName = implIfaceName; } @Override public void onScanStatus(int event) { if (DBG) localLog("onScanStatus event received, event=" + event); switch (event) { case WifiNative.WIFI_SCAN_RESULTS_AVAILABLE: case WifiNative.WIFI_SCAN_THRESHOLD_NUM_SCANS: case WifiNative.WIFI_SCAN_THRESHOLD_PERCENT: sendMessage(CMD_SCAN_RESULTS_AVAILABLE); break; case WifiNative.WIFI_SCAN_FAILED: sendMessage(CMD_SCAN_FAILED); break; default: Log.e(TAG, "Unknown scan status event: " + event); break; } } @Override public void onFullScanResult(ScanResult fullScanResult, int bucketsScanned) { if (DBG) localLog("onFullScanResult received"); sendMessage(CMD_FULL_SCAN_RESULTS, 0, bucketsScanned, fullScanResult); } @Override public void onScanPaused(ScanData[] scanData) { if (DBG) localLog("onScanPaused received"); sendMessage(CMD_SCAN_PAUSED, scanData); } @Override public void onScanRestarted() { if (DBG) localLog("onScanRestarted received"); sendMessage(CMD_SCAN_RESTARTED); } } class DefaultState extends State { @Override public void enter() { if (DBG) localLog("DefaultState"); mActiveBackgroundScans.clear(); } @Override public boolean processMessage(Message msg) { switch (msg.what) { case WifiScanner.CMD_ENABLE: if (mScannerImpls.isEmpty()) { loge("Failed to start bgscan scan state machine because scanner impl" + " is null"); return HANDLED; } // Pick any impl available and stick to it until disable. mScannerImpl = mScannerImpls.entrySet().iterator().next().getValue(); mChannelHelper = mScannerImpl.getChannelHelper(); mBackgroundScheduler = new BackgroundScanScheduler(mChannelHelper); WifiNative.ScanCapabilities capabilities = new WifiNative.ScanCapabilities(); if (!mScannerImpl.getScanCapabilities(capabilities)) { loge("could not get scan capabilities"); return HANDLED; } if (capabilities.max_scan_buckets <= 0) { loge("invalid max buckets in scan capabilities " + capabilities.max_scan_buckets); return HANDLED; } mBackgroundScheduler.setMaxBuckets(capabilities.max_scan_buckets); mBackgroundScheduler.setMaxApPerScan(capabilities.max_ap_cache_per_scan); Log.i(TAG, "wifi driver loaded with scan capabilities: " + "max buckets=" + capabilities.max_scan_buckets); transitionTo(mStartedState); return HANDLED; case WifiScanner.CMD_DISABLE: Log.i(TAG, "wifi driver unloaded"); transitionTo(mDefaultState); break; case WifiScanner.CMD_START_BACKGROUND_SCAN: case WifiScanner.CMD_STOP_BACKGROUND_SCAN: case WifiScanner.CMD_START_SINGLE_SCAN: case WifiScanner.CMD_STOP_SINGLE_SCAN: case WifiScanner.CMD_GET_SCAN_RESULTS: replyFailed(msg, WifiScanner.REASON_UNSPECIFIED, "not available"); break; case CMD_SCAN_RESULTS_AVAILABLE: if (DBG) localLog("ignored scan results available event"); break; case CMD_FULL_SCAN_RESULTS: if (DBG) localLog("ignored full scan result event"); break; default: break; } return HANDLED; } } class StartedState extends State { @Override public void enter() { if (DBG) localLog("StartedState"); if (mScannerImpl == null) { // should never happen Log.wtf(TAG, "Scanner impl unexpectedly null"); transitionTo(mDefaultState); } } @Override public void exit() { sendBackgroundScanFailedToAllAndClear( WifiScanner.REASON_UNSPECIFIED, "Scan was interrupted"); mScannerImpl = null; // reset impl } @Override public boolean processMessage(Message msg) { ClientInfo ci = mClients.get(msg.replyTo); switch (msg.what) { case WifiScanner.CMD_ENABLE: Log.e(TAG, "wifi driver loaded received while already loaded"); // Ignore if we're already in driver loaded state. return HANDLED; case WifiScanner.CMD_DISABLE: return NOT_HANDLED; case WifiScanner.CMD_START_BACKGROUND_SCAN: { mWifiMetrics.incrementBackgroundScanCount(); Bundle scanParams = (Bundle) msg.obj; if (scanParams == null) { replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "params null"); return HANDLED; } ScanSettings scanSettings = null; WorkSource workSource = null; try { scanSettings = scanParams.getParcelable( WifiScanner.SCAN_PARAMS_SCAN_SETTINGS_KEY); workSource = scanParams.getParcelable( WifiScanner.SCAN_PARAMS_WORK_SOURCE_KEY); } catch (BadParcelableException e) { Log.e(TAG, "Failed to get parcelable params", e); replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "bad parcel params"); return HANDLED; } if (addBackgroundScanRequest(ci, msg.arg2, scanSettings, workSource)) { replySucceeded(msg); } else { replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "bad request"); } break; } case WifiScanner.CMD_STOP_BACKGROUND_SCAN: removeBackgroundScanRequest(ci, msg.arg2); break; case WifiScanner.CMD_GET_SCAN_RESULTS: reportScanResults(mScannerImpl.getLatestBatchedScanResults(true)); replySucceeded(msg); break; case CMD_SCAN_RESULTS_AVAILABLE: WifiScanner.ScanData[] results = mScannerImpl.getLatestBatchedScanResults( true); mWifiMetrics.getScanMetrics().logScanSucceeded( WifiMetrics.ScanMetrics.SCAN_TYPE_BACKGROUND, results != null ? results.length : 0); reportScanResults(results); break; case CMD_FULL_SCAN_RESULTS: reportFullScanResult((ScanResult) msg.obj, /* bucketsScanned */ msg.arg2); break; case CMD_SCAN_PAUSED: reportScanResults((ScanData[]) msg.obj); transitionTo(mPausedState); break; case CMD_SCAN_FAILED: mWifiMetrics.getScanMetrics().logScanFailed( WifiMetrics.ScanMetrics.SCAN_TYPE_BACKGROUND); Log.e(TAG, "WifiScanner background scan gave CMD_SCAN_FAILED"); sendBackgroundScanFailedToAllAndClear( WifiScanner.REASON_UNSPECIFIED, "Background Scan failed"); break; default: return NOT_HANDLED; } return HANDLED; } } class PausedState extends State { @Override public void enter() { if (DBG) localLog("PausedState"); } @Override public boolean processMessage(Message msg) { switch (msg.what) { case CMD_SCAN_RESTARTED: transitionTo(mStartedState); break; default: deferMessage(msg); break; } return HANDLED; } } private boolean addBackgroundScanRequest(ClientInfo ci, int handler, ScanSettings settings, WorkSource workSource) { if (ci == null) { Log.d(TAG, "Failing scan request ClientInfo not found " + handler); return false; } if (settings.periodInMs < WifiScanner.MIN_SCAN_PERIOD_MS) { loge("Failing scan request because periodInMs is " + settings.periodInMs + ", min scan period is: " + WifiScanner.MIN_SCAN_PERIOD_MS); return false; } if (settings.band == WifiScanner.WIFI_BAND_UNSPECIFIED && settings.channels == null) { loge("Channels was null with unspecified band"); return false; } if (settings.band == WifiScanner.WIFI_BAND_UNSPECIFIED && settings.channels.length == 0) { loge("No channels specified"); return false; } int minSupportedPeriodMs = mChannelHelper.estimateScanDuration(settings); if (settings.periodInMs < minSupportedPeriodMs) { loge("Failing scan request because minSupportedPeriodMs is " + minSupportedPeriodMs + " but the request wants " + settings.periodInMs); return false; } // check truncated binary exponential back off scan settings if (settings.maxPeriodInMs != 0 && settings.maxPeriodInMs != settings.periodInMs) { if (settings.maxPeriodInMs < settings.periodInMs) { loge("Failing scan request because maxPeriodInMs is " + settings.maxPeriodInMs + " but less than periodInMs " + settings.periodInMs); return false; } if (settings.maxPeriodInMs > WifiScanner.MAX_SCAN_PERIOD_MS) { loge("Failing scan request because maxSupportedPeriodMs is " + WifiScanner.MAX_SCAN_PERIOD_MS + " but the request wants " + settings.maxPeriodInMs); return false; } if (settings.stepCount < 1) { loge("Failing scan request because stepCount is " + settings.stepCount + " which is less than 1"); return false; } } logScanRequest("addBackgroundScanRequest", ci, handler, null, settings, null); mWifiMetrics.getScanMetrics().setClientUid(ci.mUid); mWifiMetrics.getScanMetrics().setWorkSource(workSource); mActiveBackgroundScans.addRequest(ci, handler, workSource, settings); if (updateSchedule()) { return true; } else { mActiveBackgroundScans.removeRequest(ci, handler); localLog("Failing scan request because failed to reset scan"); return false; } } private boolean updateSchedule() { if (mChannelHelper == null || mBackgroundScheduler == null || mScannerImpl == null) { loge("Failed to update schedule because WifiScanningService is not initialized"); return false; } mChannelHelper.updateChannels(); Collection settings = mActiveBackgroundScans.getAllSettings(); mBackgroundScheduler.updateSchedule(settings); WifiNative.ScanSettings schedule = mBackgroundScheduler.getSchedule(); if (ScanScheduleUtil.scheduleEquals(mPreviousSchedule, schedule)) { if (DBG) Log.d(TAG, "schedule updated with no change"); return true; } mPreviousSchedule = schedule; if (schedule.num_buckets == 0) { mScannerImpl.stopBatchedScan(); if (DBG) Log.d(TAG, "scan stopped"); return true; } else { localLog("starting scan: " + "base period=" + schedule.base_period_ms + ", max ap per scan=" + schedule.max_ap_per_scan + ", batched scans=" + schedule.report_threshold_num_scans); for (int b = 0; b < schedule.num_buckets; b++) { WifiNative.BucketSettings bucket = schedule.buckets[b]; localLog("bucket " + bucket.bucket + " (" + bucket.period_ms + "ms)" + "[" + bucket.report_events + "]: " + ChannelHelper.toString(bucket)); } if (mScannerImpl.startBatchedScan(schedule, new ScanEventHandler(mScannerImpl.getIfaceName()))) { if (DBG) { Log.d(TAG, "scan restarted with " + schedule.num_buckets + " bucket(s) and base period: " + schedule.base_period_ms); } mWifiMetrics.getScanMetrics().logScanStarted( WifiMetrics.ScanMetrics.SCAN_TYPE_BACKGROUND); return true; } else { mPreviousSchedule = null; loge("error starting scan: " + "base period=" + schedule.base_period_ms + ", max ap per scan=" + schedule.max_ap_per_scan + ", batched scans=" + schedule.report_threshold_num_scans); for (int b = 0; b < schedule.num_buckets; b++) { WifiNative.BucketSettings bucket = schedule.buckets[b]; loge("bucket " + bucket.bucket + " (" + bucket.period_ms + "ms)" + "[" + bucket.report_events + "]: " + ChannelHelper.toString(bucket)); } mWifiMetrics.getScanMetrics().logScanFailedToStart( WifiMetrics.ScanMetrics.SCAN_TYPE_BACKGROUND); return false; } } } private void removeBackgroundScanRequest(ClientInfo ci, int handler) { if (ci != null) { ScanSettings settings = mActiveBackgroundScans.removeRequest(ci, handler); logScanRequest("removeBackgroundScanRequest", ci, handler, null, settings, null); updateSchedule(); } } private void reportFullScanResult(ScanResult result, int bucketsScanned) { for (RequestInfo entry : mActiveBackgroundScans) { ClientInfo ci = entry.clientInfo; int handler = entry.handlerId; ScanSettings settings = entry.settings; if (mBackgroundScheduler.shouldReportFullScanResultForSettings( result, bucketsScanned, settings)) { ScanResult newResult = new ScanResult(result); if (result.informationElements != null) { newResult.informationElements = result.informationElements.clone(); } else { newResult.informationElements = null; } ci.reportEvent(WifiScanner.CMD_FULL_SCAN_RESULT, 0, handler, newResult); } } } private void reportScanResults(ScanData[] results) { if (results == null) { Log.d(TAG,"The results is null, nothing to report."); return; } for (ScanData result : results) { if (result != null && result.getResults() != null) { if (result.getResults().length > 0) { mWifiMetrics.incrementNonEmptyScanResultCount(); } else { mWifiMetrics.incrementEmptyScanResultCount(); } } } for (RequestInfo entry : mActiveBackgroundScans) { ClientInfo ci = entry.clientInfo; int handler = entry.handlerId; ScanSettings settings = entry.settings; ScanData[] resultsToDeliver = mBackgroundScheduler.filterResultsForSettings(results, settings); if (resultsToDeliver != null) { logCallback("backgroundScanResults", ci, handler, describeForLog(resultsToDeliver)); WifiScanner.ParcelableScanData parcelableScanData = new WifiScanner.ParcelableScanData(resultsToDeliver); ci.reportEvent(WifiScanner.CMD_SCAN_RESULT, 0, handler, parcelableScanData); } } } private void sendBackgroundScanFailedToAllAndClear(int reason, String description) { for (RequestInfo entry : mActiveBackgroundScans) { ClientInfo ci = entry.clientInfo; int handler = entry.handlerId; ci.reportEvent(WifiScanner.CMD_OP_FAILED, 0, handler, new WifiScanner.OperationResult(reason, description)); } mActiveBackgroundScans.clear(); } } /** * PNO scan state machine has 5 states: * -Default State * -Started State * -Hw Pno Scan state * -Single Scan state * * These are the main state transitions: * 1. Start at |Default State| * 2. Move to |Started State| when we get the |WIFI_SCAN_AVAILABLE| broadcast from WifiManager. * 3. When a new PNO scan request comes in: * a.1. Switch to |Hw Pno Scan state| when the device supports HW PNO * (This could either be HAL based ePNO or wificond based PNO). * a.2. In |Hw Pno Scan state| when PNO scan results are received, check if the result * contains IE (information elements). If yes, send the results to the client, else * switch to |Single Scan state| and send the result to the client when the scan result * is obtained. * * Note: PNO scans only work for a single client today. We don't have support in HW to support * multiple requests at the same time, so will need non-trivial changes to support (if at all * possible) in WifiScanningService. */ class WifiPnoScanStateMachine extends StateMachine { private final DefaultState mDefaultState = new DefaultState(); private final StartedState mStartedState = new StartedState(); private final HwPnoScanState mHwPnoScanState = new HwPnoScanState(); private final SingleScanState mSingleScanState = new SingleScanState(); private InternalClientInfo mInternalClientInfo; private final RequestList> mActivePnoScans = new RequestList<>(); // Tracks scan requests across multiple scanner impls. private final ScannerImplsTracker mScannerImplsTracker; WifiPnoScanStateMachine(Looper looper) { super("WifiPnoScanStateMachine", looper); mScannerImplsTracker = new ScannerImplsTracker(); setLogRecSize(256); setLogOnlyTransitions(false); // CHECKSTYLE:OFF IndentationCheck addState(mDefaultState); addState(mStartedState, mDefaultState); addState(mHwPnoScanState, mStartedState); addState(mSingleScanState, mHwPnoScanState); // CHECKSTYLE:ON IndentationCheck setInitialState(mDefaultState); } public void removePnoSettings(ClientInfo ci) { mActivePnoScans.removeAllForClient(ci); transitionTo(mStartedState); } /** * Tracks a PNO scan request across all the available scanner impls. * * Note: If there are failures on some of the scanner impls, we ignore them since we can * get a PNO match from the other successful impls. We don't declare total scan * failures, unless all the scanner impls fail. */ private final class ScannerImplsTracker { private final class PnoEventHandler implements WifiNative.PnoEventHandler { private final String mImplIfaceName; PnoEventHandler(@NonNull String implIfaceName) { mImplIfaceName = implIfaceName; } @Override public void onPnoNetworkFound(ScanResult[] results) { if (DBG) localLog("onWifiPnoNetworkFound event received"); reportPnoNetworkFoundForImpl(mImplIfaceName, results); } @Override public void onPnoScanFailed() { if (DBG) localLog("onWifiPnoScanFailed event received"); reportPnoScanFailedForImpl(mImplIfaceName); } } private static final int STATUS_PENDING = 0; private static final int STATUS_FAILED = 2; // Tracks scan status per impl. Map mStatusPerImpl = new ArrayMap<>(); /** * Triggers a new PNO with the specified settings on all the available scanner impls. * @return true if the PNO succeeded on any of the impl, false otherwise. */ public boolean setHwPnoList(WifiNative.PnoSettings pnoSettings) { mStatusPerImpl.clear(); boolean anySuccess = false; for (Map.Entry 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; } /** * Resets any ongoing PNO on all the available scanner impls. * @return true if the PNO stop succeeded on all of the impl, false otherwise. */ public boolean resetHwPnoList() { boolean allSuccess = true; for (String ifaceName : mStatusPerImpl.keySet()) { WifiScannerImpl impl = mScannerImpls.get(ifaceName); if (impl == null) continue; boolean success = impl.resetHwPnoList(); if (!success) { Log.e(TAG, "Failed to stop pno on " + ifaceName); allSuccess = false; } } mStatusPerImpl.clear(); return allSuccess; } /** * @return true if HW PNO is supported on all the available scanner impls, * false otherwise. */ public boolean isHwPnoSupported(boolean isConnected) { for (WifiScannerImpl impl : mScannerImpls.values()) { if (!impl.isHwPnoSupported(isConnected)) { return false; } } return true; } private void reportPnoNetworkFoundForImpl(@NonNull String implIfaceName, ScanResult[] results) { Integer status = mStatusPerImpl.get(implIfaceName); if (status != null && status == STATUS_PENDING) { sendMessage(CMD_PNO_NETWORK_FOUND, 0, 0, results); } } private int getConsolidatedStatus() { boolean anyPending = mStatusPerImpl.values().stream() .anyMatch(status -> status == STATUS_PENDING); // at-least one impl status is still pending. if (anyPending) { return STATUS_PENDING; } else { // all failed. return STATUS_FAILED; } } private void reportPnoScanFailedForImpl(@NonNull String implIfaceName) { Integer currentStatus = mStatusPerImpl.get(implIfaceName); if (currentStatus != null && currentStatus == STATUS_PENDING) { mStatusPerImpl.put(implIfaceName, STATUS_FAILED); } // Now check if all the scanner impls scan status is available. int consolidatedStatus = getConsolidatedStatus(); if (consolidatedStatus == STATUS_FAILED) { sendMessage(CMD_PNO_SCAN_FAILED); } } } class DefaultState extends State { @Override public void enter() { if (DBG) localLog("DefaultState"); } @Override public boolean processMessage(Message msg) { switch (msg.what) { case WifiScanner.CMD_ENABLE: if (mScannerImpls.isEmpty()) { loge("Failed to start pno scan state machine because scanner impl" + " is null"); return HANDLED; } transitionTo(mStartedState); break; case WifiScanner.CMD_DISABLE: transitionTo(mDefaultState); break; case WifiScanner.CMD_START_PNO_SCAN: case WifiScanner.CMD_STOP_PNO_SCAN: replyFailed(msg, WifiScanner.REASON_UNSPECIFIED, "not available"); break; case CMD_PNO_NETWORK_FOUND: case CMD_PNO_SCAN_FAILED: case WifiScanner.CMD_SCAN_RESULT: case WifiScanner.CMD_OP_FAILED: loge("Unexpected message " + msg.what); break; default: return NOT_HANDLED; } return HANDLED; } } class StartedState extends State { @Override public void enter() { if (DBG) localLog("StartedState"); } @Override public void exit() { sendPnoScanFailedToAllAndClear( WifiScanner.REASON_UNSPECIFIED, "Scan was interrupted"); } @Override public boolean processMessage(Message msg) { ClientInfo ci = mClients.get(msg.replyTo); switch (msg.what) { case WifiScanner.CMD_ENABLE: // Ignore if we're already in driver loaded state. return HANDLED; case WifiScanner.CMD_START_PNO_SCAN: Bundle pnoParams = (Bundle) msg.obj; if (pnoParams == null) { replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "params null"); return HANDLED; } PnoSettings pnoSettings = null; try { pnoSettings = pnoParams.getParcelable( WifiScanner.PNO_PARAMS_PNO_SETTINGS_KEY); } catch (BadParcelableException e) { Log.e(TAG, "Failed to get parcelable params", e); replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "bad parcel params"); return HANDLED; } if (mScannerImplsTracker.isHwPnoSupported(pnoSettings.isConnected)) { deferMessage(msg); transitionTo(mHwPnoScanState); } else { replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "not supported"); } break; case WifiScanner.CMD_STOP_PNO_SCAN: replyFailed(msg, WifiScanner.REASON_UNSPECIFIED, "no scan running"); break; default: return NOT_HANDLED; } return HANDLED; } } class HwPnoScanState extends State { @Override public void enter() { if (DBG) localLog("HwPnoScanState"); } @Override public void exit() { // Reset PNO scan in ScannerImpl before we exit. mScannerImplsTracker.resetHwPnoList(); removeInternalClient(); } @Override public boolean processMessage(Message msg) { ClientInfo ci = mClients.get(msg.replyTo); switch (msg.what) { case WifiScanner.CMD_START_PNO_SCAN: Bundle pnoParams = (Bundle) msg.obj; if (pnoParams == null) { replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "params null"); return HANDLED; } PnoSettings pnoSettings = null; ScanSettings scanSettings = null; try { pnoSettings = pnoParams.getParcelable( WifiScanner.PNO_PARAMS_PNO_SETTINGS_KEY); scanSettings = pnoParams.getParcelable( WifiScanner.PNO_PARAMS_SCAN_SETTINGS_KEY); } catch (BadParcelableException e) { Log.e(TAG, "Failed to get parcelable params", e); replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "bad parcel params"); return HANDLED; } if (addHwPnoScanRequest(ci, msg.arg2, scanSettings, pnoSettings)) { mWifiMetrics.getScanMetrics().logPnoScanEvent( WifiMetrics.ScanMetrics.PNO_SCAN_STATE_STARTED); replySucceeded(msg); } else { mWifiMetrics.getScanMetrics().logPnoScanEvent( WifiMetrics.ScanMetrics.PNO_SCAN_STATE_FAILED_TO_START); replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "bad request"); transitionTo(mStartedState); } break; case WifiScanner.CMD_STOP_PNO_SCAN: removeHwPnoScanRequest(ci, msg.arg2); transitionTo(mStartedState); break; case CMD_PNO_NETWORK_FOUND: ScanResult[] scanResults = ((ScanResult[]) msg.obj); mWifiMetrics.getScanMetrics().logPnoScanEvent( WifiMetrics.ScanMetrics.PNO_SCAN_STATE_COMPLETED_NETWORK_FOUND); if (isSingleScanNeeded(scanResults)) { ScanSettings activeScanSettings = getScanSettings(); if (activeScanSettings == null) { sendPnoScanFailedToAllAndClear( WifiScanner.REASON_UNSPECIFIED, "couldn't retrieve setting"); transitionTo(mStartedState); } else { addSingleScanRequest(activeScanSettings); transitionTo(mSingleScanState); } } else { reportPnoNetworkFound((ScanResult[]) msg.obj); } break; case CMD_PNO_SCAN_FAILED: mWifiMetrics.getScanMetrics().logPnoScanEvent( WifiMetrics.ScanMetrics.PNO_SCAN_STATE_FAILED); sendPnoScanFailedToAllAndClear( WifiScanner.REASON_UNSPECIFIED, "pno scan failed"); transitionTo(mStartedState); break; default: return NOT_HANDLED; } return HANDLED; } } class SingleScanState extends State { @Override public void enter() { if (DBG) localLog("SingleScanState"); } @Override public boolean processMessage(Message msg) { ClientInfo ci = mClients.get(msg.replyTo); switch (msg.what) { case WifiScanner.CMD_SCAN_RESULT: WifiScanner.ParcelableScanData parcelableScanData = (WifiScanner.ParcelableScanData) msg.obj; ScanData[] scanDatas = parcelableScanData.getResults(); ScanData lastScanData = scanDatas[scanDatas.length - 1]; reportPnoNetworkFound(lastScanData.getResults()); transitionTo(mHwPnoScanState); break; case WifiScanner.CMD_OP_FAILED: sendPnoScanFailedToAllAndClear( WifiScanner.REASON_UNSPECIFIED, "single scan failed"); transitionTo(mStartedState); break; default: return NOT_HANDLED; } return HANDLED; } } private WifiNative.PnoSettings convertSettingsToPnoNative(ScanSettings scanSettings, PnoSettings pnoSettings) { WifiNative.PnoSettings nativePnoSetting = new WifiNative.PnoSettings(); nativePnoSetting.periodInMs = scanSettings.periodInMs; nativePnoSetting.min5GHzRssi = pnoSettings.min5GHzRssi; nativePnoSetting.min24GHzRssi = pnoSettings.min24GHzRssi; nativePnoSetting.min6GHzRssi = pnoSettings.min6GHzRssi; nativePnoSetting.isConnected = pnoSettings.isConnected; nativePnoSetting.networkList = new WifiNative.PnoNetwork[pnoSettings.networkList.length]; for (int i = 0; i < pnoSettings.networkList.length; i++) { nativePnoSetting.networkList[i] = new WifiNative.PnoNetwork(); nativePnoSetting.networkList[i].ssid = pnoSettings.networkList[i].ssid; nativePnoSetting.networkList[i].flags = pnoSettings.networkList[i].flags; nativePnoSetting.networkList[i].auth_bit_field = pnoSettings.networkList[i].authBitField; nativePnoSetting.networkList[i].frequencies = pnoSettings.networkList[i].frequencies; } return nativePnoSetting; } // Retrieve the only active scan settings. private ScanSettings getScanSettings() { for (Pair settingsPair : mActivePnoScans.getAllSettings()) { return settingsPair.second; } return null; } private void removeInternalClient() { if (mInternalClientInfo != null) { mInternalClientInfo.cleanup(); mInternalClientInfo = null; } else { Log.w(TAG, "No Internal client for PNO"); } } private void addInternalClient(ClientInfo ci) { if (mInternalClientInfo == null) { mInternalClientInfo = new InternalClientInfo(ci.getUid(), new Messenger(this.getHandler())); mInternalClientInfo.register(); } else { Log.w(TAG, "Internal client for PNO already exists"); } } private void addPnoScanRequest(ClientInfo ci, int handler, ScanSettings scanSettings, PnoSettings pnoSettings) { mActivePnoScans.addRequest(ci, handler, ClientModeImpl.WIFI_WORK_SOURCE, Pair.create(pnoSettings, scanSettings)); addInternalClient(ci); } private Pair removePnoScanRequest(ClientInfo ci, int handler) { Pair settings = mActivePnoScans.removeRequest(ci, handler); return settings; } private boolean addHwPnoScanRequest(ClientInfo ci, int handler, ScanSettings scanSettings, PnoSettings pnoSettings) { if (ci == null) { Log.d(TAG, "Failing scan request ClientInfo not found " + handler); return false; } if (!mActivePnoScans.isEmpty()) { loge("Failing scan request because there is already an active scan"); return false; } 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; } private void removeHwPnoScanRequest(ClientInfo ci, int handler) { if (ci != null) { Pair settings = removePnoScanRequest(ci, handler); logScanRequest("removeHwPnoScanRequest", ci, handler, null, settings.second, settings.first); } } private void reportPnoNetworkFound(ScanResult[] results) { WifiScanner.ParcelableScanResults parcelableScanResults = new WifiScanner.ParcelableScanResults(results); for (RequestInfo> entry : mActivePnoScans) { ClientInfo ci = entry.clientInfo; int handler = entry.handlerId; logCallback("pnoNetworkFound", ci, handler, describeForLog(results)); ci.reportEvent( WifiScanner.CMD_PNO_NETWORK_FOUND, 0, handler, parcelableScanResults); } } private void sendPnoScanFailedToAllAndClear(int reason, String description) { for (RequestInfo> entry : mActivePnoScans) { ClientInfo ci = entry.clientInfo; int handler = entry.handlerId; ci.reportEvent(WifiScanner.CMD_OP_FAILED, 0, handler, new WifiScanner.OperationResult(reason, description)); } mActivePnoScans.clear(); } private void addSingleScanRequest(ScanSettings settings) { if (DBG) localLog("Starting single scan"); if (mInternalClientInfo != null) { mInternalClientInfo.sendRequestToClientHandler( WifiScanner.CMD_START_SINGLE_SCAN, settings, ClientModeImpl.WIFI_WORK_SOURCE); } mWifiMetrics.getScanMetrics().setWorkSource(ClientModeImpl.WIFI_WORK_SOURCE); } /** * Checks if IE are present in scan data, if no single scan is needed to report event to * client */ private boolean isSingleScanNeeded(ScanResult[] scanResults) { for (ScanResult scanResult : scanResults) { if (scanResult.informationElements != null && scanResult.informationElements.length > 0) { return false; } } return true; } } private abstract class ClientInfo { private final int mUid; private final WorkSource mWorkSource; private boolean mScanWorkReported = false; protected final Messenger mMessenger; ClientInfo(int uid, Messenger messenger) { mUid = uid; mMessenger = messenger; mWorkSource = new WorkSource(uid); } /** * Register this client to main client map. */ public void register() { mClients.put(mMessenger, this); } /** * Unregister this client from main client map. */ private void unregister() { mClients.remove(mMessenger); } public void cleanup() { mSingleScanListeners.removeAllForClient(this); mSingleScanStateMachine.removeSingleScanRequests(this); mBackgroundScanStateMachine.removeBackgroundScanSettings(this); unregister(); localLog("Successfully stopped all requests for client " + this); } public int getUid() { return mUid; } public void reportEvent(int what, int arg1, int arg2) { reportEvent(what, arg1, arg2, null); } // This has to be implemented by subclasses to report events back to clients. public abstract void reportEvent(int what, int arg1, int arg2, Object obj); // TODO(b/27903217, 71530998): This is dead code. Should this be wired up ? private void reportBatchedScanStart() { if (mUid == 0) return; int csph = getCsph(); mBatteryStats.reportWifiBatchedScanStartedFromSource(mWorkSource, csph); } // TODO(b/27903217, 71530998): This is dead code. Should this be wired up ? private void reportBatchedScanStop() { if (mUid == 0) return; mBatteryStats.reportWifiBatchedScanStoppedFromSource(mWorkSource); } // TODO migrate batterystats to accept scan duration per hour instead of csph private int getCsph() { int totalScanDurationPerHour = 0; Collection settingsList = mBackgroundScanStateMachine.getBackgroundScanSettings(this); for (ScanSettings settings : settingsList) { int scanDurationMs = mChannelHelper.estimateScanDuration(settings); int scans_per_Hour = settings.periodInMs == 0 ? 1 : (3600 * 1000) / settings.periodInMs; totalScanDurationPerHour += scanDurationMs * scans_per_Hour; } return totalScanDurationPerHour / ChannelHelper.SCAN_PERIOD_PER_CHANNEL_MS; } // TODO(b/27903217, 71530998): This is dead code. Should this be wired up ? private void reportScanWorkUpdate() { if (mScanWorkReported) { reportBatchedScanStop(); mScanWorkReported = false; } if (mBackgroundScanStateMachine.getBackgroundScanSettings(this).isEmpty()) { reportBatchedScanStart(); mScanWorkReported = true; } } @Override public String toString() { return "ClientInfo[uid=" + mUid + "," + mMessenger + "]"; } } /** * This class is used to represent external clients to the WifiScanning Service. */ private class ExternalClientInfo extends ClientInfo { private final AsyncChannel mChannel; /** * Indicates if the client is still connected * If the client is no longer connected then messages to it will be silently dropped */ private boolean mDisconnected = false; ExternalClientInfo(int uid, Messenger messenger, AsyncChannel c) { super(uid, messenger); mChannel = c; if (DBG) localLog("New client, channel: " + c); } @Override public void reportEvent(int what, int arg1, int arg2, Object obj) { if (!mDisconnected) { mChannel.sendMessage(what, arg1, arg2, obj); } } @Override public void cleanup() { mDisconnected = true; mPnoScanStateMachine.removePnoSettings(this); super.cleanup(); } } /** * This class is used to represent internal clients to the WifiScanning Service. This is needed * for communicating between State Machines. * This leaves the onReportEvent method unimplemented, so that the clients have the freedom * to handle the events as they need. */ private class InternalClientInfo extends ClientInfo { private static final int INTERNAL_CLIENT_HANDLER = 0; /** * The UID here is used to proxy the original external requester UID. */ InternalClientInfo(int requesterUid, Messenger messenger) { super(requesterUid, messenger); } @Override public void reportEvent(int what, int arg1, int arg2, Object obj) { Message message = Message.obtain(); message.what = what; message.arg1 = arg1; message.arg2 = arg2; message.obj = obj; try { mMessenger.send(message); } catch (RemoteException e) { loge("Failed to send message: " + what); } } /** * Send a message to the client handler which should reroute the message to the appropriate * state machine. */ public void sendRequestToClientHandler(int what, ScanSettings settings, WorkSource workSource) { Message msg = Message.obtain(); msg.what = what; msg.arg2 = INTERNAL_CLIENT_HANDLER; if (settings != null) { Bundle bundle = new Bundle(); bundle.putParcelable(WifiScanner.SCAN_PARAMS_SCAN_SETTINGS_KEY, settings); bundle.putParcelable(WifiScanner.SCAN_PARAMS_WORK_SOURCE_KEY, workSource); msg.obj = bundle; } msg.replyTo = mMessenger; msg.sendingUid = getUid(); mClientHandler.sendMessage(msg); } /** * Send a message to the client handler which should reroute the message to the appropriate * state machine. */ public void sendRequestToClientHandler(int what) { sendRequestToClientHandler(what, null, null); } @Override public String toString() { return "InternalClientInfo[]"; } } void replySucceeded(Message msg) { if (msg.replyTo != null) { Message reply = Message.obtain(); reply.what = WifiScanner.CMD_OP_SUCCEEDED; reply.arg2 = msg.arg2; if (msg.obj != null) { reply.obj = msg.obj; } try { msg.replyTo.send(reply); mLog.trace("replySucceeded recvdMessage=%").c(msg.what).flush(); } catch (RemoteException e) { // There's not much we can do if reply can't be sent! } } else { // locally generated message; doesn't need a reply! } } void replyFailed(Message msg, int reason, String description) { if (msg.replyTo != null) { Message reply = Message.obtain(); reply.what = WifiScanner.CMD_OP_FAILED; reply.arg2 = msg.arg2; reply.obj = new WifiScanner.OperationResult(reason, description); try { msg.replyTo.send(reply); mLog.trace("replyFailed recvdMessage=% reason=%") .c(msg.what) .c(reason) .flush(); } catch (RemoteException e) { // There's not much we can do if reply can't be sent! } } else { // locally generated message; doesn't need a reply! } } private static String toString(int uid, ScanSettings settings) { StringBuilder sb = new StringBuilder(); sb.append("ScanSettings[uid=").append(uid); sb.append(", period=").append(settings.periodInMs); sb.append(", report=").append(settings.reportEvents); if (settings.reportEvents == WifiScanner.REPORT_EVENT_AFTER_BUFFER_FULL && settings.numBssidsPerScan > 0 && settings.maxScansToCache > 1) { sb.append(", batch=").append(settings.maxScansToCache); sb.append(", numAP=").append(settings.numBssidsPerScan); } sb.append(", ").append(ChannelHelper.toString(settings)); sb.append("]"); return sb.toString(); } @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) != PERMISSION_GRANTED) { pw.println("Permission Denial: can't dump WifiScanner from from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid() + " without permission " + android.Manifest.permission.DUMP); return; } pw.println("WifiScanningService - Log Begin ----"); mLocalLog.dump(fd, pw, args); pw.println("WifiScanningService - Log End ----"); pw.println(); pw.println("clients:"); for (ClientInfo client : mClients.values()) { pw.println(" " + client); } pw.println("listeners:"); for (ClientInfo client : mClients.values()) { Collection settingsList = mBackgroundScanStateMachine.getBackgroundScanSettings(client); for (ScanSettings settings : settingsList) { pw.println(" " + toString(client.mUid, settings)); } } if (mBackgroundScheduler != null) { WifiNative.ScanSettings schedule = mBackgroundScheduler.getSchedule(); if (schedule != null) { pw.println("schedule:"); pw.println(" base period: " + schedule.base_period_ms); pw.println(" max ap per scan: " + schedule.max_ap_per_scan); pw.println(" batched scans: " + schedule.report_threshold_num_scans); pw.println(" buckets:"); for (int b = 0; b < schedule.num_buckets; b++) { WifiNative.BucketSettings bucket = schedule.buckets[b]; pw.println(" bucket " + bucket.bucket + " (" + bucket.period_ms + "ms)[" + bucket.report_events + "]: " + ChannelHelper.toString(bucket)); } } } if (mPnoScanStateMachine != null) { mPnoScanStateMachine.dump(fd, pw, args); } pw.println(); if (mSingleScanStateMachine != null) { mSingleScanStateMachine.dump(fd, pw, args); pw.println(); List scanResults = mSingleScanStateMachine.getCachedScanResultsAsList(); long nowMs = mClock.getElapsedSinceBootMillis(); Log.d(TAG, "Latest scan results nowMs = " + nowMs); pw.println("Latest scan results:"); ScanResultUtil.dumpScanResults(pw, scanResults, nowMs); pw.println(); } for (WifiScannerImpl impl : mScannerImpls.values()) { impl.dump(fd, pw, args); } } void logScanRequest(String request, ClientInfo ci, int id, WorkSource workSource, ScanSettings settings, PnoSettings pnoSettings) { StringBuilder sb = new StringBuilder(); sb.append(request) .append(": ") .append((ci == null) ? "ClientInfo[unknown]" : ci.toString()) .append(",Id=") .append(id); if (workSource != null) { sb.append(",").append(workSource); } if (settings != null) { sb.append(", "); describeTo(sb, settings); } if (pnoSettings != null) { sb.append(", "); describeTo(sb, pnoSettings); } localLog(sb.toString()); } void logCallback(String callback, ClientInfo ci, int id, String extra) { StringBuilder sb = new StringBuilder(); sb.append(callback) .append(": ") .append((ci == null) ? "ClientInfo[unknown]" : ci.toString()) .append(",Id=") .append(id); if (extra != null) { sb.append(",").append(extra); } localLog(sb.toString()); } static String describeForLog(ScanData[] results) { StringBuilder sb = new StringBuilder(); sb.append("results="); for (int i = 0; i < results.length; ++i) { if (i > 0) sb.append(";"); sb.append(results[i].getResults().length); } return sb.toString(); } static String describeForLog(ScanResult[] results) { return "results=" + results.length; } static String getScanTypeString(int type) { switch(type) { case WifiScanner.SCAN_TYPE_LOW_LATENCY: return "LOW LATENCY"; case WifiScanner.SCAN_TYPE_LOW_POWER: return "LOW POWER"; case WifiScanner.SCAN_TYPE_HIGH_ACCURACY: return "HIGH ACCURACY"; default: // This should never happen because we've validated the incoming type in // |validateScanType|. throw new IllegalArgumentException("Invalid scan type " + type); } } static String describeTo(StringBuilder sb, ScanSettings scanSettings) { sb.append("ScanSettings { ") .append(" type:").append(getScanTypeString(scanSettings.type)) .append(" band:").append(ChannelHelper.bandToString(scanSettings.band)) .append(" ignoreLocationSettings:").append(scanSettings.ignoreLocationSettings) .append(" period:").append(scanSettings.periodInMs) .append(" reportEvents:").append(scanSettings.reportEvents) .append(" numBssidsPerScan:").append(scanSettings.numBssidsPerScan) .append(" maxScansToCache:").append(scanSettings.maxScansToCache) .append(" rnrSetting:").append( SdkLevel.isAtLeastS() ? scanSettings.getRnrSetting() : "Not supported") .append(" 6GhzPscOnlyEnabled:").append( SdkLevel.isAtLeastS() ? scanSettings.is6GhzPscOnlyEnabled() : "Not supported") .append(" channels:[ "); if (scanSettings.channels != null) { for (int i = 0; i < scanSettings.channels.length; i++) { sb.append(scanSettings.channels[i].frequency).append(" "); } } sb.append(" ] ").append(" } "); return sb.toString(); } static String describeTo(StringBuilder sb, PnoSettings pnoSettings) { sb.append("PnoSettings { ") .append(" min5GhzRssi:").append(pnoSettings.min5GHzRssi) .append(" min24GhzRssi:").append(pnoSettings.min24GHzRssi) .append(" min6GhzRssi:").append(pnoSettings.min6GHzRssi) .append(" isConnected:").append(pnoSettings.isConnected) .append(" networks:[ "); if (pnoSettings.networkList != null) { for (int i = 0; i < pnoSettings.networkList.length; i++) { sb.append(pnoSettings.networkList[i].ssid).append(","); } } sb.append(" ] ") .append(" } "); return sb.toString(); } }