| /* |
| * Copyright (C) 2015 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 android.app.AlarmManager; |
| import android.content.Context; |
| import android.net.wifi.ScanResult; |
| import android.net.wifi.WifiConfiguration; |
| import android.net.wifi.WifiScanner; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.util.Log; |
| |
| import com.android.internal.R; |
| import com.android.server.wifi.Clock; |
| import com.android.server.wifi.ScanDetail; |
| import com.android.server.wifi.WifiMonitor; |
| import com.android.server.wifi.WifiNative; |
| import com.android.server.wifi.scanner.ChannelHelper.ChannelCollection; |
| |
| import java.util.ArrayDeque; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * Implementation of the WifiScanner HAL API that uses wpa_supplicant to perform all scans |
| * @see com.android.server.wifi.scanner.WifiScannerImpl for more details on each method. |
| */ |
| public class SupplicantWifiScannerImpl extends WifiScannerImpl implements Handler.Callback { |
| private static final String TAG = "SupplicantWifiScannerImpl"; |
| private static final boolean DBG = false; |
| |
| public static final String BACKGROUND_PERIOD_ALARM_TAG = TAG + " Background Scan Period"; |
| public static final String TIMEOUT_ALARM_TAG = TAG + " Scan Timeout"; |
| // Max number of networks that can be specified to wpa_supplicant per scan request |
| public static final int MAX_HIDDEN_NETWORK_IDS_PER_SCAN = 16; |
| |
| private static final int SCAN_BUFFER_CAPACITY = 10; |
| private static final int MAX_APS_PER_SCAN = 32; |
| private static final int MAX_SCAN_BUCKETS = 16; |
| |
| private static final String ACTION_SCAN_PERIOD = |
| "com.android.server.util.SupplicantWifiScannerImpl.action.SCAN_PERIOD"; |
| |
| private final Context mContext; |
| private final WifiNative mWifiNative; |
| private final AlarmManager mAlarmManager; |
| private final Handler mEventHandler; |
| private final ChannelHelper mChannelHelper; |
| private final Clock mClock; |
| |
| private Object mSettingsLock = new Object(); |
| |
| // Next scan settings to apply when the previous scan completes |
| private WifiNative.ScanSettings mPendingBackgroundScanSettings = null; |
| private WifiNative.ScanEventHandler mPendingBackgroundScanEventHandler = null; |
| private WifiNative.ScanSettings mPendingSingleScanSettings = null; |
| private WifiNative.ScanEventHandler mPendingSingleScanEventHandler = null; |
| |
| // Active background scan settings/state |
| private WifiNative.ScanSettings mBackgroundScanSettings = null; |
| private WifiNative.ScanEventHandler mBackgroundScanEventHandler = null; |
| private int mNextBackgroundScanPeriod = 0; |
| private int mNextBackgroundScanId = 0; |
| private boolean mBackgroundScanPeriodPending = false; |
| private boolean mBackgroundScanPaused = false; |
| private ScanBuffer mBackgroundScanBuffer = new ScanBuffer(SCAN_BUFFER_CAPACITY); |
| |
| private WifiScanner.ScanData mLatestSingleScanResult = |
| new WifiScanner.ScanData(0, 0, new ScanResult[0]); |
| |
| // Settings for the currently running scan, null if no scan active |
| private LastScanSettings mLastScanSettings = null; |
| |
| // Active hotlist settings |
| private WifiNative.HotlistEventHandler mHotlistHandler = null; |
| private ChangeBuffer mHotlistChangeBuffer = new ChangeBuffer(); |
| |
| // Pno related info. |
| private WifiNative.PnoSettings mPnoSettings = null; |
| private WifiNative.PnoEventHandler mPnoEventHandler; |
| private final boolean mHwPnoScanSupported; |
| private final HwPnoDebouncer mHwPnoDebouncer; |
| private final HwPnoDebouncer.Listener mHwPnoDebouncerListener = new HwPnoDebouncer.Listener() { |
| public void onPnoScanFailed() { |
| Log.e(TAG, "Pno scan failure received"); |
| reportPnoScanFailure(); |
| } |
| }; |
| |
| /** |
| * Duration to wait before timing out a scan. |
| * |
| * The expected behavior is that the hardware will return a failed scan if it does not |
| * complete, but timeout just in case it does not. |
| */ |
| private static final long SCAN_TIMEOUT_MS = 15000; |
| |
| AlarmManager.OnAlarmListener mScanPeriodListener = new AlarmManager.OnAlarmListener() { |
| public void onAlarm() { |
| synchronized (mSettingsLock) { |
| handleScanPeriod(); |
| } |
| } |
| }; |
| |
| AlarmManager.OnAlarmListener mScanTimeoutListener = new AlarmManager.OnAlarmListener() { |
| public void onAlarm() { |
| synchronized (mSettingsLock) { |
| handleScanTimeout(); |
| } |
| } |
| }; |
| |
| public SupplicantWifiScannerImpl(Context context, WifiNative wifiNative, |
| ChannelHelper channelHelper, Looper looper, Clock clock) { |
| mContext = context; |
| mWifiNative = wifiNative; |
| mChannelHelper = channelHelper; |
| mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); |
| mEventHandler = new Handler(looper, this); |
| mClock = clock; |
| mHwPnoDebouncer = new HwPnoDebouncer(mWifiNative, mAlarmManager, mEventHandler, mClock); |
| |
| // Check if the device supports HW PNO scans. |
| mHwPnoScanSupported = mContext.getResources().getBoolean( |
| R.bool.config_wifi_background_scan_support); |
| |
| WifiMonitor.getInstance().registerHandler(mWifiNative.getInterfaceName(), |
| WifiMonitor.SCAN_FAILED_EVENT, mEventHandler); |
| WifiMonitor.getInstance().registerHandler(mWifiNative.getInterfaceName(), |
| WifiMonitor.SCAN_RESULTS_EVENT, mEventHandler); |
| } |
| |
| public SupplicantWifiScannerImpl(Context context, WifiNative wifiNative, Looper looper, |
| Clock clock) { |
| // TODO figure out how to get channel information from supplicant |
| this(context, wifiNative, new NoBandChannelHelper(), looper, clock); |
| } |
| |
| @Override |
| public void cleanup() { |
| synchronized (mSettingsLock) { |
| mPendingSingleScanSettings = null; |
| mPendingSingleScanEventHandler = null; |
| stopHwPnoScan(); |
| stopBatchedScan(); |
| resetHotlist(); |
| untrackSignificantWifiChange(); |
| mLastScanSettings = null; // finally clear any active scan |
| } |
| } |
| |
| @Override |
| public boolean getScanCapabilities(WifiNative.ScanCapabilities capabilities) { |
| capabilities.max_scan_cache_size = Integer.MAX_VALUE; |
| capabilities.max_scan_buckets = MAX_SCAN_BUCKETS; |
| capabilities.max_ap_cache_per_scan = MAX_APS_PER_SCAN; |
| capabilities.max_rssi_sample_size = 8; |
| capabilities.max_scan_reporting_threshold = SCAN_BUFFER_CAPACITY; |
| capabilities.max_hotlist_bssids = 0; |
| capabilities.max_significant_wifi_change_aps = 0; |
| return true; |
| } |
| |
| @Override |
| public ChannelHelper getChannelHelper() { |
| return mChannelHelper; |
| } |
| |
| @Override |
| public boolean startSingleScan(WifiNative.ScanSettings settings, |
| WifiNative.ScanEventHandler eventHandler) { |
| if (eventHandler == null || settings == null) { |
| Log.w(TAG, "Invalid arguments for startSingleScan: settings=" + settings |
| + ",eventHandler=" + eventHandler); |
| return false; |
| } |
| if (mPendingSingleScanSettings != null |
| || (mLastScanSettings != null && mLastScanSettings.singleScanActive)) { |
| Log.w(TAG, "A single scan is already running"); |
| return false; |
| } |
| synchronized (mSettingsLock) { |
| mPendingSingleScanSettings = settings; |
| mPendingSingleScanEventHandler = eventHandler; |
| processPendingScans(); |
| return true; |
| } |
| } |
| |
| @Override |
| public WifiScanner.ScanData getLatestSingleScanResults() { |
| return mLatestSingleScanResult; |
| } |
| |
| @Override |
| public boolean startBatchedScan(WifiNative.ScanSettings settings, |
| WifiNative.ScanEventHandler eventHandler) { |
| if (settings == null || eventHandler == null) { |
| Log.w(TAG, "Invalid arguments for startBatched: settings=" + settings |
| + ",eventHandler=" + eventHandler); |
| return false; |
| } |
| |
| if (settings.max_ap_per_scan < 0 || settings.max_ap_per_scan > MAX_APS_PER_SCAN) { |
| return false; |
| } |
| if (settings.num_buckets < 0 || settings.num_buckets > MAX_SCAN_BUCKETS) { |
| return false; |
| } |
| if (settings.report_threshold_num_scans < 0 |
| || settings.report_threshold_num_scans > SCAN_BUFFER_CAPACITY) { |
| return false; |
| } |
| if (settings.report_threshold_percent < 0 || settings.report_threshold_percent > 100) { |
| return false; |
| } |
| if (settings.base_period_ms <= 0) { |
| return false; |
| } |
| for (int i = 0; i < settings.num_buckets; ++i) { |
| WifiNative.BucketSettings bucket = settings.buckets[i]; |
| if (bucket.period_ms % settings.base_period_ms != 0) { |
| return false; |
| } |
| } |
| |
| synchronized (mSettingsLock) { |
| stopBatchedScan(); |
| if (DBG) { |
| Log.d(TAG, "Starting scan num_buckets=" + settings.num_buckets + ", base_period=" |
| + settings.base_period_ms + " ms"); |
| } |
| mPendingBackgroundScanSettings = settings; |
| mPendingBackgroundScanEventHandler = eventHandler; |
| handleScanPeriod(); // Try to start scan immediately |
| return true; |
| } |
| } |
| |
| @Override |
| public void stopBatchedScan() { |
| synchronized (mSettingsLock) { |
| if (DBG) Log.d(TAG, "Stopping scan"); |
| mBackgroundScanSettings = null; |
| mBackgroundScanEventHandler = null; |
| mPendingBackgroundScanSettings = null; |
| mPendingBackgroundScanEventHandler = null; |
| mBackgroundScanPaused = false; |
| mBackgroundScanPeriodPending = false; |
| unscheduleScansLocked(); |
| } |
| processPendingScans(); |
| } |
| |
| @Override |
| public void pauseBatchedScan() { |
| synchronized (mSettingsLock) { |
| if (DBG) Log.d(TAG, "Pausing scan"); |
| // if there isn't a pending scan then make the current scan pending |
| if (mPendingBackgroundScanSettings == null) { |
| mPendingBackgroundScanSettings = mBackgroundScanSettings; |
| mPendingBackgroundScanEventHandler = mBackgroundScanEventHandler; |
| } |
| mBackgroundScanSettings = null; |
| mBackgroundScanEventHandler = null; |
| mBackgroundScanPeriodPending = false; |
| mBackgroundScanPaused = true; |
| |
| unscheduleScansLocked(); |
| |
| WifiScanner.ScanData[] results = getLatestBatchedScanResults(/* flush = */ true); |
| if (mPendingBackgroundScanEventHandler != null) { |
| mPendingBackgroundScanEventHandler.onScanPaused(results); |
| } |
| } |
| processPendingScans(); |
| } |
| |
| @Override |
| public void restartBatchedScan() { |
| synchronized (mSettingsLock) { |
| if (DBG) Log.d(TAG, "Restarting scan"); |
| if (mPendingBackgroundScanEventHandler != null) { |
| mPendingBackgroundScanEventHandler.onScanRestarted(); |
| } |
| mBackgroundScanPaused = false; |
| handleScanPeriod(); |
| } |
| } |
| |
| private void unscheduleScansLocked() { |
| mAlarmManager.cancel(mScanPeriodListener); |
| if (mLastScanSettings != null) { |
| mLastScanSettings.backgroundScanActive = false; |
| } |
| } |
| |
| private void handleScanPeriod() { |
| synchronized (mSettingsLock) { |
| mBackgroundScanPeriodPending = true; |
| processPendingScans(); |
| } |
| } |
| |
| private void handleScanTimeout() { |
| Log.e(TAG, "Timed out waiting for scan result from supplicant"); |
| reportScanFailure(); |
| processPendingScans(); |
| } |
| |
| private boolean isDifferentPnoScanSettings(LastScanSettings newScanSettings) { |
| return (mLastScanSettings == null || !Arrays.equals( |
| newScanSettings.pnoNetworkList, mLastScanSettings.pnoNetworkList)); |
| } |
| |
| private void processPendingScans() { |
| synchronized (mSettingsLock) { |
| // Wait for the active scan result to come back to reschedule other scans, |
| // unless if HW pno scan is running. Hw PNO scans are paused it if there |
| // are other pending scans, |
| if (mLastScanSettings != null && !mLastScanSettings.hwPnoScanActive) { |
| return; |
| } |
| |
| ChannelCollection allFreqs = mChannelHelper.createChannelCollection(); |
| Set<Integer> hiddenNetworkIdSet = new HashSet<Integer>(); |
| final LastScanSettings newScanSettings = |
| new LastScanSettings(mClock.elapsedRealtime()); |
| |
| // Update scan settings if there is a pending scan |
| if (!mBackgroundScanPaused) { |
| if (mPendingBackgroundScanSettings != null) { |
| mBackgroundScanSettings = mPendingBackgroundScanSettings; |
| mBackgroundScanEventHandler = mPendingBackgroundScanEventHandler; |
| mNextBackgroundScanPeriod = 0; |
| mPendingBackgroundScanSettings = null; |
| mPendingBackgroundScanEventHandler = null; |
| mBackgroundScanPeriodPending = true; |
| } |
| if (mBackgroundScanPeriodPending && mBackgroundScanSettings != null) { |
| int reportEvents = WifiScanner.REPORT_EVENT_NO_BATCH; // default to no batch |
| for (int bucket_id = 0; bucket_id < mBackgroundScanSettings.num_buckets; |
| ++bucket_id) { |
| WifiNative.BucketSettings bucket = |
| mBackgroundScanSettings.buckets[bucket_id]; |
| if (mNextBackgroundScanPeriod % (bucket.period_ms |
| / mBackgroundScanSettings.base_period_ms) == 0) { |
| if ((bucket.report_events |
| & WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN) != 0) { |
| reportEvents |= WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN; |
| } |
| if ((bucket.report_events |
| & WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT) != 0) { |
| reportEvents |= WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT; |
| } |
| // only no batch if all buckets specify it |
| if ((bucket.report_events |
| & WifiScanner.REPORT_EVENT_NO_BATCH) == 0) { |
| reportEvents &= ~WifiScanner.REPORT_EVENT_NO_BATCH; |
| } |
| |
| allFreqs.addChannels(bucket); |
| } |
| } |
| if (!allFreqs.isEmpty()) { |
| newScanSettings.setBackgroundScan(mNextBackgroundScanId++, |
| mBackgroundScanSettings.max_ap_per_scan, reportEvents, |
| mBackgroundScanSettings.report_threshold_num_scans, |
| mBackgroundScanSettings.report_threshold_percent); |
| } |
| |
| int[] hiddenNetworkIds = mBackgroundScanSettings.hiddenNetworkIds; |
| if (hiddenNetworkIds != null) { |
| int numHiddenNetworkIds = Math.min(hiddenNetworkIds.length, |
| MAX_HIDDEN_NETWORK_IDS_PER_SCAN); |
| for (int i = 0; i < numHiddenNetworkIds; i++) { |
| hiddenNetworkIdSet.add(hiddenNetworkIds[i]); |
| } |
| } |
| |
| mNextBackgroundScanPeriod++; |
| mBackgroundScanPeriodPending = false; |
| mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, |
| mClock.elapsedRealtime() + mBackgroundScanSettings.base_period_ms, |
| BACKGROUND_PERIOD_ALARM_TAG, mScanPeriodListener, mEventHandler); |
| } |
| } |
| |
| if (mPendingSingleScanSettings != null) { |
| boolean reportFullResults = false; |
| ChannelCollection singleScanFreqs = mChannelHelper.createChannelCollection(); |
| for (int i = 0; i < mPendingSingleScanSettings.num_buckets; ++i) { |
| WifiNative.BucketSettings bucketSettings = |
| mPendingSingleScanSettings.buckets[i]; |
| if ((bucketSettings.report_events |
| & WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT) != 0) { |
| reportFullResults = true; |
| } |
| singleScanFreqs.addChannels(bucketSettings); |
| allFreqs.addChannels(bucketSettings); |
| } |
| newScanSettings.setSingleScan(reportFullResults, singleScanFreqs, |
| mPendingSingleScanEventHandler); |
| int[] hiddenNetworkIds = mPendingSingleScanSettings.hiddenNetworkIds; |
| if (hiddenNetworkIds != null) { |
| int numHiddenNetworkIds = Math.min(hiddenNetworkIds.length, |
| MAX_HIDDEN_NETWORK_IDS_PER_SCAN); |
| for (int i = 0; i < numHiddenNetworkIds; i++) { |
| hiddenNetworkIdSet.add(hiddenNetworkIds[i]); |
| } |
| } |
| mPendingSingleScanSettings = null; |
| mPendingSingleScanEventHandler = null; |
| } |
| |
| if ((newScanSettings.backgroundScanActive || newScanSettings.singleScanActive) |
| && !allFreqs.isEmpty()) { |
| pauseHwPnoScan(); |
| Set<Integer> freqs = allFreqs.getSupplicantScanFreqs(); |
| boolean success = mWifiNative.scan(freqs, hiddenNetworkIdSet); |
| if (success) { |
| // TODO handle scan timeout |
| if (DBG) { |
| Log.d(TAG, "Starting wifi scan for freqs=" + freqs |
| + ", background=" + newScanSettings.backgroundScanActive |
| + ", single=" + newScanSettings.singleScanActive); |
| } |
| mLastScanSettings = newScanSettings; |
| mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, |
| mClock.elapsedRealtime() + SCAN_TIMEOUT_MS, |
| TIMEOUT_ALARM_TAG, mScanTimeoutListener, mEventHandler); |
| } else { |
| Log.e(TAG, "Failed to start scan, freqs=" + freqs); |
| // indicate scan failure async |
| mEventHandler.post(new Runnable() { |
| public void run() { |
| if (newScanSettings.singleScanEventHandler != null) { |
| newScanSettings.singleScanEventHandler |
| .onScanStatus(WifiNative.WIFI_SCAN_FAILED); |
| } |
| } |
| }); |
| // TODO(b/27769665) background scans should be failed too if scans fail enough |
| } |
| } else if (isHwPnoScanRequired()) { |
| newScanSettings.setHwPnoScan(mPnoSettings.networkList, mPnoEventHandler); |
| boolean status; |
| // If the PNO network list has changed from the previous request, ensure that |
| // we bypass the debounce logic and restart PNO scan. |
| if (isDifferentPnoScanSettings(newScanSettings)) { |
| status = restartHwPnoScan(); |
| } else { |
| status = startHwPnoScan(); |
| } |
| if (status) { |
| mLastScanSettings = newScanSettings; |
| } else { |
| Log.e(TAG, "Failed to start PNO scan"); |
| // indicate scan failure async |
| mEventHandler.post(new Runnable() { |
| public void run() { |
| if (mPnoEventHandler != null) { |
| mPnoEventHandler.onPnoScanFailed(); |
| } |
| // Clean up PNO state, we don't want to continue PNO scanning. |
| mPnoSettings = null; |
| mPnoEventHandler = null; |
| } |
| }); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public boolean handleMessage(Message msg) { |
| switch(msg.what) { |
| case WifiMonitor.SCAN_FAILED_EVENT: |
| Log.w(TAG, "Scan failed"); |
| mAlarmManager.cancel(mScanTimeoutListener); |
| reportScanFailure(); |
| processPendingScans(); |
| break; |
| case WifiMonitor.SCAN_RESULTS_EVENT: |
| mAlarmManager.cancel(mScanTimeoutListener); |
| pollLatestScanData(); |
| processPendingScans(); |
| break; |
| default: |
| // ignore unknown event |
| } |
| return true; |
| } |
| |
| private void reportScanFailure() { |
| synchronized (mSettingsLock) { |
| if (mLastScanSettings != null) { |
| if (mLastScanSettings.singleScanEventHandler != null) { |
| mLastScanSettings.singleScanEventHandler |
| .onScanStatus(WifiNative.WIFI_SCAN_FAILED); |
| } |
| // TODO(b/27769665) background scans should be failed too if scans fail enough |
| mLastScanSettings = null; |
| } |
| } |
| } |
| |
| private void reportPnoScanFailure() { |
| synchronized (mSettingsLock) { |
| if (mLastScanSettings != null && mLastScanSettings.hwPnoScanActive) { |
| if (mLastScanSettings.pnoScanEventHandler != null) { |
| mLastScanSettings.pnoScanEventHandler.onPnoScanFailed(); |
| } |
| // Clean up PNO state, we don't want to continue PNO scanning. |
| mPnoSettings = null; |
| mPnoEventHandler = null; |
| mLastScanSettings = null; |
| } |
| } |
| } |
| |
| private void pollLatestScanData() { |
| synchronized (mSettingsLock) { |
| if (mLastScanSettings == null) { |
| // got a scan before we started scanning or after scan was canceled |
| return; |
| } |
| |
| if (DBG) Log.d(TAG, "Polling scan data for scan: " + mLastScanSettings.scanId); |
| ArrayList<ScanDetail> nativeResults = mWifiNative.getScanResults(); |
| List<ScanResult> singleScanResults = new ArrayList<>(); |
| List<ScanResult> backgroundScanResults = new ArrayList<>(); |
| List<ScanResult> hwPnoScanResults = new ArrayList<>(); |
| for (int i = 0; i < nativeResults.size(); ++i) { |
| ScanResult result = nativeResults.get(i).getScanResult(); |
| long timestamp_ms = result.timestamp / 1000; // convert us -> ms |
| if (timestamp_ms > mLastScanSettings.startTime) { |
| if (mLastScanSettings.backgroundScanActive) { |
| backgroundScanResults.add(result); |
| } |
| if (mLastScanSettings.singleScanActive |
| && mLastScanSettings.singleScanFreqs.containsChannel( |
| result.frequency)) { |
| singleScanResults.add(result); |
| } |
| if (mLastScanSettings.hwPnoScanActive) { |
| hwPnoScanResults.add(result); |
| } |
| } else { |
| // was a cached result in wpa_supplicant |
| } |
| } |
| |
| if (mLastScanSettings.backgroundScanActive) { |
| if (mBackgroundScanEventHandler != null) { |
| if ((mLastScanSettings.reportEvents |
| & WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT) != 0) { |
| for (ScanResult scanResult : backgroundScanResults) { |
| // TODO(b/27506257): Fill in correct bucketsScanned value |
| mBackgroundScanEventHandler.onFullScanResult(scanResult, 0); |
| } |
| } |
| } |
| |
| Collections.sort(backgroundScanResults, SCAN_RESULT_SORT_COMPARATOR); |
| ScanResult[] scanResultsArray = new ScanResult[Math.min(mLastScanSettings.maxAps, |
| backgroundScanResults.size())]; |
| for (int i = 0; i < scanResultsArray.length; ++i) { |
| scanResultsArray[i] = backgroundScanResults.get(i); |
| } |
| |
| if ((mLastScanSettings.reportEvents & WifiScanner.REPORT_EVENT_NO_BATCH) == 0) { |
| // TODO(b/27506257): Fill in correct bucketsScanned value |
| mBackgroundScanBuffer.add(new WifiScanner.ScanData(mLastScanSettings.scanId, 0, |
| scanResultsArray)); |
| } |
| |
| if (mBackgroundScanEventHandler != null) { |
| if ((mLastScanSettings.reportEvents |
| & WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT) != 0 |
| || (mLastScanSettings.reportEvents |
| & WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN) != 0 |
| || (mLastScanSettings.reportEvents |
| == WifiScanner.REPORT_EVENT_AFTER_BUFFER_FULL |
| && (mBackgroundScanBuffer.size() |
| >= (mBackgroundScanBuffer.capacity() |
| * mLastScanSettings.reportPercentThreshold |
| / 100) |
| || mBackgroundScanBuffer.size() |
| >= mLastScanSettings.reportNumScansThreshold))) { |
| mBackgroundScanEventHandler |
| .onScanStatus(WifiNative.WIFI_SCAN_RESULTS_AVAILABLE); |
| } |
| } |
| |
| if (mHotlistHandler != null) { |
| int event = mHotlistChangeBuffer.processScan(backgroundScanResults); |
| if ((event & ChangeBuffer.EVENT_FOUND) != 0) { |
| mHotlistHandler.onHotlistApFound( |
| mHotlistChangeBuffer.getLastResults(ChangeBuffer.EVENT_FOUND)); |
| } |
| if ((event & ChangeBuffer.EVENT_LOST) != 0) { |
| mHotlistHandler.onHotlistApLost( |
| mHotlistChangeBuffer.getLastResults(ChangeBuffer.EVENT_LOST)); |
| } |
| } |
| } |
| |
| if (mLastScanSettings.singleScanActive |
| && mLastScanSettings.singleScanEventHandler != null) { |
| if (mLastScanSettings.reportSingleScanFullResults) { |
| for (ScanResult scanResult : singleScanResults) { |
| // ignore buckets scanned since there is only one bucket for a single scan |
| mLastScanSettings.singleScanEventHandler.onFullScanResult(scanResult, |
| /* bucketsScanned */ 0); |
| } |
| } |
| Collections.sort(singleScanResults, SCAN_RESULT_SORT_COMPARATOR); |
| mLatestSingleScanResult = new WifiScanner.ScanData(mLastScanSettings.scanId, 0, 0, |
| mLastScanSettings.singleScanFreqs.isAllChannels(), |
| singleScanResults.toArray(new ScanResult[singleScanResults.size()])); |
| mLastScanSettings.singleScanEventHandler |
| .onScanStatus(WifiNative.WIFI_SCAN_RESULTS_AVAILABLE); |
| } |
| |
| if (mLastScanSettings.hwPnoScanActive |
| && mLastScanSettings.pnoScanEventHandler != null) { |
| ScanResult[] pnoScanResultsArray = new ScanResult[hwPnoScanResults.size()]; |
| for (int i = 0; i < pnoScanResultsArray.length; ++i) { |
| pnoScanResultsArray[i] = hwPnoScanResults.get(i); |
| } |
| mLastScanSettings.pnoScanEventHandler.onPnoNetworkFound(pnoScanResultsArray); |
| } |
| |
| mLastScanSettings = null; |
| } |
| } |
| |
| |
| @Override |
| public WifiScanner.ScanData[] getLatestBatchedScanResults(boolean flush) { |
| synchronized (mSettingsLock) { |
| WifiScanner.ScanData[] results = mBackgroundScanBuffer.get(); |
| if (flush) { |
| mBackgroundScanBuffer.clear(); |
| } |
| return results; |
| } |
| } |
| |
| private boolean setNetworkPriorities(WifiNative.PnoNetwork[] networkList) { |
| if (networkList != null) { |
| if (DBG) Log.i(TAG, "Enable network and Set priorities for PNO."); |
| for (WifiNative.PnoNetwork network : networkList) { |
| if (!mWifiNative.setNetworkVariable(network.networkId, |
| WifiConfiguration.priorityVarName, |
| Integer.toString(network.priority))) { |
| Log.e(TAG, "Set priority failed for: " + network.networkId); |
| return false; |
| } |
| if (!mWifiNative.enableNetworkWithoutConnect(network.networkId)) { |
| Log.e(TAG, "Enable network failed for: " + network.networkId); |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| private boolean startHwPnoScan() { |
| return mHwPnoDebouncer.startPnoScan(mHwPnoDebouncerListener); |
| } |
| |
| private void stopHwPnoScan() { |
| mHwPnoDebouncer.stopPnoScan(); |
| } |
| |
| private void pauseHwPnoScan() { |
| mHwPnoDebouncer.forceStopPnoScan(); |
| } |
| |
| private boolean restartHwPnoScan() { |
| mHwPnoDebouncer.forceStopPnoScan(); |
| return mHwPnoDebouncer.startPnoScan(mHwPnoDebouncerListener); |
| } |
| |
| /** |
| * Hw Pno Scan is required only for disconnected PNO when the device supports it. |
| * @param isConnectedPno Whether this is connected PNO vs disconnected PNO. |
| * @return true if HW PNO scan is required, false otherwise. |
| */ |
| private boolean isHwPnoScanRequired(boolean isConnectedPno) { |
| return (!isConnectedPno & mHwPnoScanSupported); |
| } |
| |
| private boolean isHwPnoScanRequired() { |
| if (mPnoSettings == null) return false; |
| return isHwPnoScanRequired(mPnoSettings.isConnected); |
| } |
| |
| @Override |
| public boolean setHwPnoList(WifiNative.PnoSettings settings, |
| WifiNative.PnoEventHandler eventHandler) { |
| synchronized (mSettingsLock) { |
| if (mPnoSettings != null) { |
| Log.w(TAG, "Already running a PNO scan"); |
| return false; |
| } |
| mPnoEventHandler = eventHandler; |
| mPnoSettings = settings; |
| if (!setNetworkPriorities(settings.networkList)) return false; |
| // For supplicant based PNO, we start the scan immediately when we set pno list. |
| processPendingScans(); |
| return true; |
| } |
| } |
| |
| @Override |
| public boolean resetHwPnoList() { |
| synchronized (mSettingsLock) { |
| if (mPnoSettings == null) { |
| Log.w(TAG, "No PNO scan running"); |
| return false; |
| } |
| mPnoEventHandler = null; |
| mPnoSettings = null; |
| // For supplicant based PNO, we stop the scan immediately when we reset pno list. |
| stopHwPnoScan(); |
| return true; |
| } |
| } |
| |
| @Override |
| public boolean isHwPnoSupported(boolean isConnectedPno) { |
| // Hw Pno Scan is supported only for disconnected PNO when the device supports it. |
| return isHwPnoScanRequired(isConnectedPno); |
| } |
| |
| @Override |
| public boolean shouldScheduleBackgroundScanForHwPno() { |
| return false; |
| } |
| |
| @Override |
| public boolean setHotlist(WifiScanner.HotlistSettings settings, |
| WifiNative.HotlistEventHandler eventHandler) { |
| if (settings == null || eventHandler == null) { |
| return false; |
| } |
| synchronized (mSettingsLock) { |
| mHotlistHandler = eventHandler; |
| mHotlistChangeBuffer.setSettings(settings.bssidInfos, settings.apLostThreshold, 1); |
| return true; |
| } |
| } |
| |
| @Override |
| public void resetHotlist() { |
| synchronized (mSettingsLock) { |
| mHotlistChangeBuffer.clearSettings(); |
| mHotlistHandler = null; |
| } |
| } |
| |
| /* |
| * Significant Wifi Change API is not implemented |
| */ |
| @Override |
| public boolean trackSignificantWifiChange(WifiScanner.WifiChangeSettings settings, |
| WifiNative.SignificantWifiChangeEventHandler handler) { |
| return false; |
| } |
| @Override |
| public void untrackSignificantWifiChange() {} |
| |
| |
| private static class LastScanSettings { |
| public long startTime; |
| |
| public LastScanSettings(long startTime) { |
| this.startTime = startTime; |
| } |
| |
| // Background settings |
| public boolean backgroundScanActive = false; |
| public int scanId; |
| public int maxAps; |
| public int reportEvents; |
| public int reportNumScansThreshold; |
| public int reportPercentThreshold; |
| |
| public void setBackgroundScan(int scanId, int maxAps, int reportEvents, |
| int reportNumScansThreshold, int reportPercentThreshold) { |
| this.backgroundScanActive = true; |
| this.scanId = scanId; |
| this.maxAps = maxAps; |
| this.reportEvents = reportEvents; |
| this.reportNumScansThreshold = reportNumScansThreshold; |
| this.reportPercentThreshold = reportPercentThreshold; |
| } |
| |
| // Single scan settings |
| public boolean singleScanActive = false; |
| public boolean reportSingleScanFullResults; |
| public ChannelCollection singleScanFreqs; |
| public WifiNative.ScanEventHandler singleScanEventHandler; |
| |
| public void setSingleScan(boolean reportSingleScanFullResults, |
| ChannelCollection singleScanFreqs, |
| WifiNative.ScanEventHandler singleScanEventHandler) { |
| singleScanActive = true; |
| this.reportSingleScanFullResults = reportSingleScanFullResults; |
| this.singleScanFreqs = singleScanFreqs; |
| this.singleScanEventHandler = singleScanEventHandler; |
| } |
| |
| public boolean hwPnoScanActive = false; |
| public WifiNative.PnoNetwork[] pnoNetworkList; |
| public WifiNative.PnoEventHandler pnoScanEventHandler; |
| |
| public void setHwPnoScan( |
| WifiNative.PnoNetwork[] pnoNetworkList, |
| WifiNative.PnoEventHandler pnoScanEventHandler) { |
| hwPnoScanActive = true; |
| this.pnoNetworkList = pnoNetworkList; |
| this.pnoScanEventHandler = pnoScanEventHandler; |
| } |
| } |
| |
| |
| private static class ScanBuffer { |
| private final ArrayDeque<WifiScanner.ScanData> mBuffer; |
| private int mCapacity; |
| |
| public ScanBuffer(int capacity) { |
| mCapacity = capacity; |
| mBuffer = new ArrayDeque<>(mCapacity); |
| } |
| |
| public int size() { |
| return mBuffer.size(); |
| } |
| |
| public int capacity() { |
| return mCapacity; |
| } |
| |
| public boolean isFull() { |
| return size() == mCapacity; |
| } |
| |
| public void add(WifiScanner.ScanData scanData) { |
| if (isFull()) { |
| mBuffer.pollFirst(); |
| } |
| mBuffer.offerLast(scanData); |
| } |
| |
| public void clear() { |
| mBuffer.clear(); |
| } |
| |
| public WifiScanner.ScanData[] get() { |
| return mBuffer.toArray(new WifiScanner.ScanData[mBuffer.size()]); |
| } |
| } |
| |
| private static class ChangeBuffer { |
| public static int EVENT_NONE = 0; |
| public static int EVENT_LOST = 1; |
| public static int EVENT_FOUND = 2; |
| |
| public static int STATE_FOUND = 0; |
| |
| private WifiScanner.BssidInfo[] mBssidInfos = null; |
| private int mApLostThreshold; |
| private int mMinEvents; |
| private int[] mLostCount = null; |
| private ScanResult[] mMostRecentResult = null; |
| private int[] mPendingEvent = null; |
| private boolean mFiredEvents = false; |
| |
| private static ScanResult findResult(List<ScanResult> results, String bssid) { |
| for (int i = 0; i < results.size(); ++i) { |
| if (bssid.equalsIgnoreCase(results.get(i).BSSID)) { |
| return results.get(i); |
| } |
| } |
| return null; |
| } |
| |
| public void setSettings(WifiScanner.BssidInfo[] bssidInfos, int apLostThreshold, |
| int minEvents) { |
| mBssidInfos = bssidInfos; |
| if (apLostThreshold <= 0) { |
| mApLostThreshold = 1; |
| } else { |
| mApLostThreshold = apLostThreshold; |
| } |
| mMinEvents = minEvents; |
| if (bssidInfos != null) { |
| mLostCount = new int[bssidInfos.length]; |
| Arrays.fill(mLostCount, mApLostThreshold); // default to lost |
| mMostRecentResult = new ScanResult[bssidInfos.length]; |
| mPendingEvent = new int[bssidInfos.length]; |
| mFiredEvents = false; |
| } else { |
| mLostCount = null; |
| mMostRecentResult = null; |
| mPendingEvent = null; |
| } |
| } |
| |
| public void clearSettings() { |
| setSettings(null, 0, 0); |
| } |
| |
| /** |
| * Get the most recent scan results for APs that triggered the given event on the last call |
| * to {@link #processScan}. |
| */ |
| public ScanResult[] getLastResults(int event) { |
| ArrayList<ScanResult> results = new ArrayList<>(); |
| for (int i = 0; i < mLostCount.length; ++i) { |
| if (mPendingEvent[i] == event) { |
| results.add(mMostRecentResult[i]); |
| } |
| } |
| return results.toArray(new ScanResult[results.size()]); |
| } |
| |
| /** |
| * Process the supplied scan results and determine if any events should be generated based |
| * on the configured settings |
| * @return The events that occurred |
| */ |
| public int processScan(List<ScanResult> scanResults) { |
| if (mBssidInfos == null) { |
| return EVENT_NONE; |
| } |
| |
| // clear events from last time |
| if (mFiredEvents) { |
| mFiredEvents = false; |
| for (int i = 0; i < mLostCount.length; ++i) { |
| mPendingEvent[i] = EVENT_NONE; |
| } |
| } |
| |
| int eventCount = 0; |
| int eventType = EVENT_NONE; |
| for (int i = 0; i < mLostCount.length; ++i) { |
| ScanResult result = findResult(scanResults, mBssidInfos[i].bssid); |
| int rssi = Integer.MIN_VALUE; |
| if (result != null) { |
| mMostRecentResult[i] = result; |
| rssi = result.level; |
| } |
| |
| if (rssi < mBssidInfos[i].low) { |
| if (mLostCount[i] < mApLostThreshold) { |
| mLostCount[i]++; |
| |
| if (mLostCount[i] >= mApLostThreshold) { |
| if (mPendingEvent[i] == EVENT_FOUND) { |
| mPendingEvent[i] = EVENT_NONE; |
| } else { |
| mPendingEvent[i] = EVENT_LOST; |
| } |
| } |
| } |
| } else { |
| if (mLostCount[i] >= mApLostThreshold) { |
| if (mPendingEvent[i] == EVENT_LOST) { |
| mPendingEvent[i] = EVENT_NONE; |
| } else { |
| mPendingEvent[i] = EVENT_FOUND; |
| } |
| } |
| mLostCount[i] = STATE_FOUND; |
| } |
| if (DBG) { |
| Log.d(TAG, "ChangeBuffer BSSID: " + mBssidInfos[i].bssid + "=" + mLostCount[i] |
| + ", " + mPendingEvent[i] + ", rssi=" + rssi); |
| } |
| if (mPendingEvent[i] != EVENT_NONE) { |
| ++eventCount; |
| eventType |= mPendingEvent[i]; |
| } |
| } |
| if (DBG) Log.d(TAG, "ChangeBuffer events count=" + eventCount + ": " + eventType); |
| if (eventCount >= mMinEvents) { |
| mFiredEvents = true; |
| return eventType; |
| } |
| return EVENT_NONE; |
| } |
| } |
| |
| /** |
| * HW PNO Debouncer is used to debounce PNO requests. This guards against toggling the PNO |
| * state too often which is not handled very well by some drivers. |
| * Note: This is not thread safe! |
| */ |
| public static class HwPnoDebouncer { |
| public static final String PNO_DEBOUNCER_ALARM_TAG = TAG + "Pno Monitor"; |
| private static final int MINIMUM_PNO_GAP_MS = 5 * 1000; |
| |
| private final WifiNative mWifiNative; |
| private final AlarmManager mAlarmManager; |
| private final Handler mEventHandler; |
| private final Clock mClock; |
| private long mLastPnoChangeTimeStamp = -1L; |
| private boolean mExpectedPnoState = false; |
| private boolean mCurrentPnoState = false;; |
| private boolean mWaitForTimer = false; |
| private Listener mListener; |
| |
| /** |
| * Interface used to indicate PNO scan notifications. |
| */ |
| public interface Listener { |
| /** |
| * Used to indicate a delayed PNO scan request failure. |
| */ |
| void onPnoScanFailed(); |
| } |
| |
| public HwPnoDebouncer(WifiNative wifiNative, AlarmManager alarmManager, |
| Handler eventHandler, Clock clock) { |
| mWifiNative = wifiNative; |
| mAlarmManager = alarmManager; |
| mEventHandler = eventHandler; |
| mClock = clock; |
| } |
| |
| /** |
| * Enable/Disable PNO state in wpa_supplicant |
| * @param enable boolean indicating whether PNO is being enabled or disabled. |
| */ |
| private boolean updatePnoState(boolean enable) { |
| if (mCurrentPnoState == enable) { |
| if (DBG) Log.d(TAG, "PNO state is already " + enable); |
| return true; |
| } |
| mLastPnoChangeTimeStamp = mClock.elapsedRealtime(); |
| if (mWifiNative.setPnoScan(enable)) { |
| Log.d(TAG, "Changed PNO state from " + mCurrentPnoState + " to " + enable); |
| mCurrentPnoState = enable; |
| return true; |
| } else { |
| Log.e(TAG, "PNO state change to " + enable + " failed"); |
| mCurrentPnoState = false; |
| return false; |
| } |
| } |
| |
| private final AlarmManager.OnAlarmListener mAlarmListener = |
| new AlarmManager.OnAlarmListener() { |
| public void onAlarm() { |
| if (DBG) Log.d(TAG, "PNO timer expired, expected state " + mExpectedPnoState); |
| if (!updatePnoState(mExpectedPnoState)) { |
| if (mListener != null) { |
| mListener.onPnoScanFailed(); |
| } |
| } |
| mWaitForTimer = false; |
| } |
| }; |
| |
| /** |
| * Enable/Disable PNO state. This method will debounce PNO scan requests. |
| * @param enable boolean indicating whether PNO is being enabled or disabled. |
| */ |
| private boolean setPnoState(boolean enable) { |
| boolean isSuccess = true; |
| mExpectedPnoState = enable; |
| if (!mWaitForTimer) { |
| long timeDifference = mClock.elapsedRealtime() - mLastPnoChangeTimeStamp; |
| if (timeDifference >= MINIMUM_PNO_GAP_MS) { |
| isSuccess = updatePnoState(enable); |
| } else { |
| long alarmTimeout = MINIMUM_PNO_GAP_MS - timeDifference; |
| Log.d(TAG, "Start PNO timer with delay " + alarmTimeout); |
| mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, |
| mClock.elapsedRealtime() + alarmTimeout, PNO_DEBOUNCER_ALARM_TAG, |
| mAlarmListener, mEventHandler); |
| mWaitForTimer = true; |
| } |
| } |
| return isSuccess; |
| } |
| |
| /** |
| * Start PNO scan |
| */ |
| public boolean startPnoScan(Listener listener) { |
| if (DBG) Log.d(TAG, "Starting PNO scan"); |
| mListener = listener; |
| if (!setPnoState(true)) { |
| mListener = null; |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Stop PNO scan |
| */ |
| public void stopPnoScan() { |
| if (DBG) Log.d(TAG, "Stopping PNO scan"); |
| setPnoState(false); |
| mListener = null; |
| } |
| |
| /** |
| * Force stop PNO scanning. This method will bypass the debounce logic and stop PNO |
| * scan immediately. |
| */ |
| public void forceStopPnoScan() { |
| if (DBG) Log.d(TAG, "Force stopping Pno scan"); |
| // Cancel the debounce timer and stop PNO scan. |
| if (mWaitForTimer) { |
| mAlarmManager.cancel(mAlarmListener); |
| mWaitForTimer = false; |
| } |
| updatePnoState(false); |
| } |
| } |
| } |