1 /* 2 * Copyright 2017 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.WifiController.CMD_WIFI_TOGGLED; 20 21 import android.content.Context; 22 import android.database.ContentObserver; 23 import android.net.wifi.ScanResult; 24 import android.net.wifi.WifiConfiguration; 25 import android.net.wifi.WifiNetworkSuggestion; 26 import android.net.wifi.WifiScanner; 27 import android.os.Handler; 28 import android.os.Looper; 29 import android.os.Process; 30 import android.provider.Settings; 31 import android.util.Log; 32 33 import com.android.internal.annotations.VisibleForTesting; 34 35 import java.io.FileDescriptor; 36 import java.io.PrintWriter; 37 import java.util.Arrays; 38 import java.util.Collection; 39 import java.util.HashSet; 40 import java.util.List; 41 import java.util.Set; 42 import java.util.stream.Collectors; 43 44 45 /** 46 * WakeupController is responsible managing Auto Wifi. 47 * 48 * <p>It determines if and when to re-enable wifi after it has been turned off by the user. 49 */ 50 public class WakeupController { 51 52 private static final String TAG = "WakeupController"; 53 54 private static final boolean USE_PLATFORM_WIFI_WAKE = true; 55 56 private final Context mContext; 57 private final Handler mHandler; 58 private final FrameworkFacade mFrameworkFacade; 59 private final ContentObserver mContentObserver; 60 private final WakeupLock mWakeupLock; 61 private final WakeupEvaluator mWakeupEvaluator; 62 private final WakeupOnboarding mWakeupOnboarding; 63 private final WifiConfigManager mWifiConfigManager; 64 private final WifiNetworkSuggestionsManager mWifiNetworkSuggestionsManager; 65 private final WifiInjector mWifiInjector; 66 private final WakeupConfigStoreData mWakeupConfigStoreData; 67 private final WifiWakeMetrics mWifiWakeMetrics; 68 private final Clock mClock; 69 70 private final WifiScanner.ScanListener mScanListener = new WifiScanner.ScanListener() { 71 @Override 72 public void onPeriodChanged(int periodInMs) { 73 // no-op 74 } 75 76 @Override 77 public void onResults(WifiScanner.ScanData[] results) { 78 // We treat any full band scans (with DFS or not) as "full". 79 boolean isFullBandScanResults = 80 results[0].getBandScanned() == WifiScanner.WIFI_BAND_BOTH_WITH_DFS 81 || results[0].getBandScanned() == WifiScanner.WIFI_BAND_BOTH; 82 if (results.length == 1 && isFullBandScanResults) { 83 handleScanResults(filterDfsScanResults(Arrays.asList(results[0].getResults()))); 84 } 85 } 86 87 @Override 88 public void onFullResult(ScanResult fullScanResult) { 89 // no-op 90 } 91 92 @Override 93 public void onSuccess() { 94 // no-op 95 } 96 97 @Override 98 public void onFailure(int reason, String description) { 99 Log.e(TAG, "ScanListener onFailure: " + reason + ": " + description); 100 } 101 }; 102 103 /** Whether this feature is enabled in Settings. */ 104 private boolean mWifiWakeupEnabled; 105 106 /** Whether the WakeupController is currently active. */ 107 private boolean mIsActive = false; 108 109 /** The number of scans that have been handled by the controller since last {@link #reset()}. */ 110 private int mNumScansHandled = 0; 111 112 /** Whether Wifi verbose logging is enabled. */ 113 private boolean mVerboseLoggingEnabled; 114 115 /** 116 * The timestamp of when the Wifi network was last disconnected (either device disconnected 117 * from the network or Wifi was turned off entirely). 118 * Note: mLastDisconnectTimestampMillis and mLastDisconnectInfo must always be updated together. 119 */ 120 private long mLastDisconnectTimestampMillis; 121 122 /** 123 * The SSID of the last Wifi network the device was connected to (either device disconnected 124 * from the network or Wifi was turned off entirely). 125 * Note: mLastDisconnectTimestampMillis and mLastDisconnectInfo must always be updated together. 126 */ 127 private ScanResultMatchInfo mLastDisconnectInfo; 128 WakeupController( Context context, Looper looper, WakeupLock wakeupLock, WakeupEvaluator wakeupEvaluator, WakeupOnboarding wakeupOnboarding, WifiConfigManager wifiConfigManager, WifiConfigStore wifiConfigStore, WifiNetworkSuggestionsManager wifiNetworkSuggestionsManager, WifiWakeMetrics wifiWakeMetrics, WifiInjector wifiInjector, FrameworkFacade frameworkFacade, Clock clock)129 public WakeupController( 130 Context context, 131 Looper looper, 132 WakeupLock wakeupLock, 133 WakeupEvaluator wakeupEvaluator, 134 WakeupOnboarding wakeupOnboarding, 135 WifiConfigManager wifiConfigManager, 136 WifiConfigStore wifiConfigStore, 137 WifiNetworkSuggestionsManager wifiNetworkSuggestionsManager, 138 WifiWakeMetrics wifiWakeMetrics, 139 WifiInjector wifiInjector, 140 FrameworkFacade frameworkFacade, 141 Clock clock) { 142 mContext = context; 143 mHandler = new Handler(looper); 144 mWakeupLock = wakeupLock; 145 mWakeupEvaluator = wakeupEvaluator; 146 mWakeupOnboarding = wakeupOnboarding; 147 mWifiConfigManager = wifiConfigManager; 148 mWifiNetworkSuggestionsManager = wifiNetworkSuggestionsManager; 149 mWifiWakeMetrics = wifiWakeMetrics; 150 mFrameworkFacade = frameworkFacade; 151 mWifiInjector = wifiInjector; 152 mContentObserver = new ContentObserver(mHandler) { 153 @Override 154 public void onChange(boolean selfChange) { 155 readWifiWakeupEnabledFromSettings(); 156 mWakeupOnboarding.setOnboarded(); 157 } 158 }; 159 mFrameworkFacade.registerContentObserver(mContext, Settings.Global.getUriFor( 160 Settings.Global.WIFI_WAKEUP_ENABLED), true, mContentObserver); 161 readWifiWakeupEnabledFromSettings(); 162 163 // registering the store data here has the effect of reading the persisted value of the 164 // data sources after system boot finishes 165 mWakeupConfigStoreData = new WakeupConfigStoreData( 166 new IsActiveDataSource(), 167 mWakeupOnboarding.getIsOnboadedDataSource(), 168 mWakeupOnboarding.getNotificationsDataSource(), 169 mWakeupLock.getDataSource()); 170 wifiConfigStore.registerStoreData(mWakeupConfigStoreData); 171 mClock = clock; 172 mLastDisconnectTimestampMillis = 0; 173 mLastDisconnectInfo = null; 174 } 175 readWifiWakeupEnabledFromSettings()176 private void readWifiWakeupEnabledFromSettings() { 177 mWifiWakeupEnabled = mFrameworkFacade.getIntegerSetting( 178 mContext, Settings.Global.WIFI_WAKEUP_ENABLED, 0) == 1; 179 Log.d(TAG, "WifiWake " + (mWifiWakeupEnabled ? "enabled" : "disabled")); 180 } 181 setActive(boolean isActive)182 private void setActive(boolean isActive) { 183 if (mIsActive != isActive) { 184 Log.d(TAG, "Setting active to " + isActive); 185 mIsActive = isActive; 186 mWifiConfigManager.saveToStore(false /* forceWrite */); 187 } 188 } 189 190 /** 191 * Saves the SSID of the last Wifi network that was disconnected. Should only be called before 192 * WakeupController is active. 193 */ setLastDisconnectInfo(ScanResultMatchInfo scanResultMatchInfo)194 public void setLastDisconnectInfo(ScanResultMatchInfo scanResultMatchInfo) { 195 if (mIsActive) { 196 Log.e(TAG, "Unexpected setLastDisconnectInfo when WakeupController is active!"); 197 return; 198 } 199 if (scanResultMatchInfo == null) { 200 Log.e(TAG, "Unexpected setLastDisconnectInfo(null)"); 201 return; 202 } 203 mLastDisconnectTimestampMillis = mClock.getElapsedSinceBootMillis(); 204 mLastDisconnectInfo = scanResultMatchInfo; 205 if (mVerboseLoggingEnabled) { 206 Log.d(TAG, "mLastDisconnectInfo set to " + scanResultMatchInfo); 207 } 208 } 209 210 /** 211 * If Wifi was disabled within LAST_DISCONNECT_TIMEOUT_MILLIS of losing a Wifi connection, 212 * add that Wifi connection to the Wakeup Lock as if Wifi was disabled while connected to that 213 * connection. 214 * Often times, networks with poor signal intermittently connect and disconnect, causing the 215 * user to manually turn off Wifi. If the Wifi was turned off during the disconnected phase of 216 * the intermittent connection, then that connection normally would not be added to the Wakeup 217 * Lock. This constant defines the timeout after disconnecting, in milliseconds, within which 218 * if Wifi was disabled, the network would still be added to the wakeup lock. 219 */ 220 @VisibleForTesting 221 static final long LAST_DISCONNECT_TIMEOUT_MILLIS = 5 * 1000; 222 223 /** 224 * Starts listening for incoming scans. 225 * 226 * <p>Should only be called upon entering ScanMode. WakeupController registers its listener with 227 * the WifiScanner. If the WakeupController is already active, then it returns early. Otherwise 228 * it performs its initialization steps and sets {@link #mIsActive} to true. 229 */ start()230 public void start() { 231 Log.d(TAG, "start()"); 232 mWifiInjector.getWifiScanner().registerScanListener(mScanListener); 233 234 // If already active, we don't want to restart the session, so return early. 235 if (mIsActive) { 236 mWifiWakeMetrics.recordIgnoredStart(); 237 return; 238 } 239 setActive(true); 240 241 // ensure feature is enabled and store data has been read before performing work 242 if (isEnabled()) { 243 mWakeupOnboarding.maybeShowNotification(); 244 245 List<ScanResult> scanResults = 246 filterDfsScanResults(mWifiInjector.getWifiScanner().getSingleScanResults()); 247 Set<ScanResultMatchInfo> matchInfos = toMatchInfos(scanResults); 248 matchInfos.retainAll(getGoodSavedNetworksAndSuggestions()); 249 250 // ensure that the last disconnected network is added to the wakeup lock, since we don't 251 // want to automatically reconnect to the same network that the user manually 252 // disconnected from 253 long now = mClock.getElapsedSinceBootMillis(); 254 if (mLastDisconnectInfo != null && ((now - mLastDisconnectTimestampMillis) 255 <= LAST_DISCONNECT_TIMEOUT_MILLIS)) { 256 matchInfos.add(mLastDisconnectInfo); 257 if (mVerboseLoggingEnabled) { 258 Log.d(TAG, "Added last connected network to lock: " + mLastDisconnectInfo); 259 } 260 } 261 262 if (mVerboseLoggingEnabled) { 263 Log.d(TAG, "Saved networks in most recent scan:" + matchInfos); 264 } 265 266 mWifiWakeMetrics.recordStartEvent(matchInfos.size()); 267 mWakeupLock.setLock(matchInfos); 268 // TODO(b/77291248): request low latency scan here 269 } 270 } 271 272 /** 273 * Stops listening for scans. 274 * 275 * <p>Should only be called upon leaving ScanMode. It deregisters the listener from 276 * WifiScanner. 277 */ stop()278 public void stop() { 279 Log.d(TAG, "stop()"); 280 mLastDisconnectTimestampMillis = 0; 281 mLastDisconnectInfo = null; 282 mWifiInjector.getWifiScanner().deregisterScanListener(mScanListener); 283 mWakeupOnboarding.onStop(); 284 } 285 286 /** Resets the WakeupController, setting {@link #mIsActive} to false. */ reset()287 public void reset() { 288 Log.d(TAG, "reset()"); 289 mWifiWakeMetrics.recordResetEvent(mNumScansHandled); 290 mNumScansHandled = 0; 291 setActive(false); 292 } 293 294 /** Sets verbose logging flag based on verbose level. */ enableVerboseLogging(int verbose)295 public void enableVerboseLogging(int verbose) { 296 mVerboseLoggingEnabled = verbose > 0; 297 mWakeupLock.enableVerboseLogging(mVerboseLoggingEnabled); 298 } 299 300 /** Returns a list of ScanResults with DFS channels removed. */ filterDfsScanResults(Collection<ScanResult> scanResults)301 private List<ScanResult> filterDfsScanResults(Collection<ScanResult> scanResults) { 302 int[] dfsChannels = mWifiInjector.getWifiNative() 303 .getChannelsForBand(WifiScanner.WIFI_BAND_5_GHZ_DFS_ONLY); 304 if (dfsChannels == null) { 305 dfsChannels = new int[0]; 306 } 307 308 final Set<Integer> dfsChannelSet = Arrays.stream(dfsChannels).boxed() 309 .collect(Collectors.toSet()); 310 311 return scanResults.stream() 312 .filter(scanResult -> !dfsChannelSet.contains(scanResult.frequency)) 313 .collect(Collectors.toList()); 314 } 315 316 /** Returns a filtered set of saved networks from WifiConfigManager & suggestions 317 * from WifiNetworkSuggestionsManager. */ getGoodSavedNetworksAndSuggestions()318 private Set<ScanResultMatchInfo> getGoodSavedNetworksAndSuggestions() { 319 List<WifiConfiguration> savedNetworks = mWifiConfigManager.getSavedNetworks( 320 Process.WIFI_UID); 321 322 Set<ScanResultMatchInfo> goodNetworks = new HashSet<>(savedNetworks.size()); 323 for (WifiConfiguration config : savedNetworks) { 324 if (isWideAreaNetwork(config) 325 || config.hasNoInternetAccess() 326 || config.noInternetAccessExpected 327 || !config.getNetworkSelectionStatus().getHasEverConnected()) { 328 continue; 329 } 330 goodNetworks.add(ScanResultMatchInfo.fromWifiConfiguration(config)); 331 } 332 333 Set<WifiNetworkSuggestion> networkSuggestions = 334 mWifiNetworkSuggestionsManager.getAllNetworkSuggestions(); 335 for (WifiNetworkSuggestion suggestion : networkSuggestions) { 336 // TODO(b/127799111): Do we need to filter the list similar to saved networks above? 337 goodNetworks.add( 338 ScanResultMatchInfo.fromWifiConfiguration(suggestion.wifiConfiguration)); 339 } 340 return goodNetworks; 341 } 342 343 //TODO(b/69271702) implement WAN filtering isWideAreaNetwork(WifiConfiguration config)344 private static boolean isWideAreaNetwork(WifiConfiguration config) { 345 return false; 346 } 347 348 /** 349 * Handles incoming scan results. 350 * 351 * <p>The controller updates the WakeupLock with the incoming scan results. If WakeupLock is not 352 * yet fully initialized, it adds the current scanResults to the lock and returns. If WakeupLock 353 * is initialized but not empty, the controller updates the lock with the current scan. If it is 354 * both initialized and empty, it evaluates scan results for a match with saved networks. If a 355 * match exists, it enables wifi. 356 * 357 * <p>The feature must be enabled and the store data must be loaded in order for the controller 358 * to handle scan results. 359 * 360 * @param scanResults The scan results with which to update the controller 361 */ handleScanResults(Collection<ScanResult> scanResults)362 private void handleScanResults(Collection<ScanResult> scanResults) { 363 if (!isEnabled()) { 364 Log.d(TAG, "Attempted to handleScanResults while not enabled"); 365 return; 366 } 367 368 // only count scan as handled if isEnabled 369 mNumScansHandled++; 370 if (mVerboseLoggingEnabled) { 371 Log.d(TAG, "Incoming scan #" + mNumScansHandled); 372 } 373 374 // need to show notification here in case user turns phone on while wifi is off 375 mWakeupOnboarding.maybeShowNotification(); 376 377 // filter out unknown networks 378 Set<ScanResultMatchInfo> goodNetworks = getGoodSavedNetworksAndSuggestions(); 379 Set<ScanResultMatchInfo> matchInfos = toMatchInfos(scanResults); 380 matchInfos.retainAll(goodNetworks); 381 382 mWakeupLock.update(matchInfos); 383 if (!mWakeupLock.isUnlocked()) { 384 return; 385 } 386 387 ScanResult network = mWakeupEvaluator.findViableNetwork(scanResults, goodNetworks); 388 389 if (network != null) { 390 Log.d(TAG, "Enabling wifi for network: " + network.SSID); 391 enableWifi(); 392 } 393 } 394 395 /** 396 * Converts ScanResults to ScanResultMatchInfos. 397 */ toMatchInfos(Collection<ScanResult> scanResults)398 private static Set<ScanResultMatchInfo> toMatchInfos(Collection<ScanResult> scanResults) { 399 return scanResults.stream() 400 .map(ScanResultMatchInfo::fromScanResult) 401 .collect(Collectors.toSet()); 402 } 403 404 /** 405 * Enables wifi. 406 * 407 * <p>This method ignores all checks and assumes that {@link WifiController} is currently 408 * in ScanModeState. 409 */ enableWifi()410 private void enableWifi() { 411 if (USE_PLATFORM_WIFI_WAKE) { 412 // TODO(b/72180295): ensure that there is no race condition with WifiServiceImpl here 413 if (mWifiInjector.getWifiSettingsStore().handleWifiToggled(true /* wifiEnabled */)) { 414 mWifiInjector.getWifiController().sendMessage(CMD_WIFI_TOGGLED); 415 mWifiWakeMetrics.recordWakeupEvent(mNumScansHandled); 416 } 417 } 418 } 419 420 /** 421 * Whether the feature is currently enabled. 422 * 423 * <p>This method checks both the Settings value and the store data to ensure that it has been 424 * read. 425 */ 426 @VisibleForTesting isEnabled()427 boolean isEnabled() { 428 return mWifiWakeupEnabled && mWakeupConfigStoreData.hasBeenRead(); 429 } 430 431 /** Dumps wakeup controller state. */ dump(FileDescriptor fd, PrintWriter pw, String[] args)432 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 433 pw.println("Dump of WakeupController"); 434 pw.println("USE_PLATFORM_WIFI_WAKE: " + USE_PLATFORM_WIFI_WAKE); 435 pw.println("mWifiWakeupEnabled: " + mWifiWakeupEnabled); 436 pw.println("isOnboarded: " + mWakeupOnboarding.isOnboarded()); 437 pw.println("configStore hasBeenRead: " + mWakeupConfigStoreData.hasBeenRead()); 438 pw.println("mIsActive: " + mIsActive); 439 pw.println("mNumScansHandled: " + mNumScansHandled); 440 441 mWakeupLock.dump(fd, pw, args); 442 } 443 444 private class IsActiveDataSource implements WakeupConfigStoreData.DataSource<Boolean> { 445 446 @Override getData()447 public Boolean getData() { 448 return mIsActive; 449 } 450 451 @Override setData(Boolean data)452 public void setData(Boolean data) { 453 mIsActive = data; 454 } 455 } 456 } 457