1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.wifi;
18 
19 import static com.android.server.wifi.WifiSettingsConfigStore.WIFI_SCAN_THROTTLE_ENABLED;
20 
21 import android.annotation.NonNull;
22 import android.app.ActivityManager;
23 import android.app.AppOpsManager;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.net.wifi.IScanResultsCallback;
27 import android.net.wifi.ScanResult;
28 import android.net.wifi.WifiManager;
29 import android.net.wifi.WifiScanner;
30 import android.os.Handler;
31 import android.os.HandlerExecutor;
32 import android.os.RemoteCallbackList;
33 import android.os.RemoteException;
34 import android.os.UserHandle;
35 import android.os.WorkSource;
36 import android.util.ArrayMap;
37 import android.util.Log;
38 import android.util.Pair;
39 
40 import com.android.internal.annotations.VisibleForTesting;
41 import com.android.server.wifi.util.WifiPermissionsUtil;
42 
43 import java.util.ArrayList;
44 import java.util.Arrays;
45 import java.util.Iterator;
46 import java.util.LinkedList;
47 import java.util.List;
48 
49 import javax.annotation.concurrent.NotThreadSafe;
50 
51 /**
52  * This class manages all scan requests originating from external apps using the
53  * {@link WifiManager#startScan()}.
54  *
55  * This class is responsible for:
56  * a) Enable/Disable scanning based on the request from {@link ActiveModeWarden}.
57  * a) Forwarding scan requests from {@link WifiManager#startScan()} to
58  * {@link WifiScanner#startScan(WifiScanner.ScanSettings, WifiScanner.ScanListener)}.
59  * Will essentially proxy scan requests from WifiService to WifiScanningService.
60  * b) Cache the results of these scan requests and return them when
61  * {@link WifiManager#getScanResults()} is invoked.
62  * c) Will send out the {@link WifiManager#SCAN_RESULTS_AVAILABLE_ACTION} broadcast when new
63  * scan results are available.
64  * d) Throttle scan requests from non-setting apps:
65  *  a) Each foreground app can request a max of
66  *   {@link #SCAN_REQUEST_THROTTLE_MAX_IN_TIME_WINDOW_FG_APPS} scan every
67  *   {@link #SCAN_REQUEST_THROTTLE_TIME_WINDOW_FG_APPS_MS}.
68  *  b) Background apps combined can request 1 scan every
69  *   {@link #SCAN_REQUEST_THROTTLE_INTERVAL_BG_APPS_MS}.
70  * Note: This class is not thread-safe. It needs to be invoked from the main Wifi thread only.
71  */
72 @NotThreadSafe
73 public class ScanRequestProxy {
74     private static final String TAG = "WifiScanRequestProxy";
75 
76     @VisibleForTesting
77     public static final int SCAN_REQUEST_THROTTLE_TIME_WINDOW_FG_APPS_MS = 120 * 1000;
78     @VisibleForTesting
79     public static final int SCAN_REQUEST_THROTTLE_MAX_IN_TIME_WINDOW_FG_APPS = 4;
80     @VisibleForTesting
81     public static final int SCAN_REQUEST_THROTTLE_INTERVAL_BG_APPS_MS = 30 * 60 * 1000;
82 
83     private final Context mContext;
84     private final Handler mHandler;
85     private final AppOpsManager mAppOps;
86     private final ActivityManager mActivityManager;
87     private final WifiInjector mWifiInjector;
88     private final WifiConfigManager mWifiConfigManager;
89     private final WifiPermissionsUtil mWifiPermissionsUtil;
90     private final WifiMetrics mWifiMetrics;
91     private final Clock mClock;
92     private final WifiSettingsConfigStore mSettingsConfigStore;
93     private WifiScanner mWifiScanner;
94 
95     // Verbose logging flag.
96     private boolean mVerboseLoggingEnabled = false;
97     private boolean mThrottleEnabled = true;
98     // Flag to decide if we need to scan or not.
99     private boolean mScanningEnabled = false;
100     // Flag to decide if we need to scan for hidden networks or not.
101     private boolean mScanningForHiddenNetworksEnabled = false;
102     // Timestamps for the last scan requested by any background app.
103     private long mLastScanTimestampForBgApps = 0;
104     // Timestamps for the list of last few scan requests by each foreground app.
105     // Keys in the map = Pair<Uid, PackageName> of the app.
106     // Values in the map = List of the last few scan request timestamps from the app.
107     private final ArrayMap<Pair<Integer, String>, LinkedList<Long>> mLastScanTimestampsForFgApps =
108             new ArrayMap();
109     // Scan results cached from the last full single scan request.
110     private final List<ScanResult> mLastScanResults = new ArrayList<>();
111     // external ScanResultCallback tracker
112     private final RemoteCallbackList<IScanResultsCallback> mRegisteredScanResultsCallbacks;
113     // Global scan listener for listening to all scan requests.
114     private class GlobalScanListener implements WifiScanner.ScanListener {
115         @Override
onSuccess()116         public void onSuccess() {
117             // Ignore. These will be processed from the scan request listener.
118         }
119 
120         @Override
onFailure(int reason, String description)121         public void onFailure(int reason, String description) {
122             // Ignore. These will be processed from the scan request listener.
123         }
124 
125         @Override
onResults(WifiScanner.ScanData[] scanDatas)126         public void onResults(WifiScanner.ScanData[] scanDatas) {
127             if (mVerboseLoggingEnabled) {
128                 Log.d(TAG, "Scan results received");
129             }
130             // For single scans, the array size should always be 1.
131             if (scanDatas.length != 1) {
132                 Log.wtf(TAG, "Found more than 1 batch of scan results, Failing...");
133                 sendScanResultBroadcast(false);
134                 return;
135             }
136             WifiScanner.ScanData scanData = scanDatas[0];
137             ScanResult[] scanResults = scanData.getResults();
138             if (mVerboseLoggingEnabled) {
139                 Log.d(TAG, "Received " + scanResults.length + " scan results");
140             }
141             // Only process full band scan results.
142             if (WifiScanner.isFullBandScan(scanData.getBandScanned(), false)) {
143                 // Store the last scan results & send out the scan completion broadcast.
144                 mLastScanResults.clear();
145                 mLastScanResults.addAll(Arrays.asList(scanResults));
146                 sendScanResultBroadcast(true);
147                 sendScanResultsAvailableToCallbacks();
148             }
149         }
150 
151         @Override
onFullResult(ScanResult fullScanResult)152         public void onFullResult(ScanResult fullScanResult) {
153             // Ignore for single scans.
154         }
155 
156         @Override
onPeriodChanged(int periodInMs)157         public void onPeriodChanged(int periodInMs) {
158             // Ignore for single scans.
159         }
160     };
161 
162     // Common scan listener for scan requests initiated by this class.
163     private class ScanRequestProxyScanListener implements WifiScanner.ScanListener {
164         @Override
onSuccess()165         public void onSuccess() {
166             // Scan request succeeded, wait for results to report to external clients.
167             if (mVerboseLoggingEnabled) {
168                 Log.d(TAG, "Scan request succeeded");
169             }
170         }
171 
172         @Override
onFailure(int reason, String description)173         public void onFailure(int reason, String description) {
174             Log.e(TAG, "Scan failure received. reason: " + reason + ",description: " + description);
175             sendScanResultBroadcast(false);
176         }
177 
178         @Override
onResults(WifiScanner.ScanData[] scanDatas)179         public void onResults(WifiScanner.ScanData[] scanDatas) {
180             // Ignore. These will be processed from the global listener.
181         }
182 
183         @Override
onFullResult(ScanResult fullScanResult)184         public void onFullResult(ScanResult fullScanResult) {
185             // Ignore for single scans.
186         }
187 
188         @Override
onPeriodChanged(int periodInMs)189         public void onPeriodChanged(int periodInMs) {
190             // Ignore for single scans.
191         }
192     };
193 
ScanRequestProxy(Context context, AppOpsManager appOpsManager, ActivityManager activityManager, WifiInjector wifiInjector, WifiConfigManager configManager, WifiPermissionsUtil wifiPermissionUtil, WifiMetrics wifiMetrics, Clock clock, Handler handler, WifiSettingsConfigStore settingsConfigStore)194     ScanRequestProxy(Context context, AppOpsManager appOpsManager, ActivityManager activityManager,
195                      WifiInjector wifiInjector, WifiConfigManager configManager,
196                      WifiPermissionsUtil wifiPermissionUtil, WifiMetrics wifiMetrics, Clock clock,
197                      Handler handler, WifiSettingsConfigStore settingsConfigStore) {
198         mContext = context;
199         mHandler = handler;
200         mAppOps = appOpsManager;
201         mActivityManager = activityManager;
202         mWifiInjector = wifiInjector;
203         mWifiConfigManager = configManager;
204         mWifiPermissionsUtil = wifiPermissionUtil;
205         mWifiMetrics = wifiMetrics;
206         mClock = clock;
207         mSettingsConfigStore = settingsConfigStore;
208         mRegisteredScanResultsCallbacks = new RemoteCallbackList<>();
209     }
210 
211     /**
212      * Enable verbose logging.
213      */
enableVerboseLogging(int verbose)214     public void enableVerboseLogging(int verbose) {
215         mVerboseLoggingEnabled = (verbose > 0);
216     }
217 
218     /**
219      * Helper method to populate WifiScanner handle. This is done lazily because
220      * WifiScanningService is started after WifiService.
221      */
retrieveWifiScannerIfNecessary()222     private boolean retrieveWifiScannerIfNecessary() {
223         if (mWifiScanner == null) {
224             mWifiScanner = mWifiInjector.getWifiScanner();
225             // Start listening for throttle settings change after we retrieve scanner instance.
226             mThrottleEnabled = mSettingsConfigStore.get(WIFI_SCAN_THROTTLE_ENABLED);
227             if (mVerboseLoggingEnabled) {
228                 Log.v(TAG, "Scan throttle enabled " + mThrottleEnabled);
229             }
230             // Register the global scan listener.
231             if (mWifiScanner != null) {
232                 mWifiScanner.registerScanListener(
233                         new HandlerExecutor(mHandler), new GlobalScanListener());
234             }
235         }
236         return mWifiScanner != null;
237     }
238 
239     /**
240      * Method that lets public apps know that scans are available.
241      *
242      * @param context Context to use for the notification
243      * @param available boolean indicating if scanning is available
244      */
sendScanAvailableBroadcast(Context context, boolean available)245     private void sendScanAvailableBroadcast(Context context, boolean available) {
246         Log.d(TAG, "Sending scan available broadcast: " + available);
247         final Intent intent = new Intent(WifiManager.ACTION_WIFI_SCAN_AVAILABILITY_CHANGED);
248         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
249         intent.putExtra(WifiManager.EXTRA_SCAN_AVAILABLE, available);
250         context.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
251     }
252 
enableScanningInternal(boolean enable)253     private void enableScanningInternal(boolean enable) {
254         if (!retrieveWifiScannerIfNecessary()) {
255             Log.e(TAG, "Failed to retrieve wifiscanner");
256             return;
257         }
258         mWifiScanner.setScanningEnabled(enable);
259         sendScanAvailableBroadcast(mContext, enable);
260         clearScanResults();
261         Log.i(TAG, "Scanning is " + (enable ? "enabled" : "disabled"));
262     }
263 
264     /**
265      * Enable/disable scanning.
266      *
267      * @param enable true to enable, false to disable.
268      * @param enableScanningForHiddenNetworks true to enable scanning for hidden networks,
269      *                                        false to disable.
270      */
enableScanning(boolean enable, boolean enableScanningForHiddenNetworks)271     public void enableScanning(boolean enable, boolean enableScanningForHiddenNetworks) {
272         if (enable) {
273             enableScanningInternal(true);
274             mScanningForHiddenNetworksEnabled = enableScanningForHiddenNetworks;
275             Log.i(TAG, "Scanning for hidden networks is "
276                     + (enableScanningForHiddenNetworks ? "enabled" : "disabled"));
277         } else {
278             enableScanningInternal(false);
279         }
280         mScanningEnabled = enable;
281     }
282 
283 
284     /**
285      * Helper method to send the scan request status broadcast.
286      */
sendScanResultBroadcast(boolean scanSucceeded)287     private void sendScanResultBroadcast(boolean scanSucceeded) {
288         Intent intent = new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
289         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
290         intent.putExtra(WifiManager.EXTRA_RESULTS_UPDATED, scanSucceeded);
291         mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
292     }
293 
294     /**
295      * Helper method to send the scan request failure broadcast to specified package.
296      */
sendScanResultFailureBroadcastToPackage(String packageName)297     private void sendScanResultFailureBroadcastToPackage(String packageName) {
298         Intent intent = new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
299         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
300         intent.putExtra(WifiManager.EXTRA_RESULTS_UPDATED, false);
301         intent.setPackage(packageName);
302         mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
303     }
304 
trimPastScanRequestTimesForForegroundApp( List<Long> scanRequestTimestamps, long currentTimeMillis)305     private void trimPastScanRequestTimesForForegroundApp(
306             List<Long> scanRequestTimestamps, long currentTimeMillis) {
307         Iterator<Long> timestampsIter = scanRequestTimestamps.iterator();
308         while (timestampsIter.hasNext()) {
309             Long scanRequestTimeMillis = timestampsIter.next();
310             if ((currentTimeMillis - scanRequestTimeMillis)
311                     > SCAN_REQUEST_THROTTLE_TIME_WINDOW_FG_APPS_MS) {
312                 timestampsIter.remove();
313             } else {
314                 // This list is sorted by timestamps, so we can skip any more checks
315                 break;
316             }
317         }
318     }
319 
getOrCreateScanRequestTimestampsForForegroundApp( int callingUid, String packageName)320     private LinkedList<Long> getOrCreateScanRequestTimestampsForForegroundApp(
321             int callingUid, String packageName) {
322         Pair<Integer, String> uidAndPackageNamePair = Pair.create(callingUid, packageName);
323         LinkedList<Long> scanRequestTimestamps =
324                 mLastScanTimestampsForFgApps.get(uidAndPackageNamePair);
325         if (scanRequestTimestamps == null) {
326             scanRequestTimestamps = new LinkedList<>();
327             mLastScanTimestampsForFgApps.put(uidAndPackageNamePair, scanRequestTimestamps);
328         }
329         return scanRequestTimestamps;
330     }
331 
332     /**
333      * Checks if the scan request from the app (specified by packageName) needs
334      * to be throttled.
335      * The throttle limit allows a max of {@link #SCAN_REQUEST_THROTTLE_MAX_IN_TIME_WINDOW_FG_APPS}
336      * in {@link #SCAN_REQUEST_THROTTLE_TIME_WINDOW_FG_APPS_MS} window.
337      */
shouldScanRequestBeThrottledForForegroundApp( int callingUid, String packageName)338     private boolean shouldScanRequestBeThrottledForForegroundApp(
339             int callingUid, String packageName) {
340         LinkedList<Long> scanRequestTimestamps =
341                 getOrCreateScanRequestTimestampsForForegroundApp(callingUid, packageName);
342         long currentTimeMillis = mClock.getElapsedSinceBootMillis();
343         // First evict old entries from the list.
344         trimPastScanRequestTimesForForegroundApp(scanRequestTimestamps, currentTimeMillis);
345         if (scanRequestTimestamps.size() >= SCAN_REQUEST_THROTTLE_MAX_IN_TIME_WINDOW_FG_APPS) {
346             return true;
347         }
348         // Proceed with the scan request and record the time.
349         scanRequestTimestamps.addLast(currentTimeMillis);
350         return false;
351     }
352 
353     /**
354      * Checks if the scan request from a background app needs to be throttled.
355      */
shouldScanRequestBeThrottledForBackgroundApp()356     private boolean shouldScanRequestBeThrottledForBackgroundApp() {
357         long lastScanMs = mLastScanTimestampForBgApps;
358         long elapsedRealtime = mClock.getElapsedSinceBootMillis();
359         if (lastScanMs != 0
360                 && (elapsedRealtime - lastScanMs) < SCAN_REQUEST_THROTTLE_INTERVAL_BG_APPS_MS) {
361             return true;
362         }
363         // Proceed with the scan request and record the time.
364         mLastScanTimestampForBgApps = elapsedRealtime;
365         return false;
366     }
367 
368     /**
369      * Check if the request comes from background app.
370      */
isRequestFromBackground(int callingUid, String packageName)371     private boolean isRequestFromBackground(int callingUid, String packageName) {
372         mAppOps.checkPackage(callingUid, packageName);
373         try {
374             return mActivityManager.getPackageImportance(packageName)
375                     > ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
376         } catch (SecurityException e) {
377             Log.e(TAG, "Failed to check the app state", e);
378             return true;
379         }
380     }
381 
382     /**
383      * Checks if the scan request from the app (specified by callingUid & packageName) needs
384      * to be throttled.
385      */
shouldScanRequestBeThrottledForApp(int callingUid, String packageName)386     private boolean shouldScanRequestBeThrottledForApp(int callingUid, String packageName) {
387         boolean isThrottled;
388         if (isRequestFromBackground(callingUid, packageName)) {
389             isThrottled = shouldScanRequestBeThrottledForBackgroundApp();
390             if (isThrottled) {
391                 if (mVerboseLoggingEnabled) {
392                     Log.v(TAG, "Background scan app request [" + callingUid + ", "
393                             + packageName + "]");
394                 }
395                 mWifiMetrics.incrementExternalBackgroundAppOneshotScanRequestsThrottledCount();
396             }
397         } else {
398             isThrottled = shouldScanRequestBeThrottledForForegroundApp(callingUid, packageName);
399             if (isThrottled) {
400                 if (mVerboseLoggingEnabled) {
401                     Log.v(TAG, "Foreground scan app request [" + callingUid + ", "
402                             + packageName + "]");
403                 }
404                 mWifiMetrics.incrementExternalForegroundAppOneshotScanRequestsThrottledCount();
405             }
406         }
407         mWifiMetrics.incrementExternalAppOneshotScanRequestsCount();
408         return isThrottled;
409     }
410 
411     /**
412      * Initiate a wifi scan.
413      *
414      * @param callingUid The uid initiating the wifi scan. Blame will be given to this uid.
415      * @return true if the scan request was placed or a scan is already ongoing, false otherwise.
416      */
startScan(int callingUid, String packageName)417     public boolean startScan(int callingUid, String packageName) {
418         if (!mScanningEnabled || !retrieveWifiScannerIfNecessary()) {
419             Log.e(TAG, "Failed to retrieve wifiscanner");
420             sendScanResultFailureBroadcastToPackage(packageName);
421             return false;
422         }
423         boolean fromSettingsOrSetupWizard =
424                 mWifiPermissionsUtil.checkNetworkSettingsPermission(callingUid)
425                         || mWifiPermissionsUtil.checkNetworkSetupWizardPermission(callingUid);
426         // Check and throttle scan request unless,
427         // a) App has either NETWORK_SETTINGS or NETWORK_SETUP_WIZARD permission.
428         // b) Throttling has been disabled by user.
429         if (!fromSettingsOrSetupWizard && mThrottleEnabled
430                 && shouldScanRequestBeThrottledForApp(callingUid, packageName)) {
431             Log.i(TAG, "Scan request from " + packageName + " throttled");
432             sendScanResultFailureBroadcastToPackage(packageName);
433             return false;
434         }
435         // Create a worksource using the caller's UID.
436         WorkSource workSource = new WorkSource(callingUid, packageName);
437 
438         // Create the scan settings.
439         WifiScanner.ScanSettings settings = new WifiScanner.ScanSettings();
440         // Scan requests from apps with network settings will be of high accuracy type.
441         if (fromSettingsOrSetupWizard) {
442             settings.type = WifiScanner.SCAN_TYPE_HIGH_ACCURACY;
443         }
444         // always do full scans
445         settings.band = WifiScanner.WIFI_BAND_ALL;
446         settings.reportEvents = WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN
447                 | WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT;
448         if (mScanningForHiddenNetworksEnabled) {
449             settings.hiddenNetworks.clear();
450             // retrieve the list of hidden network SSIDs from saved network to scan for, if enabled.
451             settings.hiddenNetworks.addAll(mWifiConfigManager.retrieveHiddenNetworkList());
452             // retrieve the list of hidden network SSIDs from Network suggestion to scan for.
453             settings.hiddenNetworks.addAll(
454                     mWifiInjector.getWifiNetworkSuggestionsManager().retrieveHiddenNetworkList());
455         }
456         mWifiScanner.startScan(settings, new HandlerExecutor(mHandler),
457                 new ScanRequestProxyScanListener(), workSource);
458         return true;
459     }
460 
461     /**
462      * Return the results of the most recent access point scan, in the form of
463      * a list of {@link ScanResult} objects.
464      * @return the list of results
465      */
getScanResults()466     public List<ScanResult> getScanResults() {
467         return mLastScanResults;
468     }
469 
470     /**
471      * Clear the stored scan results.
472      */
clearScanResults()473     private void clearScanResults() {
474         mLastScanResults.clear();
475         mLastScanTimestampForBgApps = 0;
476         mLastScanTimestampsForFgApps.clear();
477     }
478 
479     /**
480      * Clear any scan timestamps being stored for the app.
481      *
482      * @param uid Uid of the package.
483      * @param packageName Name of the package.
484      */
clearScanRequestTimestampsForApp(@onNull String packageName, int uid)485     public void clearScanRequestTimestampsForApp(@NonNull String packageName, int uid) {
486         if (mVerboseLoggingEnabled) {
487             Log.v(TAG, "Clearing scan request timestamps for uid=" + uid + ", packageName="
488                     + packageName);
489         }
490         mLastScanTimestampsForFgApps.remove(Pair.create(uid, packageName));
491     }
492 
sendScanResultsAvailableToCallbacks()493     private void sendScanResultsAvailableToCallbacks() {
494         int itemCount = mRegisteredScanResultsCallbacks.beginBroadcast();
495         for (int i = 0; i < itemCount; i++) {
496             try {
497                 mRegisteredScanResultsCallbacks.getBroadcastItem(i).onScanResultsAvailable();
498             } catch (RemoteException e) {
499                 Log.e(TAG, "onScanResultsAvailable: remote exception -- " + e);
500             }
501         }
502         mRegisteredScanResultsCallbacks.finishBroadcast();
503     }
504 
505     /**
506      * Register a callback on scan event
507      * @param callback IScanResultListener instance to add.
508      * @return true if succeed otherwise false.
509      */
registerScanResultsCallback(IScanResultsCallback callback)510     public boolean registerScanResultsCallback(IScanResultsCallback callback) {
511         return mRegisteredScanResultsCallbacks.register(callback);
512     }
513 
514     /**
515      * Unregister a callback on scan event
516      * @param callback IScanResultListener instance to add.
517      */
unregisterScanResultsCallback(IScanResultsCallback callback)518     public void unregisterScanResultsCallback(IScanResultsCallback callback) {
519         mRegisteredScanResultsCallbacks.unregister(callback);
520     }
521 
522     /**
523      * Enable/disable wifi scan throttling from 3rd party apps.
524      */
setScanThrottleEnabled(boolean enable)525     public void setScanThrottleEnabled(boolean enable) {
526         mThrottleEnabled = enable;
527         mSettingsConfigStore.put(WIFI_SCAN_THROTTLE_ENABLED, enable);
528         Log.i(TAG, "Scan throttle enabled " + mThrottleEnabled);
529     }
530 
531     /**
532      * Get the persisted Wi-Fi scan throttle state, set by
533      * {@link #setScanThrottleEnabled(boolean)}.
534      */
isScanThrottleEnabled()535     public boolean isScanThrottleEnabled() {
536         return mThrottleEnabled;
537     }
538 }
539