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