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