1 /* 2 * Copyright (C) 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 package com.android.networkrecommendation.wakeup; 17 18 import static com.android.networkrecommendation.Constants.TAG; 19 20 import android.content.BroadcastReceiver; 21 import android.content.ContentResolver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.database.ContentObserver; 26 import android.net.wifi.ScanResult; 27 import android.net.wifi.WifiConfiguration; 28 import android.net.wifi.WifiManager; 29 import android.os.Handler; 30 import android.os.PowerManager; 31 import android.os.UserManager; 32 import android.provider.Settings; 33 import android.support.annotation.VisibleForTesting; 34 import android.text.TextUtils; 35 import android.util.ArrayMap; 36 import android.util.ArraySet; 37 import com.android.networkrecommendation.config.G; 38 import com.android.networkrecommendation.config.Preferences; 39 import com.android.networkrecommendation.config.WideAreaNetworks; 40 import com.android.networkrecommendation.scoring.util.HashUtil; 41 import com.android.networkrecommendation.util.Blog; 42 import com.android.networkrecommendation.util.RoboCompatUtil; 43 import com.android.networkrecommendation.util.WifiConfigurationUtil; 44 import java.io.FileDescriptor; 45 import java.io.PrintWriter; 46 import java.util.List; 47 import java.util.Map; 48 import java.util.Set; 49 import java.util.concurrent.atomic.AtomicBoolean; 50 51 /** 52 * Handles enabling Wi-Fi for the Wi-Fi Wakeup feature. 53 * 54 * <p>This class enables Wi-Fi when the user is near a network that would would autojoined if Wi-Fi 55 * were enabled. When a user disables Wi-Fi, Wi-Fi Wakeup will not enable Wi-Fi until the user's 56 * context has changed. For saved networks, this context change is defined by the user leaving the 57 * range of the saved SSIDs that were in range when the user disabled Wi-Fi. 58 * 59 * @hide 60 */ 61 public class WifiWakeupController { 62 /** Number of scans to ensure that a previously in range AP is now out of range. */ 63 private static final int NUM_SCANS_TO_CONFIRM_AP_LOSS = 3; 64 65 private final Context mContext; 66 private final ContentResolver mContentResolver; 67 private final WifiManager mWifiManager; 68 private final PowerManager mPowerManager; 69 private final UserManager mUserManager; 70 private final WifiWakeupNetworkSelector mWifiWakeupNetworkSelector; 71 private final Handler mHandler; 72 private final WifiWakeupHelper mWifiWakeupHelper; 73 private final AtomicBoolean mStarted; 74 @VisibleForTesting final ContentObserver mContentObserver; 75 76 private final Map<String, WifiConfiguration> mSavedNetworks = new ArrayMap<>(); 77 private final Set<String> mSavedSsidsInLastScan = new ArraySet<>(); 78 private final Set<String> mSavedSsids = new ArraySet<>(); 79 private final Map<String, Integer> mSavedSsidsOnDisable = new ArrayMap<>(); 80 private final SavedNetworkCounts mSavedNetworkCounts = new SavedNetworkCounts(); 81 private int mWifiState; 82 private int mWifiApState; 83 private boolean mWifiWakeupEnabled; 84 private boolean mAirplaneModeEnabled; 85 private boolean mAutopilotEnabledWifi; 86 private boolean mPowerSaverModeOn; 87 private boolean mWifiConfigRestricted; 88 89 public WifiWakeupController( 90 Context context, 91 ContentResolver contentResolver, 92 Handler handler, 93 WifiManager wifiManager, 94 PowerManager powerManager, 95 UserManager userManager, 96 WifiWakeupNetworkSelector wifiWakeupNetworkSelector, 97 WifiWakeupHelper wifiWakeupHelper) { 98 mContext = context; 99 mContentResolver = contentResolver; 100 mHandler = handler; 101 mWifiWakeupHelper = wifiWakeupHelper; 102 mStarted = new AtomicBoolean(false); 103 mWifiManager = wifiManager; 104 mPowerManager = powerManager; 105 mUserManager = userManager; 106 mWifiWakeupNetworkSelector = wifiWakeupNetworkSelector; 107 mContentObserver = 108 new ContentObserver(mHandler) { 109 @Override 110 public void onChange(boolean selfChange) { 111 mWifiWakeupEnabled = 112 Settings.Global.getInt( 113 mContentResolver, 114 Settings.Global.WIFI_WAKEUP_ENABLED, 115 0) 116 == 1; 117 mAirplaneModeEnabled = 118 Settings.Global.getInt( 119 mContentResolver, 120 Settings.Global.AIRPLANE_MODE_ON, 121 0) 122 == 1; 123 Blog.d( 124 TAG, 125 "onChange: [mWifiWakeupEnabled=%b,mAirplaneModeEnabled=%b]", 126 mWifiWakeupEnabled, 127 mAirplaneModeEnabled); 128 } 129 }; 130 } 131 132 private final BroadcastReceiver mBroadcastReceiver = 133 new BroadcastReceiver() { 134 @Override 135 public void onReceive(Context context, Intent intent) { 136 try { 137 if (WifiManager.WIFI_AP_STATE_CHANGED_ACTION.equals(intent.getAction())) { 138 handleWifiApStateChanged(); 139 } else if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals( 140 intent.getAction())) { 141 handleWifiStateChanged(false); 142 } else if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals( 143 intent.getAction())) { 144 handleScanResultsAvailable(); 145 } else if (WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION.equals( 146 intent.getAction())) { 147 handleConfiguredNetworksChanged(); 148 } else if (PowerManager.ACTION_POWER_SAVE_MODE_CHANGED.equals( 149 intent.getAction())) { 150 handlePowerSaverModeChanged(); 151 } else if (RoboCompatUtil.ACTION_USER_RESTRICTIONS_CHANGED.equals( 152 intent.getAction())) { 153 handleUserRestrictionsChanged(); 154 } 155 } catch (RuntimeException re) { 156 // TODO(b/35044022) Remove try/catch after a couple of releases when we are confident 157 // this is not going to throw. 158 Blog.e(TAG, re, "RuntimeException in broadcast receiver."); 159 } 160 } 161 }; 162 163 /** Starts {@link WifiWakeupController}. */ 164 public void start() { 165 if (!mStarted.compareAndSet(false, true)) { 166 return; 167 } 168 Blog.d(TAG, "Starting WifiWakeupController."); 169 170 IntentFilter filter = new IntentFilter(); 171 filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); 172 filter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); 173 filter.addAction(WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION); 174 filter.addAction(WifiManager.WIFI_AP_STATE_CHANGED_ACTION); 175 filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED); 176 filter.addAction(RoboCompatUtil.ACTION_USER_RESTRICTIONS_CHANGED); 177 // TODO(b/33695273): conditionally register this receiver based on wifi enabled setting 178 mContext.registerReceiver(mBroadcastReceiver, filter, null, mHandler); 179 mContentResolver.registerContentObserver( 180 Settings.Global.getUriFor(Settings.Global.WIFI_WAKEUP_ENABLED), 181 true, 182 mContentObserver); 183 mContentResolver.registerContentObserver( 184 Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON), 185 true, 186 mContentObserver); 187 mContentObserver.onChange(true); 188 handlePowerSaverModeChanged(); 189 handleUserRestrictionsChanged(); 190 handleWifiApStateChanged(); 191 handleConfiguredNetworksChanged(); 192 handleWifiStateChanged(true); 193 handleScanResultsAvailable(); 194 } 195 196 /** Stops {@link WifiWakeupController}. */ 197 public void stop() { 198 if (!mStarted.compareAndSet(true, false)) { 199 return; 200 } 201 Blog.d(TAG, "Stopping WifiWakeupController."); 202 mContext.unregisterReceiver(mBroadcastReceiver); 203 mContentResolver.unregisterContentObserver(mContentObserver); 204 } 205 206 private void handlePowerSaverModeChanged() { 207 mPowerSaverModeOn = mPowerManager.isPowerSaveMode(); 208 Blog.v(TAG, "handlePowerSaverModeChanged: %b", mPowerSaverModeOn); 209 } 210 211 private void handleWifiApStateChanged() { 212 mWifiApState = mWifiManager.getWifiApState(); 213 Blog.v(TAG, "handleWifiApStateChanged: %d", mWifiApState); 214 } 215 216 private void handleUserRestrictionsChanged() { 217 mWifiConfigRestricted = mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_WIFI); 218 Blog.v(TAG, "handleUserRestrictionsChanged: %b", mWifiConfigRestricted); 219 } 220 221 private void handleConfiguredNetworksChanged() { 222 List<WifiConfiguration> wifiConfigurations = mWifiManager.getConfiguredNetworks(); 223 if (wifiConfigurations == null) { 224 return; 225 } 226 Blog.v(TAG, "handleConfiguredNetworksChanged: %d", wifiConfigurations.size()); 227 228 mSavedNetworkCounts.clear(); 229 mSavedNetworkCounts.total = wifiConfigurations.size(); 230 mSavedNetworks.clear(); 231 mSavedSsids.clear(); 232 for (int i = 0; i < wifiConfigurations.size(); i++) { 233 WifiConfiguration wifiConfiguration = wifiConfigurations.get(i); 234 if (wifiConfiguration.status != WifiConfiguration.Status.ENABLED 235 && wifiConfiguration.status != WifiConfiguration.Status.CURRENT) { 236 continue; // Ignore networks that are not connected or enabled. 237 } 238 mSavedNetworkCounts.enabled++; 239 if (RoboCompatUtil.getInstance().hasNoInternetAccess(wifiConfiguration)) { 240 mSavedNetworkCounts.noInternetAccess++; 241 continue; // Ignore networks that do not have verified internet access. 242 } 243 if (RoboCompatUtil.getInstance().isNoInternetAccessExpected(wifiConfiguration)) { 244 mSavedNetworkCounts.noInternetAccessExpected++; 245 continue; // Ignore networks that are expected not to have internet access. 246 } 247 String ssid = WifiConfigurationUtil.removeDoubleQuotes(wifiConfiguration); 248 if (TextUtils.isEmpty(ssid)) { 249 continue; 250 } 251 if (WideAreaNetworks.contains(ssid)) { 252 mSavedNetworkCounts.blacklisted++; 253 continue; // Ignore wide area networks. 254 } 255 mSavedNetworks.put(ssid, wifiConfiguration); 256 mSavedSsids.add(ssid); 257 258 if (WifiConfigurationUtil.isConfigForOpenNetwork(wifiConfiguration)) { 259 mSavedNetworkCounts.open++; 260 } 261 if (RoboCompatUtil.getInstance().useExternalScores(wifiConfiguration)) { 262 mSavedNetworkCounts.useExternalScores++; 263 } 264 } 265 mSavedSsidsInLastScan.retainAll(mSavedSsids); 266 } 267 268 private void handleWifiStateChanged(boolean calledOnStart) { 269 mWifiState = mWifiManager.getWifiState(); 270 Blog.v(TAG, "handleWifiStateChanged: %d", mWifiState); 271 272 switch (mWifiState) { 273 case WifiManager.WIFI_STATE_ENABLED: 274 mSavedSsidsOnDisable.clear(); 275 if (!mAutopilotEnabledWifi) {} 276 break; 277 case WifiManager.WIFI_STATE_DISABLED: 278 if (calledOnStart) { 279 readDisabledSsidsFromSharedPreferences(); 280 } else { 281 for (String ssid : mSavedSsidsInLastScan) { 282 mSavedSsidsOnDisable.put(ssid, NUM_SCANS_TO_CONFIRM_AP_LOSS); 283 } 284 writeDisabledSsidsToSharedPreferences(); 285 } 286 Blog.d(TAG, "Disabled ssid set: %s", mSavedSsidsOnDisable); 287 288 mAutopilotEnabledWifi = false; 289 break; 290 default: // Only handle ENABLED and DISABLED states 291 } 292 } 293 294 private void readDisabledSsidsFromSharedPreferences() { 295 Set<String> ssidsOnDisable = Preferences.savedSsidsOnDisable.get(); 296 for (String ssid : mSavedSsids) { 297 if (ssidsOnDisable.contains(HashUtil.getSsidHash(ssid))) { 298 mSavedSsidsOnDisable.put(ssid, NUM_SCANS_TO_CONFIRM_AP_LOSS); 299 } 300 } 301 } 302 303 private void writeDisabledSsidsToSharedPreferences() { 304 Set<String> ssids = new ArraySet<>(); 305 for (String ssid : mSavedSsidsOnDisable.keySet()) { 306 ssids.add(HashUtil.getSsidHash(ssid)); 307 } 308 Preferences.savedSsidsOnDisable.put(ssids); 309 } 310 311 private void handleScanResultsAvailable() { 312 if (!mWifiWakeupEnabled || mWifiConfigRestricted) { 313 return; 314 } 315 List<ScanResult> scanResults = mWifiManager.getScanResults(); 316 if (scanResults == null) { 317 return; 318 } 319 Blog.v(TAG, "handleScanResultsAvailable: %d", scanResults.size()); 320 321 mSavedSsidsInLastScan.clear(); 322 for (int i = 0; i < scanResults.size(); i++) { 323 String ssid = scanResults.get(i).SSID; 324 if (mSavedSsids.contains(ssid)) { 325 mSavedSsidsInLastScan.add(ssid); 326 } 327 } 328 329 if (mAirplaneModeEnabled 330 || mWifiState != WifiManager.WIFI_STATE_DISABLED 331 || mWifiApState != WifiManager.WIFI_AP_STATE_DISABLED 332 || mPowerSaverModeOn) { 333 return; 334 } 335 336 // Update mSavedSsidsOnDisable to remove ssids that the user has moved away from. 337 for (Map.Entry<String, Integer> entry : mSavedSsidsOnDisable.entrySet()) { 338 if (mSavedSsidsInLastScan.contains(entry.getKey())) { 339 mSavedSsidsOnDisable.put(entry.getKey(), NUM_SCANS_TO_CONFIRM_AP_LOSS); 340 } else { 341 if (entry.getValue() > 1) { 342 mSavedSsidsOnDisable.put(entry.getKey(), entry.getValue() - 1); 343 } else { 344 mSavedSsidsOnDisable.remove(entry.getKey()); 345 } 346 } 347 } 348 349 if (!mSavedSsidsOnDisable.isEmpty()) { 350 Blog.d( 351 TAG, 352 "Scan results contain ssids from the disabled set: %s", 353 mSavedSsidsOnDisable); 354 return; 355 } 356 357 if (mSavedSsidsInLastScan.isEmpty()) { 358 Blog.v(TAG, "Scan results do not contain any saved ssids."); 359 return; 360 } 361 362 WifiConfiguration selectedNetwork = 363 mWifiWakeupNetworkSelector.selectNetwork(mSavedNetworks, scanResults); 364 if (selectedNetwork != null) { 365 Blog.d( 366 TAG, 367 "Enabling wifi for ssid: %s", 368 Blog.pii(selectedNetwork.SSID, G.Netrec.enableSensitiveLogging.get())); 369 370 mAutopilotEnabledWifi = true; 371 mWifiManager.setWifiEnabled(true /* enabled */); 372 mWifiWakeupHelper.startWifiSession(selectedNetwork); 373 } 374 } 375 376 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 377 pw.println("mStarted " + mStarted.get()); 378 pw.println("mWifiWakeupEnabled: " + mWifiWakeupEnabled); 379 pw.println("mSavedSsids: " + mSavedSsids); 380 pw.println("mSavedSsidsInLastScan: " + mSavedSsidsInLastScan); 381 pw.println("mSavedSsidsOnDisable: " + mSavedSsidsOnDisable); 382 } 383 384 /** Class to track counts for saved networks for logging. */ 385 private static class SavedNetworkCounts { 386 int total; 387 int open; 388 int enabled; 389 int noInternetAccess; 390 int noInternetAccessExpected; 391 int useExternalScores; 392 int blacklisted; 393 394 void clear() { 395 total = 0; 396 open = 0; 397 enabled = 0; 398 noInternetAccess = 0; 399 noInternetAccessExpected = 0; 400 useExternalScores = 0; 401 blacklisted = 0; 402 } 403 } 404 } 405