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.notify; 17 18 import static com.android.networkrecommendation.Constants.TAG; 19 20 import android.app.Notification; 21 import android.app.NotificationManager; 22 import android.content.BroadcastReceiver; 23 import android.content.ContentResolver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.database.ContentObserver; 28 import android.net.NetworkInfo; 29 import android.net.NetworkScoreManager; 30 import android.net.RecommendationRequest; 31 import android.net.RecommendationResult; 32 import android.net.wifi.ScanResult; 33 import android.net.wifi.WifiConfiguration; 34 import android.net.wifi.WifiManager; 35 import android.os.Handler; 36 import android.os.UserManager; 37 import android.provider.Settings; 38 import android.support.annotation.IntDef; 39 import android.support.annotation.Nullable; 40 import com.android.networkrecommendation.R; 41 import com.android.networkrecommendation.SynchronousNetworkRecommendationProvider; 42 import com.android.networkrecommendation.util.Blog; 43 import com.android.networkrecommendation.util.RoboCompatUtil; 44 import com.android.networkrecommendation.util.ScanResultUtil; 45 import java.io.FileDescriptor; 46 import java.io.PrintWriter; 47 import java.lang.annotation.Retention; 48 import java.lang.annotation.RetentionPolicy; 49 import java.util.ArrayList; 50 import java.util.List; 51 import java.util.concurrent.TimeUnit; 52 import java.util.concurrent.atomic.AtomicBoolean; 53 54 /** Takes care of handling the "open wi-fi network available" notification */ 55 public class WifiNotificationController { 56 /** The unique ID of the Notification given to the NotificationManager. */ 57 private static final int NOTIFICATION_ID = R.string.wifi_available_title; 58 59 /** When a notification is shown, we wait this amount before possibly showing it again. */ 60 private final long mNotificationRepeatDelayMs; 61 62 /** Whether the user has set the setting to show the 'available networks' notification. */ 63 private boolean mNotificationEnabled; 64 65 /** Whether the user has {@link UserManager#DISALLOW_CONFIG_WIFI} restriction. */ 66 private boolean mWifiConfigRestricted; 67 68 /** Observes the user setting to keep {@link #mNotificationEnabled} in sync. */ 69 private final NotificationEnabledSettingObserver mNotificationEnabledSettingObserver; 70 71 /** 72 * The {@link System#currentTimeMillis()} must be at least this value for us to show the 73 * notification again. 74 */ 75 private long mNotificationRepeatTime; 76 77 /** These are all of the possible states for the open networks available notification. */ 78 @IntDef({ 79 State.NO_RECOMMENDATION, 80 State.SHOWING_RECOMMENDATION_NOTIFICATION, 81 State.CONNECTING_IN_NOTIFICATION, 82 State.CONNECTING_IN_WIFI_PICKER, 83 State.CONNECTED, 84 State.CONNECT_FAILED 85 }) 86 @Retention(RetentionPolicy.SOURCE) 87 public @interface State { 88 int NO_RECOMMENDATION = 0; 89 int SHOWING_RECOMMENDATION_NOTIFICATION = 1; 90 int CONNECTING_IN_NOTIFICATION = 2; 91 int CONNECTING_IN_WIFI_PICKER = 3; 92 int CONNECTED = 4; 93 int CONNECT_FAILED = 5; 94 } 95 96 /** 97 * The {@link System#currentTimeMillis()} must be at least this value to log that open networks 98 * are available. 99 */ 100 private long mOpenNetworksLoggingRepeatTime; 101 102 /** Current state of the notification. */ 103 @State private int mState = State.NO_RECOMMENDATION; 104 105 /** 106 * The number of continuous scans that must occur before consider the supplicant in a scanning 107 * state. This allows supplicant to associate with remembered networks that are in the scan 108 * results. 109 */ 110 private static final int NUM_SCANS_BEFORE_ACTUALLY_SCANNING = 3; 111 112 /** 113 * The number of scans since the last network state change. When this exceeds {@link 114 * #NUM_SCANS_BEFORE_ACTUALLY_SCANNING}, we consider the supplicant to actually be scanning. 115 * When the network state changes to something other than scanning, we reset this to 0. 116 */ 117 private int mNumScansSinceNetworkStateChange; 118 119 /** Time in milliseconds to display the Connecting notification. */ 120 private static final int TIME_TO_SHOW_CONNECTING_MILLIS = 10000; 121 122 /** Time in milliseconds to display the Connected notification. */ 123 private static final int TIME_TO_SHOW_CONNECTED_MILLIS = 5000; 124 125 /** Time in milliseconds to display the Failed To Connect notification. */ 126 private static final int TIME_TO_SHOW_FAILED_MILLIS = 5000; 127 128 /** Try to connect to the recommended WifiConfiguration and also open the wifi picker. */ 129 static final String ACTION_CONNECT_TO_RECOMMENDED_NETWORK_AND_OPEN_SETTINGS = 130 "com.android.networkrecommendation.notify.CONNECT_TO_RECOMMENDED_NETWORK_AND_OPEN_SETTINGS"; 131 132 /** Try to connect to the recommended WifiConfiguration. */ 133 static final String ACTION_CONNECT_TO_RECOMMENDED_NETWORK = 134 "com.android.networkrecommendation.notify.CONNECT_TO_RECOMMENDED_NETWORK"; 135 136 /** Open wifi picker to see all available networks. */ 137 static final String ACTION_PICK_WIFI_NETWORK = 138 "com.android.networkrecommendation.notify.ACTION_PICK_WIFI_NETWORK"; 139 140 /** Open wifi picker to see all networks after connect to the recommended network failed. */ 141 static final String ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE = 142 "com.android.networkrecommendation.notify.ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE"; 143 144 /** Handles behavior when notification is deleted. */ 145 static final String ACTION_NOTIFICATION_DELETED = 146 "com.android.networkrecommendation.notify.NOTIFICATION_DELETED"; 147 148 /** Network recommended by {@link NetworkScoreManager#requestRecommendation}. */ 149 private WifiConfiguration mRecommendedNetwork; 150 151 /** Whether {@link WifiNotificationController} has been started. */ 152 private final AtomicBoolean mStarted; 153 154 private static final String NOTIFICATION_TAG = "WifiNotification"; 155 156 private final Context mContext; 157 private final Handler mHandler; 158 private final ContentResolver mContentResolver; 159 private final SynchronousNetworkRecommendationProvider mNetworkRecommendationProvider; 160 private final WifiManager mWifiManager; 161 private final NotificationManager mNotificationManager; 162 private final UserManager mUserManager; 163 private final WifiNotificationHelper mWifiNotificationHelper; 164 private NetworkInfo mNetworkInfo; 165 private NetworkInfo.DetailedState mDetailedState; 166 private volatile int mWifiState; 167 WifiNotificationController( Context context, ContentResolver contentResolver, Handler handler, SynchronousNetworkRecommendationProvider networkRecommendationProvider, WifiManager wifiManager, NotificationManager notificationManager, UserManager userManager, WifiNotificationHelper helper)168 public WifiNotificationController( 169 Context context, 170 ContentResolver contentResolver, 171 Handler handler, 172 SynchronousNetworkRecommendationProvider networkRecommendationProvider, 173 WifiManager wifiManager, 174 NotificationManager notificationManager, 175 UserManager userManager, 176 WifiNotificationHelper helper) { 177 mContext = context; 178 mContentResolver = contentResolver; 179 mNetworkRecommendationProvider = networkRecommendationProvider; 180 mWifiManager = wifiManager; 181 mNotificationManager = notificationManager; 182 mUserManager = userManager; 183 mHandler = handler; 184 mWifiNotificationHelper = helper; 185 mStarted = new AtomicBoolean(false); 186 187 // Setting is in seconds 188 mNotificationRepeatDelayMs = 189 TimeUnit.SECONDS.toMillis( 190 Settings.Global.getInt( 191 contentResolver, 192 Settings.Global.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY, 193 900)); 194 mNotificationEnabledSettingObserver = new NotificationEnabledSettingObserver(mHandler); 195 } 196 197 /** Starts {@link WifiNotificationController}. */ start()198 public void start() { 199 if (!mStarted.compareAndSet(false, true)) { 200 return; 201 } 202 203 mWifiState = mWifiManager.getWifiState(); 204 mDetailedState = NetworkInfo.DetailedState.IDLE; 205 206 IntentFilter filter = new IntentFilter(); 207 filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); 208 filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); 209 filter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); 210 filter.addAction(RoboCompatUtil.ACTION_USER_RESTRICTIONS_CHANGED); 211 filter.addAction(ACTION_CONNECT_TO_RECOMMENDED_NETWORK_AND_OPEN_SETTINGS); 212 filter.addAction(ACTION_CONNECT_TO_RECOMMENDED_NETWORK); 213 filter.addAction(ACTION_NOTIFICATION_DELETED); 214 filter.addAction(ACTION_PICK_WIFI_NETWORK); 215 filter.addAction(ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE); 216 217 mContext.registerReceiver( 218 mBroadcastReceiver, filter, null /* broadcastPermission */, mHandler); 219 mNotificationEnabledSettingObserver.register(); 220 221 handleUserRestrictionsChanged(); 222 } 223 224 /** Stops {@link WifiNotificationController}. */ stop()225 public void stop() { 226 if (!mStarted.compareAndSet(true, false)) { 227 return; 228 } 229 mContext.unregisterReceiver(mBroadcastReceiver); 230 mNotificationEnabledSettingObserver.unregister(); 231 } 232 233 private final BroadcastReceiver mBroadcastReceiver = 234 new BroadcastReceiver() { 235 @Override 236 public void onReceive(Context context, Intent intent) { 237 try { 238 switch (intent.getAction()) { 239 case WifiManager.WIFI_STATE_CHANGED_ACTION: 240 mWifiState = mWifiManager.getWifiState(); 241 resetNotification(); 242 break; 243 case WifiManager.NETWORK_STATE_CHANGED_ACTION: 244 handleNetworkStateChange(intent); 245 break; 246 case WifiManager.SCAN_RESULTS_AVAILABLE_ACTION: 247 checkAndSetNotification(mNetworkInfo); 248 break; 249 case RoboCompatUtil.ACTION_USER_RESTRICTIONS_CHANGED: 250 handleUserRestrictionsChanged(); 251 break; 252 case ACTION_CONNECT_TO_RECOMMENDED_NETWORK_AND_OPEN_SETTINGS: 253 connectToRecommendedNetwork(); 254 openWifiPicker(); 255 updateOnConnecting(false /* showNotification*/); 256 break; 257 case ACTION_CONNECT_TO_RECOMMENDED_NETWORK: 258 connectToRecommendedNetwork(); 259 updateOnConnecting(true /* showNotification*/); 260 break; 261 case ACTION_NOTIFICATION_DELETED: 262 handleNotificationDeleted(); 263 break; 264 case ACTION_PICK_WIFI_NETWORK: 265 openWifiPicker(); 266 break; 267 case ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE: 268 openWifiPicker(); 269 break; 270 default: 271 Blog.e( 272 TAG, 273 "Unexpected broadcast. [action=%s]", 274 intent.getAction()); 275 } 276 277 } catch (RuntimeException re) { 278 // TODO(b/35044022) Remove try/catch after a couple of releases when we are confident 279 // this is not going to throw. 280 Blog.e(TAG, re, "RuntimeException in broadcast receiver."); 281 } 282 } 283 }; 284 handleNetworkStateChange(Intent intent)285 private void handleNetworkStateChange(Intent intent) { 286 mNetworkInfo = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); 287 NetworkInfo.DetailedState detailedState = mNetworkInfo.getDetailedState(); 288 if (detailedState != NetworkInfo.DetailedState.SCANNING 289 && detailedState != mDetailedState) { 290 mDetailedState = detailedState; 291 switch (mDetailedState) { 292 case CONNECTED: 293 updateOnConnect(); 294 break; 295 case DISCONNECTED: 296 case CAPTIVE_PORTAL_CHECK: 297 resetNotification(); 298 break; 299 300 // TODO: figure out if these are failure cases when connecting 301 case IDLE: 302 case SCANNING: 303 case CONNECTING: 304 case DISCONNECTING: 305 case AUTHENTICATING: 306 case OBTAINING_IPADDR: 307 case SUSPENDED: 308 case FAILED: 309 case BLOCKED: 310 case VERIFYING_POOR_LINK: 311 break; 312 } 313 } 314 } 315 handleUserRestrictionsChanged()316 private void handleUserRestrictionsChanged() { 317 mWifiConfigRestricted = mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_WIFI); 318 Blog.v(TAG, "handleUserRestrictionsChanged: %b", mWifiConfigRestricted); 319 } 320 checkAndSetNotification(NetworkInfo networkInfo)321 private void checkAndSetNotification(NetworkInfo networkInfo) { 322 // TODO: unregister broadcast so we do not have to check here 323 // If we shouldn't place a notification on available networks, then 324 // don't bother doing any of the following 325 if (!mNotificationEnabled 326 || mWifiConfigRestricted 327 || mWifiState != WifiManager.WIFI_STATE_ENABLED 328 || mState > State.SHOWING_RECOMMENDATION_NOTIFICATION) { 329 return; 330 } 331 332 NetworkInfo.State state = NetworkInfo.State.DISCONNECTED; 333 if (networkInfo != null) { 334 state = networkInfo.getState(); 335 } 336 337 if (state == NetworkInfo.State.DISCONNECTED || state == NetworkInfo.State.UNKNOWN) { 338 maybeLogOpenNetworksAvailable(); 339 RecommendationResult result = getOpenNetworkRecommendation(); 340 if (result != null && result.getWifiConfiguration() != null) { 341 mRecommendedNetwork = result.getWifiConfiguration(); 342 343 if (++mNumScansSinceNetworkStateChange >= NUM_SCANS_BEFORE_ACTUALLY_SCANNING) { 344 /* 345 * We have scanned continuously at least 346 * NUM_SCANS_BEFORE_NOTIFICATION times. The user 347 * probably does not have a remembered network in range, 348 * since otherwise supplicant would have tried to 349 * associate and thus resetting this counter. 350 */ 351 displayNotification(); 352 } 353 return; 354 } 355 } 356 357 // No open networks in range, remove the notification 358 removeNotification(); 359 } 360 maybeLogOpenNetworksAvailable()361 private void maybeLogOpenNetworksAvailable() { 362 long now = System.currentTimeMillis(); 363 if (now < mOpenNetworksLoggingRepeatTime) { 364 return; 365 } 366 mOpenNetworksLoggingRepeatTime = now + mNotificationRepeatDelayMs; 367 } 368 369 /** 370 * Uses {@link NetworkScoreManager} to choose a qualified network out of the list of {@link 371 * ScanResult}s. 372 * 373 * @return returns the best qualified open networks, if any. 374 */ 375 @Nullable getOpenNetworkRecommendation()376 private RecommendationResult getOpenNetworkRecommendation() { 377 List<ScanResult> scanResults = mWifiManager.getScanResults(); 378 if (scanResults == null || scanResults.isEmpty()) { 379 return null; 380 } 381 382 ArrayList<ScanResult> openNetworks = new ArrayList<>(); 383 List<WifiConfiguration> configuredNetworks = mWifiManager.getConfiguredNetworks(); 384 for (ScanResult scanResult : scanResults) { 385 //A capability of [ESS] represents an open access point 386 //that is available for an STA to connect 387 //TODO: potentially handle this within NetworkRecommendationProvider instead. 388 if ("[ESS]".equals(scanResult.capabilities)) { 389 if (isSavedNetwork(scanResult, configuredNetworks)) { 390 continue; 391 } 392 openNetworks.add(scanResult); 393 } 394 } 395 396 Blog.d(TAG, "Sending RecommendationRequest. [num_open_networks=%d]", openNetworks.size()); 397 RecommendationRequest request = 398 new RecommendationRequest.Builder() 399 .setScanResults(openNetworks.toArray(new ScanResult[openNetworks.size()])) 400 .build(); 401 return mNetworkRecommendationProvider.requestRecommendation(request); 402 } 403 404 /** Returns true if scanResult matches the list of saved networks */ isSavedNetwork(ScanResult scanResult, List<WifiConfiguration> savedNetworks)405 private boolean isSavedNetwork(ScanResult scanResult, List<WifiConfiguration> savedNetworks) { 406 if (savedNetworks == null) { 407 return false; 408 } 409 for (int i = 0; i < savedNetworks.size(); i++) { 410 if (ScanResultUtil.doesScanResultMatchWithNetwork(scanResult, savedNetworks.get(i))) { 411 return true; 412 } 413 } 414 return false; 415 } 416 417 /** Display's a notification that there are open Wi-Fi networks. */ displayNotification()418 private void displayNotification() { 419 // Since we use auto cancel on the notification, when the 420 // mNetworksAvailableNotificationShown is true, the notification may 421 // have actually been canceled. However, when it is false we know 422 // for sure that it is not being shown (it will not be shown any other 423 // place than here) 424 425 // Not enough time has passed to show the notification again 426 if (mState == State.NO_RECOMMENDATION 427 && System.currentTimeMillis() < mNotificationRepeatTime) { 428 return; 429 } 430 Notification notification = 431 mWifiNotificationHelper.createMainNotification(mRecommendedNetwork); 432 mNotificationRepeatTime = System.currentTimeMillis() + mNotificationRepeatDelayMs; 433 postNotification(notification); 434 if (mState != State.SHOWING_RECOMMENDATION_NOTIFICATION) { 435 mState = State.SHOWING_RECOMMENDATION_NOTIFICATION; 436 } 437 } 438 439 /** Opens activity to allow the user to select a wifi network. */ openWifiPicker()440 private void openWifiPicker() { 441 // Close notification drawer before opening the picker. 442 mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); 443 mContext.startActivity( 444 new Intent(WifiManager.ACTION_PICK_WIFI_NETWORK) 445 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); 446 removeNotification(); 447 } 448 449 /** Attempts to connect to recommended network the recommended network. */ connectToRecommendedNetwork()450 private void connectToRecommendedNetwork() { 451 if (mRecommendedNetwork == null) { 452 return; 453 } 454 mRecommendedNetwork.BSSID = null; 455 456 // Attempts to connect to recommended network. 457 RoboCompatUtil.getInstance().connectToWifi(mWifiManager, mRecommendedNetwork); 458 } 459 updateOnConnecting(boolean showNotification)460 private void updateOnConnecting(boolean showNotification) { 461 if (showNotification) { 462 // Update notification to connecting status. 463 Notification notification = 464 mWifiNotificationHelper.createConnectingNotification(mRecommendedNetwork); 465 postNotification(notification); 466 mState = State.CONNECTING_IN_NOTIFICATION; 467 } else { 468 mState = State.CONNECTING_IN_WIFI_PICKER; 469 } 470 mHandler.postDelayed( 471 () -> { 472 updateOnFailedToConnect(); 473 }, 474 TIME_TO_SHOW_CONNECTING_MILLIS); 475 } 476 477 /** 478 * When detailed state changes to CONNECTED, show connected notification or reset notification. 479 */ updateOnConnect()480 private void updateOnConnect() { 481 if (mState == State.CONNECTING_IN_NOTIFICATION) { 482 Notification notification = 483 mWifiNotificationHelper.createConnectedNotification(mRecommendedNetwork); 484 postNotification(notification); 485 mState = State.CONNECTED; 486 mHandler.postDelayed( 487 () -> { 488 if (mState == State.CONNECTED) { 489 removeNotification(); 490 } 491 }, 492 TIME_TO_SHOW_CONNECTED_MILLIS); 493 } else if (mState == State.CONNECTING_IN_WIFI_PICKER) { 494 removeNotification(); 495 } 496 } 497 498 /** 499 * Displays the Failed To Connect notification after the Connecting notification is shown for 500 * {@link #TIME_TO_SHOW_CONNECTING_MILLIS} duration. 501 */ updateOnFailedToConnect()502 private void updateOnFailedToConnect() { 503 if (mState == State.CONNECTING_IN_NOTIFICATION) { 504 Notification notification = mWifiNotificationHelper.createFailedToConnectNotification(); 505 postNotification(notification); 506 mState = State.CONNECT_FAILED; 507 mHandler.postDelayed( 508 () -> { 509 if (mState == State.CONNECT_FAILED) { 510 removeNotification(); 511 } 512 }, 513 TIME_TO_SHOW_FAILED_MILLIS); 514 } else if (mState == State.CONNECTING_IN_WIFI_PICKER) { 515 removeNotification(); 516 } 517 } 518 519 /** Handles behavior when notification is dismissed. */ handleNotificationDeleted()520 private void handleNotificationDeleted() { 521 mState = State.NO_RECOMMENDATION; 522 mRecommendedNetwork = null; 523 } 524 postNotification(Notification notification)525 private void postNotification(Notification notification) { 526 mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification); 527 } 528 529 /** 530 * Clears variables related to tracking whether a notification has been shown recently and 531 * clears the current notification. 532 */ resetNotification()533 private void resetNotification() { 534 if (mState != State.NO_RECOMMENDATION) { 535 removeNotification(); 536 } 537 mRecommendedNetwork = null; 538 mNotificationRepeatTime = 0; 539 mNumScansSinceNetworkStateChange = 0; 540 mOpenNetworksLoggingRepeatTime = 0; 541 } 542 removeNotification()543 private void removeNotification() { 544 mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_ID); 545 mState = State.NO_RECOMMENDATION; 546 mRecommendedNetwork = null; 547 } 548 dump(FileDescriptor fd, PrintWriter pw, String[] args)549 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 550 pw.println("mNotificationEnabled " + mNotificationEnabled); 551 pw.println("mNotificationRepeatTime " + mNotificationRepeatTime); 552 pw.println("mState " + mState); 553 pw.println("mNumScansSinceNetworkStateChange " + mNumScansSinceNetworkStateChange); 554 } 555 556 private class NotificationEnabledSettingObserver extends ContentObserver { NotificationEnabledSettingObserver(Handler handler)557 NotificationEnabledSettingObserver(Handler handler) { 558 super(handler); 559 } 560 register()561 public void register() { 562 mContentResolver.registerContentObserver( 563 Settings.Global.getUriFor( 564 Settings.Global.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON), 565 true, 566 this); 567 mNotificationEnabled = getValue(); 568 } 569 unregister()570 public void unregister() { 571 mContentResolver.unregisterContentObserver(this); 572 } 573 574 @Override onChange(boolean selfChange)575 public void onChange(boolean selfChange) { 576 super.onChange(selfChange); 577 578 mNotificationEnabled = getValue(); 579 resetNotification(); 580 } 581 getValue()582 private boolean getValue() { 583 return Settings.Global.getInt( 584 mContentResolver, 585 Settings.Global.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, 586 0) 587 == 1; 588 } 589 } 590 } 591