blob: efbb7b64917a3185304ae9009bc7d802fd47f997 [file] [log] [blame]
/*
* Copyright (C) 2018 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;
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiScanner;
import android.os.Handler;
import android.os.UserHandle;
import android.os.WorkSource;
import android.provider.Settings;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wifi.util.WifiPermissionsUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import javax.annotation.concurrent.NotThreadSafe;
/**
* This class manages all scan requests originating from external apps using the
* {@link WifiManager#startScan()}.
*
* This class is responsible for:
* a) Enable/Disable scanning based on the request from {@link ActiveModeWarden}.
* a) Forwarding scan requests from {@link WifiManager#startScan()} to
* {@link WifiScanner#startScan(WifiScanner.ScanSettings, WifiScanner.ScanListener)}.
* Will essentially proxy scan requests from WifiService to WifiScanningService.
* b) Cache the results of these scan requests and return them when
* {@link WifiManager#getScanResults()} is invoked.
* c) Will send out the {@link WifiManager#SCAN_RESULTS_AVAILABLE_ACTION} broadcast when new
* scan results are available.
* d) Throttle scan requests from non-setting apps:
* a) Each foreground app can request a max of
* {@link #SCAN_REQUEST_THROTTLE_MAX_IN_TIME_WINDOW_FG_APPS} scan every
* {@link #SCAN_REQUEST_THROTTLE_TIME_WINDOW_FG_APPS_MS}.
* b) Background apps combined can request 1 scan every
* {@link #SCAN_REQUEST_THROTTLE_INTERVAL_BG_APPS_MS}.
* Note: This class is not thread-safe. It needs to be invoked from ClientModeImpl thread only.
*/
@NotThreadSafe
public class ScanRequestProxy {
private static final String TAG = "WifiScanRequestProxy";
@VisibleForTesting
public static final int SCAN_REQUEST_THROTTLE_TIME_WINDOW_FG_APPS_MS = 120 * 1000;
@VisibleForTesting
public static final int SCAN_REQUEST_THROTTLE_MAX_IN_TIME_WINDOW_FG_APPS = 4;
@VisibleForTesting
public static final int SCAN_REQUEST_THROTTLE_INTERVAL_BG_APPS_MS = 30 * 60 * 1000;
private final Context mContext;
private final AppOpsManager mAppOps;
private final ActivityManager mActivityManager;
private final WifiInjector mWifiInjector;
private final WifiConfigManager mWifiConfigManager;
private final WifiPermissionsUtil mWifiPermissionsUtil;
private final WifiMetrics mWifiMetrics;
private final Clock mClock;
private final FrameworkFacade mFrameworkFacade;
private final ThrottleEnabledSettingObserver mThrottleEnabledSettingObserver;
private WifiScanner mWifiScanner;
// Verbose logging flag.
private boolean mVerboseLoggingEnabled = false;
// Flag to decide if we need to scan or not.
private boolean mScanningEnabled = false;
// Flag to decide if we need to scan for hidden networks or not.
private boolean mScanningForHiddenNetworksEnabled = false;
// Timestamps for the last scan requested by any background app.
private long mLastScanTimestampForBgApps = 0;
// Timestamps for the list of last few scan requests by each foreground app.
// Keys in the map = Pair<Uid, PackageName> of the app.
// Values in the map = List of the last few scan request timestamps from the app.
private final ArrayMap<Pair<Integer, String>, LinkedList<Long>> mLastScanTimestampsForFgApps =
new ArrayMap();
// Scan results cached from the last full single scan request.
private final List<ScanResult> mLastScanResults = new ArrayList<>();
// Global scan listener for listening to all scan requests.
private class GlobalScanListener implements WifiScanner.ScanListener {
@Override
public void onSuccess() {
// Ignore. These will be processed from the scan request listener.
}
@Override
public void onFailure(int reason, String description) {
// Ignore. These will be processed from the scan request listener.
}
@Override
public void onResults(WifiScanner.ScanData[] scanDatas) {
if (mVerboseLoggingEnabled) {
Log.d(TAG, "Scan results received");
}
// For single scans, the array size should always be 1.
if (scanDatas.length != 1) {
Log.wtf(TAG, "Found more than 1 batch of scan results, Failing...");
sendScanResultBroadcast(false);
return;
}
WifiScanner.ScanData scanData = scanDatas[0];
ScanResult[] scanResults = scanData.getResults();
if (mVerboseLoggingEnabled) {
Log.d(TAG, "Received " + scanResults.length + " scan results");
}
// Only process full band scan results.
if (scanData.getBandScanned() == WifiScanner.WIFI_BAND_BOTH_WITH_DFS) {
// Store the last scan results & send out the scan completion broadcast.
mLastScanResults.clear();
mLastScanResults.addAll(Arrays.asList(scanResults));
sendScanResultBroadcast(true);
}
}
@Override
public void onFullResult(ScanResult fullScanResult) {
// Ignore for single scans.
}
@Override
public void onPeriodChanged(int periodInMs) {
// Ignore for single scans.
}
};
// Common scan listener for scan requests initiated by this class.
private class ScanRequestProxyScanListener implements WifiScanner.ScanListener {
@Override
public void onSuccess() {
// Scan request succeeded, wait for results to report to external clients.
if (mVerboseLoggingEnabled) {
Log.d(TAG, "Scan request succeeded");
}
}
@Override
public void onFailure(int reason, String description) {
Log.e(TAG, "Scan failure received. reason: " + reason + ",description: " + description);
sendScanResultBroadcast(false);
}
@Override
public void onResults(WifiScanner.ScanData[] scanDatas) {
// Ignore. These will be processed from the global listener.
}
@Override
public void onFullResult(ScanResult fullScanResult) {
// Ignore for single scans.
}
@Override
public void onPeriodChanged(int periodInMs) {
// Ignore for single scans.
}
};
/**
* Observer for scan throttle enable settings changes.
* This is enabled by default. Will be toggled off via adb command or a developer settings
* toggle by the user to disable all scan throttling.
*/
private class ThrottleEnabledSettingObserver extends ContentObserver {
private boolean mThrottleEnabled = true;
ThrottleEnabledSettingObserver(Handler handler) {
super(handler);
}
/**
* Register for any changes to the scan throttle setting.
*/
public void initialize() {
mFrameworkFacade.registerContentObserver(mContext,
Settings.Global.getUriFor(Settings.Global.WIFI_SCAN_THROTTLE_ENABLED),
true, this);
mThrottleEnabled = getValue();
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Scan throttle enabled " + mThrottleEnabled);
}
}
/**
* Check if throttling is enabled or not.
*
* @return true if throttling is enabled, false otherwise.
*/
public boolean isEnabled() {
return mThrottleEnabled;
}
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
mThrottleEnabled = getValue();
Log.i(TAG, "Scan throttle enabled " + mThrottleEnabled);
}
private boolean getValue() {
return mFrameworkFacade.getIntegerSetting(mContext,
Settings.Global.WIFI_SCAN_THROTTLE_ENABLED, 1) == 1;
}
}
ScanRequestProxy(Context context, AppOpsManager appOpsManager, ActivityManager activityManager,
WifiInjector wifiInjector, WifiConfigManager configManager,
WifiPermissionsUtil wifiPermissionUtil, WifiMetrics wifiMetrics, Clock clock,
FrameworkFacade frameworkFacade, Handler handler) {
mContext = context;
mAppOps = appOpsManager;
mActivityManager = activityManager;
mWifiInjector = wifiInjector;
mWifiConfigManager = configManager;
mWifiPermissionsUtil = wifiPermissionUtil;
mWifiMetrics = wifiMetrics;
mClock = clock;
mFrameworkFacade = frameworkFacade;
mThrottleEnabledSettingObserver = new ThrottleEnabledSettingObserver(handler);
}
/**
* Enable verbose logging.
*/
public void enableVerboseLogging(int verbose) {
mVerboseLoggingEnabled = (verbose > 0);
}
/**
* Helper method to populate WifiScanner handle. This is done lazily because
* WifiScanningService is started after WifiService.
*/
private boolean retrieveWifiScannerIfNecessary() {
if (mWifiScanner == null) {
mWifiScanner = mWifiInjector.getWifiScanner();
// Start listening for throttle settings change after we retrieve scanner instance.
mThrottleEnabledSettingObserver.initialize();
// Register the global scan listener.
if (mWifiScanner != null) {
mWifiScanner.registerScanListener(new GlobalScanListener());
}
}
return mWifiScanner != null;
}
/**
* Method that lets public apps know that scans are available.
*
* @param context Context to use for the notification
* @param available boolean indicating if scanning is available
*/
private void sendScanAvailableBroadcast(Context context, boolean available) {
Log.d(TAG, "Sending scan available broadcast: " + available);
final Intent intent = new Intent(WifiManager.WIFI_SCAN_AVAILABLE);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
if (available) {
intent.putExtra(WifiManager.EXTRA_SCAN_AVAILABLE, WifiManager.WIFI_STATE_ENABLED);
} else {
intent.putExtra(WifiManager.EXTRA_SCAN_AVAILABLE, WifiManager.WIFI_STATE_DISABLED);
}
context.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
}
private void enableScanningInternal(boolean enable) {
if (!retrieveWifiScannerIfNecessary()) {
Log.e(TAG, "Failed to retrieve wifiscanner");
return;
}
mWifiScanner.setScanningEnabled(enable);
sendScanAvailableBroadcast(mContext, enable);
clearScanResults();
Log.i(TAG, "Scanning is " + (enable ? "enabled" : "disabled"));
}
/**
* Enable/disable scanning.
*
* @param enable true to enable, false to disable.
* @param enableScanningForHiddenNetworks true to enable scanning for hidden networks,
* false to disable.
*/
public void enableScanning(boolean enable, boolean enableScanningForHiddenNetworks) {
if (enable) {
enableScanningInternal(true);
mScanningForHiddenNetworksEnabled = enableScanningForHiddenNetworks;
Log.i(TAG, "Scanning for hidden networks is "
+ (enableScanningForHiddenNetworks ? "enabled" : "disabled"));
} else {
enableScanningInternal(false);
}
mScanningEnabled = enable;
}
/**
* Helper method to send the scan request status broadcast.
*/
private void sendScanResultBroadcast(boolean scanSucceeded) {
Intent intent = new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
intent.putExtra(WifiManager.EXTRA_RESULTS_UPDATED, scanSucceeded);
mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
}
/**
* Helper method to send the scan request failure broadcast to specified package.
*/
private void sendScanResultFailureBroadcastToPackage(String packageName) {
Intent intent = new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
intent.putExtra(WifiManager.EXTRA_RESULTS_UPDATED, false);
intent.setPackage(packageName);
mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
}
private void trimPastScanRequestTimesForForegroundApp(
List<Long> scanRequestTimestamps, long currentTimeMillis) {
Iterator<Long> timestampsIter = scanRequestTimestamps.iterator();
while (timestampsIter.hasNext()) {
Long scanRequestTimeMillis = timestampsIter.next();
if ((currentTimeMillis - scanRequestTimeMillis)
> SCAN_REQUEST_THROTTLE_TIME_WINDOW_FG_APPS_MS) {
timestampsIter.remove();
} else {
// This list is sorted by timestamps, so we can skip any more checks
break;
}
}
}
private LinkedList<Long> getOrCreateScanRequestTimestampsForForegroundApp(
int callingUid, String packageName) {
Pair<Integer, String> uidAndPackageNamePair = Pair.create(callingUid, packageName);
LinkedList<Long> scanRequestTimestamps =
mLastScanTimestampsForFgApps.get(uidAndPackageNamePair);
if (scanRequestTimestamps == null) {
scanRequestTimestamps = new LinkedList<>();
mLastScanTimestampsForFgApps.put(uidAndPackageNamePair, scanRequestTimestamps);
}
return scanRequestTimestamps;
}
/**
* Checks if the scan request from the app (specified by packageName) needs
* to be throttled.
* The throttle limit allows a max of {@link #SCAN_REQUEST_THROTTLE_MAX_IN_TIME_WINDOW_FG_APPS}
* in {@link #SCAN_REQUEST_THROTTLE_TIME_WINDOW_FG_APPS_MS} window.
*/
private boolean shouldScanRequestBeThrottledForForegroundApp(
int callingUid, String packageName) {
LinkedList<Long> scanRequestTimestamps =
getOrCreateScanRequestTimestampsForForegroundApp(callingUid, packageName);
long currentTimeMillis = mClock.getElapsedSinceBootMillis();
// First evict old entries from the list.
trimPastScanRequestTimesForForegroundApp(scanRequestTimestamps, currentTimeMillis);
if (scanRequestTimestamps.size() >= SCAN_REQUEST_THROTTLE_MAX_IN_TIME_WINDOW_FG_APPS) {
return true;
}
// Proceed with the scan request and record the time.
scanRequestTimestamps.addLast(currentTimeMillis);
return false;
}
/**
* Checks if the scan request from a background app needs to be throttled.
*/
private boolean shouldScanRequestBeThrottledForBackgroundApp() {
long lastScanMs = mLastScanTimestampForBgApps;
long elapsedRealtime = mClock.getElapsedSinceBootMillis();
if (lastScanMs != 0
&& (elapsedRealtime - lastScanMs) < SCAN_REQUEST_THROTTLE_INTERVAL_BG_APPS_MS) {
return true;
}
// Proceed with the scan request and record the time.
mLastScanTimestampForBgApps = elapsedRealtime;
return false;
}
/**
* Check if the request comes from background app.
*/
private boolean isRequestFromBackground(int callingUid, String packageName) {
mAppOps.checkPackage(callingUid, packageName);
try {
return mActivityManager.getPackageImportance(packageName)
> ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
} catch (SecurityException e) {
Log.e(TAG, "Failed to check the app state", e);
return true;
}
}
/**
* Checks if the scan request from the app (specified by callingUid & packageName) needs
* to be throttled.
*/
private boolean shouldScanRequestBeThrottledForApp(int callingUid, String packageName) {
boolean isThrottled;
if (isRequestFromBackground(callingUid, packageName)) {
isThrottled = shouldScanRequestBeThrottledForBackgroundApp();
if (isThrottled) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Background scan app request [" + callingUid + ", "
+ packageName + "]");
}
mWifiMetrics.incrementExternalBackgroundAppOneshotScanRequestsThrottledCount();
}
} else {
isThrottled = shouldScanRequestBeThrottledForForegroundApp(callingUid, packageName);
if (isThrottled) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Foreground scan app request [" + callingUid + ", "
+ packageName + "]");
}
mWifiMetrics.incrementExternalForegroundAppOneshotScanRequestsThrottledCount();
}
}
mWifiMetrics.incrementExternalAppOneshotScanRequestsCount();
return isThrottled;
}
/**
* Initiate a wifi scan.
*
* @param callingUid The uid initiating the wifi scan. Blame will be given to this uid.
* @return true if the scan request was placed or a scan is already ongoing, false otherwise.
*/
public boolean startScan(int callingUid, String packageName) {
if (!retrieveWifiScannerIfNecessary()) {
Log.e(TAG, "Failed to retrieve wifiscanner");
sendScanResultFailureBroadcastToPackage(packageName);
return false;
}
boolean fromSettingsOrSetupWizard =
mWifiPermissionsUtil.checkNetworkSettingsPermission(callingUid)
|| mWifiPermissionsUtil.checkNetworkSetupWizardPermission(callingUid);
// Check and throttle scan request unless,
// a) App has either NETWORK_SETTINGS or NETWORK_SETUP_WIZARD permission.
// b) Throttling has been disabled by user.
if (!fromSettingsOrSetupWizard && mThrottleEnabledSettingObserver.isEnabled()
&& shouldScanRequestBeThrottledForApp(callingUid, packageName)) {
Log.i(TAG, "Scan request from " + packageName + " throttled");
sendScanResultFailureBroadcastToPackage(packageName);
return false;
}
// Create a worksource using the caller's UID.
WorkSource workSource = new WorkSource(callingUid, packageName);
// Create the scan settings.
WifiScanner.ScanSettings settings = new WifiScanner.ScanSettings();
// Scan requests from apps with network settings will be of high accuracy type.
if (fromSettingsOrSetupWizard) {
settings.type = WifiScanner.TYPE_HIGH_ACCURACY;
}
// always do full scans
settings.band = WifiScanner.WIFI_BAND_BOTH_WITH_DFS;
settings.reportEvents = WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN
| WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT;
if (mScanningForHiddenNetworksEnabled) {
// retrieve the list of hidden network SSIDs to scan for, if enabled.
List<WifiScanner.ScanSettings.HiddenNetwork> hiddenNetworkList =
mWifiConfigManager.retrieveHiddenNetworkList();
settings.hiddenNetworks = hiddenNetworkList.toArray(
new WifiScanner.ScanSettings.HiddenNetwork[hiddenNetworkList.size()]);
}
mWifiScanner.startScan(settings, new ScanRequestProxyScanListener(), workSource);
return true;
}
/**
* Return the results of the most recent access point scan, in the form of
* a list of {@link ScanResult} objects.
* @return the list of results
*/
public List<ScanResult> getScanResults() {
return mLastScanResults;
}
/**
* Clear the stored scan results.
*/
private void clearScanResults() {
mLastScanResults.clear();
mLastScanTimestampForBgApps = 0;
mLastScanTimestampsForFgApps.clear();
}
/**
* Clear any scan timestamps being stored for the app.
*
* @param uid Uid of the package.
* @param packageName Name of the package.
*/
public void clearScanRequestTimestampsForApp(@NonNull String packageName, int uid) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Clearing scan request timestamps for uid=" + uid + ", packageName="
+ packageName);
}
mLastScanTimestampsForFgApps.remove(Pair.create(uid, packageName));
}
}