1 /* 2 * Copyright (C) 2018 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.settings.wifi; 18 19 import android.app.Activity; 20 import android.app.Dialog; 21 import android.app.settings.SettingsEnums; 22 import android.content.Context; 23 import android.content.DialogInterface; 24 import android.content.Intent; 25 import android.graphics.drawable.Drawable; 26 import android.net.wifi.ScanResult; 27 import android.net.wifi.WifiConfiguration; 28 import android.net.wifi.WifiManager; 29 import android.net.wifi.WifiManager.NetworkRequestMatchCallback; 30 import android.net.wifi.WifiManager.NetworkRequestUserSelectionCallback; 31 import android.os.Bundle; 32 import android.os.Handler; 33 import android.os.Message; 34 import android.text.TextUtils; 35 import android.view.LayoutInflater; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.widget.ArrayAdapter; 39 import android.widget.BaseAdapter; 40 import android.widget.Button; 41 import android.widget.ProgressBar; 42 import android.widget.TextView; 43 import android.widget.Toast; 44 45 import androidx.annotation.NonNull; 46 import androidx.annotation.VisibleForTesting; 47 import androidx.appcompat.app.AlertDialog; 48 import androidx.preference.internal.PreferenceImageView; 49 50 import com.android.settings.R; 51 import com.android.settings.core.instrumentation.InstrumentedDialogFragment; 52 import com.android.settings.wifi.NetworkRequestErrorDialogFragment.ERROR_DIALOG_TYPE; 53 import com.android.settingslib.Utils; 54 import com.android.settingslib.core.lifecycle.Lifecycle; 55 import com.android.settingslib.wifi.AccessPoint; 56 import com.android.settingslib.wifi.WifiTracker; 57 import com.android.settingslib.wifi.WifiTrackerFactory; 58 59 import java.util.ArrayList; 60 import java.util.List; 61 62 /** 63 * The Fragment sets up callback {@link NetworkRequestMatchCallback} with framework. To handle most 64 * behaviors of the callback when requesting wifi network, except for error message. When error 65 * happens, {@link NetworkRequestErrorDialogFragment} will be called to display error message. 66 */ 67 public class NetworkRequestDialogFragment extends InstrumentedDialogFragment implements 68 DialogInterface.OnClickListener, NetworkRequestMatchCallback { 69 70 /** Message sent to us to stop scanning wifi and pop up timeout dialog. */ 71 private static final int MESSAGE_STOP_SCAN_WIFI_LIST = 0; 72 73 /** 74 * Spec defines there should be 5 wifi ap on the list at most or just show all if {@code 75 * mShowLimitedItem} is false. 76 */ 77 private static final int MAX_NUMBER_LIST_ITEM = 5; 78 private boolean mShowLimitedItem = true; 79 80 /** Delayed time to stop scanning wifi. */ 81 private static final int DELAY_TIME_STOP_SCAN_MS = 30 * 1000; 82 83 @VisibleForTesting 84 final static String EXTRA_APP_NAME = "com.android.settings.wifi.extra.APP_NAME"; 85 final static String EXTRA_IS_SPECIFIED_SSID = 86 "com.android.settings.wifi.extra.REQUEST_IS_FOR_SINGLE_NETWORK"; 87 88 private List<AccessPoint> mAccessPointList; 89 private FilterWifiTracker mFilterWifiTracker; 90 private AccessPointAdapter mDialogAdapter; 91 private NetworkRequestUserSelectionCallback mUserSelectionCallback; 92 private boolean mIsSpecifiedSsid; 93 private boolean mWaitingConnectCallback; 94 newInstance()95 public static NetworkRequestDialogFragment newInstance() { 96 NetworkRequestDialogFragment dialogFragment = new NetworkRequestDialogFragment(); 97 return dialogFragment; 98 } 99 100 @Override onCreateDialog(Bundle savedInstanceState)101 public Dialog onCreateDialog(Bundle savedInstanceState) { 102 final Context context = getContext(); 103 104 // Prepares title. 105 final LayoutInflater inflater = LayoutInflater.from(context); 106 final View customTitle = inflater.inflate(R.layout.network_request_dialog_title, null); 107 108 final TextView title = customTitle.findViewById(R.id.network_request_title_text); 109 title.setText(getTitle()); 110 111 final Intent intent = getActivity().getIntent(); 112 if (intent != null) { 113 mIsSpecifiedSsid = intent.getBooleanExtra(EXTRA_IS_SPECIFIED_SSID, false); 114 } 115 116 final ProgressBar progressBar = customTitle.findViewById( 117 R.id.network_request_title_progress); 118 progressBar.setVisibility(View.VISIBLE); 119 120 // Prepares adapter. 121 mDialogAdapter = new AccessPointAdapter(context, 122 R.layout.preference_access_point, getAccessPointList()); 123 124 final AlertDialog.Builder builder = new AlertDialog.Builder(context) 125 .setCustomTitle(customTitle) 126 .setAdapter(mDialogAdapter, this) 127 .setNegativeButton(R.string.cancel, (dialog, which) -> onCancel(dialog)) 128 // Do nothings, will replace the onClickListener to avoid auto closing dialog. 129 .setNeutralButton(R.string.network_connection_request_dialog_showall, 130 null /* OnClickListener */); 131 if (mIsSpecifiedSsid) { 132 builder.setPositiveButton(R.string.wifi_connect, null /* OnClickListener */); 133 } 134 135 // Clicking list item is to connect wifi ap. 136 final AlertDialog dialog = builder.create(); 137 dialog.getListView() 138 .setOnItemClickListener( 139 (parent, view, position, id) -> this.onClick(dialog, position)); 140 141 // Don't dismiss dialog when touching outside. User reports it is easy to touch outside. 142 // This causes dialog to close. 143 setCancelable(false); 144 145 dialog.setOnShowListener((dialogInterface) -> { 146 // Replace NeutralButton onClickListener to avoid closing dialog 147 final Button neutralBtn = dialog.getButton(AlertDialog.BUTTON_NEUTRAL); 148 neutralBtn.setVisibility(View.GONE); 149 neutralBtn.setOnClickListener(v -> { 150 mShowLimitedItem = false; 151 renewAccessPointList(null /* List<ScanResult> */); 152 notifyAdapterRefresh(); 153 neutralBtn.setVisibility(View.GONE); 154 }); 155 156 // Replace Positive onClickListener to avoid closing dialog 157 if (mIsSpecifiedSsid) { 158 final Button positiveBtn = dialog.getButton(AlertDialog.BUTTON_POSITIVE); 159 positiveBtn.setOnClickListener(v -> { 160 // When clicking connect button, should connect to the first and the only one 161 // list item. 162 this.onClick(dialog, 0 /* position */); 163 }); 164 // Disable button in first, and enable it after there are some accesspoints in list. 165 positiveBtn.setEnabled(false); 166 } 167 }); 168 return dialog; 169 } 170 getTitle()171 private String getTitle() { 172 final Intent intent = getActivity().getIntent(); 173 String appName = ""; 174 if (intent != null) { 175 appName = intent.getStringExtra(EXTRA_APP_NAME); 176 } 177 178 return getString(R.string.network_connection_request_dialog_title, appName); 179 } 180 181 @NonNull getAccessPointList()182 List<AccessPoint> getAccessPointList() { 183 // Initials list for adapter, in case of display crashing. 184 if (mAccessPointList == null) { 185 mAccessPointList = new ArrayList<>(); 186 } 187 return mAccessPointList; 188 } 189 getDialogAdapter()190 private BaseAdapter getDialogAdapter() { 191 return mDialogAdapter; 192 } 193 194 @Override onClick(DialogInterface dialog, int which)195 public void onClick(DialogInterface dialog, int which) { 196 final List<AccessPoint> accessPointList = getAccessPointList(); 197 if (accessPointList.size() == 0) { 198 return; // Invalid values. 199 } 200 if (mUserSelectionCallback == null) { 201 return; // Callback is missing or not ready. 202 } 203 204 if (which < accessPointList.size()) { 205 final AccessPoint selectedAccessPoint = accessPointList.get(which); 206 WifiConfiguration wifiConfig = selectedAccessPoint.getConfig(); 207 if (wifiConfig == null) { 208 wifiConfig = WifiUtils.getWifiConfig(selectedAccessPoint, /* scanResult */ 209 null, /* password */ null); 210 } 211 212 if (wifiConfig != null) { 213 mUserSelectionCallback.select(wifiConfig); 214 215 mWaitingConnectCallback = true; 216 updateConnectButton(false); 217 } 218 } 219 } 220 221 @Override onCancel(@onNull DialogInterface dialog)222 public void onCancel(@NonNull DialogInterface dialog) { 223 super.onCancel(dialog); 224 // Finishes the activity when user clicks back key or outside of the dialog. 225 if (getActivity() != null) { 226 getActivity().finish(); 227 } 228 if (mUserSelectionCallback != null) { 229 mUserSelectionCallback.reject(); 230 } 231 } 232 233 @Override onPause()234 public void onPause() { 235 super.onPause(); 236 237 mHandler.removeMessages(MESSAGE_STOP_SCAN_WIFI_LIST); 238 final WifiManager wifiManager = getContext().getApplicationContext() 239 .getSystemService(WifiManager.class); 240 if (wifiManager != null) { 241 wifiManager.unregisterNetworkRequestMatchCallback(this); 242 } 243 244 if (mFilterWifiTracker != null) { 245 mFilterWifiTracker.onPause(); 246 } 247 } 248 249 @Override onDestroy()250 public void onDestroy() { 251 super.onDestroy(); 252 253 if (mFilterWifiTracker != null) { 254 mFilterWifiTracker.onDestroy(); 255 mFilterWifiTracker = null; 256 } 257 } 258 showAllButton()259 private void showAllButton() { 260 final AlertDialog alertDialog = (AlertDialog) getDialog(); 261 if (alertDialog == null) { 262 return; 263 } 264 265 final Button neutralBtn = alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL); 266 if (neutralBtn != null) { 267 neutralBtn.setVisibility(View.VISIBLE); 268 } 269 } 270 updateConnectButton(boolean enabled)271 private void updateConnectButton(boolean enabled) { 272 // The button is only showed in single SSID mode. 273 if (!mIsSpecifiedSsid) { 274 return; 275 } 276 277 final AlertDialog alertDialog = (AlertDialog) getDialog(); 278 if (alertDialog == null) { 279 return; 280 } 281 282 final Button positiveBtn = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); 283 if (positiveBtn != null) { 284 positiveBtn.setEnabled(enabled); 285 } 286 } 287 hideProgressIcon()288 private void hideProgressIcon() { 289 final AlertDialog alertDialog = (AlertDialog) getDialog(); 290 if (alertDialog == null) { 291 return; 292 } 293 294 final View progress = alertDialog.findViewById(R.id.network_request_title_progress); 295 if (progress != null) { 296 progress.setVisibility(View.GONE); 297 } 298 } 299 300 @Override onResume()301 public void onResume() { 302 super.onResume(); 303 304 final WifiManager wifiManager = getContext().getApplicationContext() 305 .getSystemService(WifiManager.class); 306 if (wifiManager != null) { 307 wifiManager.registerNetworkRequestMatchCallback(this, mHandler); 308 } 309 // Sets time-out to stop scanning. 310 mHandler.sendEmptyMessageDelayed(MESSAGE_STOP_SCAN_WIFI_LIST, DELAY_TIME_STOP_SCAN_MS); 311 312 if (mFilterWifiTracker == null) { 313 mFilterWifiTracker = new FilterWifiTracker(getActivity(), getSettingsLifecycle()); 314 } 315 mFilterWifiTracker.onResume(); 316 } 317 318 private final Handler mHandler = new Handler() { 319 @Override 320 public void handleMessage(Message msg) { 321 switch (msg.what) { 322 case MESSAGE_STOP_SCAN_WIFI_LIST: 323 removeMessages(MESSAGE_STOP_SCAN_WIFI_LIST); 324 stopScanningAndPopErrorDialog(ERROR_DIALOG_TYPE.TIME_OUT); 325 break; 326 default: 327 // Do nothing. 328 break; 329 } 330 } 331 }; 332 stopScanningAndPopErrorDialog(ERROR_DIALOG_TYPE type)333 protected void stopScanningAndPopErrorDialog(ERROR_DIALOG_TYPE type) { 334 // Dismisses current dialog. 335 final Dialog dialog = getDialog(); 336 if (dialog != null && dialog.isShowing()) { 337 dismiss(); 338 } 339 340 // Throws error dialog. 341 final NetworkRequestErrorDialogFragment fragment = NetworkRequestErrorDialogFragment 342 .newInstance(); 343 final Bundle bundle = new Bundle(); 344 bundle.putSerializable(NetworkRequestErrorDialogFragment.DIALOG_TYPE, type); 345 fragment.setArguments(bundle); 346 fragment.show(getActivity().getSupportFragmentManager(), 347 NetworkRequestDialogFragment.class.getSimpleName()); 348 } 349 350 @Override getMetricsCategory()351 public int getMetricsCategory() { 352 return SettingsEnums.WIFI_SCANNING_NEEDED_DIALOG; 353 } 354 355 private class AccessPointAdapter extends ArrayAdapter<AccessPoint> { 356 357 private final int mResourceId; 358 private final LayoutInflater mInflater; 359 AccessPointAdapter(Context context, int resourceId, List<AccessPoint> objects)360 public AccessPointAdapter(Context context, int resourceId, List<AccessPoint> objects) { 361 super(context, resourceId, objects); 362 mResourceId = resourceId; 363 mInflater = LayoutInflater.from(context); 364 } 365 366 @Override getView(int position, View view, ViewGroup parent)367 public View getView(int position, View view, ViewGroup parent) { 368 if (view == null) { 369 view = mInflater.inflate(mResourceId, parent, false); 370 371 final View divider = view.findViewById( 372 com.android.settingslib.R.id.two_target_divider); 373 divider.setVisibility(View.GONE); 374 } 375 376 final AccessPoint accessPoint = getItem(position); 377 378 final TextView titleView = view.findViewById(android.R.id.title); 379 if (titleView != null) { 380 // Shows whole SSID for better UX. 381 titleView.setSingleLine(false); 382 titleView.setText(accessPoint.getTitle()); 383 } 384 385 final TextView summary = view.findViewById(android.R.id.summary); 386 if (summary != null) { 387 final String summaryString = accessPoint.getSettingsSummary(); 388 if (TextUtils.isEmpty(summaryString)) { 389 summary.setVisibility(View.GONE); 390 } else { 391 summary.setVisibility(View.VISIBLE); 392 summary.setText(summaryString); 393 } 394 } 395 396 final PreferenceImageView imageView = view.findViewById(android.R.id.icon); 397 final int level = accessPoint.getLevel(); 398 if (imageView != null) { 399 final Drawable drawable = getContext().getDrawable( 400 Utils.getWifiIconResource(level)); 401 drawable.setTintList( 402 Utils.getColorAttr(getContext(), android.R.attr.colorControlNormal)); 403 imageView.setImageDrawable(drawable); 404 } 405 406 return view; 407 } 408 } 409 410 @Override onAbort()411 public void onAbort() { 412 stopScanningAndPopErrorDialog(ERROR_DIALOG_TYPE.ABORT); 413 } 414 415 @Override onUserSelectionCallbackRegistration( NetworkRequestUserSelectionCallback userSelectionCallback)416 public void onUserSelectionCallbackRegistration( 417 NetworkRequestUserSelectionCallback userSelectionCallback) { 418 mUserSelectionCallback = userSelectionCallback; 419 } 420 421 @Override onMatch(List<ScanResult> scanResults)422 public void onMatch(List<ScanResult> scanResults) { 423 // Shouldn't need to renew cached list, since input result is empty. 424 if (scanResults != null && scanResults.size() > 0) { 425 mHandler.removeMessages(MESSAGE_STOP_SCAN_WIFI_LIST); 426 renewAccessPointList(scanResults); 427 428 notifyAdapterRefresh(); 429 } 430 } 431 432 // Updates internal AccessPoint list from WifiTracker. scanResults are used to update key list 433 // of AccessPoint, and could be null if there is no necessary to update key list. renewAccessPointList(List<ScanResult> scanResults)434 private void renewAccessPointList(List<ScanResult> scanResults) { 435 if (mFilterWifiTracker == null) { 436 return; 437 } 438 439 // TODO(b/119846365): Checks if we could escalate the converting effort. 440 // Updates keys of scanResults into FilterWifiTracker for updating matched AccessPoints. 441 if (scanResults != null) { 442 mFilterWifiTracker.updateKeys(scanResults); 443 } 444 445 // Re-gets matched AccessPoints from WifiTracker. 446 final List<AccessPoint> list = getAccessPointList(); 447 list.clear(); 448 list.addAll(mFilterWifiTracker.getAccessPoints()); 449 } 450 451 @VisibleForTesting notifyAdapterRefresh()452 void notifyAdapterRefresh() { 453 if (getDialogAdapter() != null) { 454 getDialogAdapter().notifyDataSetChanged(); 455 } 456 } 457 458 @Override onUserSelectionConnectSuccess(WifiConfiguration wificonfiguration)459 public void onUserSelectionConnectSuccess(WifiConfiguration wificonfiguration) { 460 final Activity activity = getActivity(); 461 if (activity != null) { 462 Toast.makeText(activity, R.string.network_connection_connect_successful, 463 Toast.LENGTH_SHORT).show(); 464 activity.finish(); 465 } 466 } 467 468 @Override onUserSelectionConnectFailure(WifiConfiguration wificonfiguration)469 public void onUserSelectionConnectFailure(WifiConfiguration wificonfiguration) { 470 // Do nothing when selection is failed, let user could try again easily. 471 mWaitingConnectCallback = false; 472 updateConnectButton(true); 473 } 474 475 private final class FilterWifiTracker { 476 private final List<String> mAccessPointKeys; 477 private final WifiTracker mWifiTracker; 478 FilterWifiTracker(Context context, Lifecycle lifecycle)479 public FilterWifiTracker(Context context, Lifecycle lifecycle) { 480 mWifiTracker = WifiTrackerFactory.create(context, mWifiListener, 481 lifecycle, /* includeSaved */ true, /* includeScans */ true); 482 mAccessPointKeys = new ArrayList<>(); 483 } 484 485 /** 486 * Updates key list from input. {@code onMatch()} may be called in multi-times according 487 * wifi scanning result, so needs patchwork here. 488 */ updateKeys(List<ScanResult> scanResults)489 public void updateKeys(List<ScanResult> scanResults) { 490 for (ScanResult scanResult : scanResults) { 491 final String key = AccessPoint.getKey(scanResult); 492 if (!mAccessPointKeys.contains(key)) { 493 mAccessPointKeys.add(key); 494 } 495 } 496 } 497 498 /** 499 * Returns only AccessPoints whose key is in {@code mAccessPointKeys}. 500 * 501 * @return List of matched AccessPoints. 502 */ getAccessPoints()503 public List<AccessPoint> getAccessPoints() { 504 final List<AccessPoint> allAccessPoints = mWifiTracker.getAccessPoints(); 505 final List<AccessPoint> result = new ArrayList<>(); 506 507 // The order should be kept, because order means wifi score (sorting in WifiTracker). 508 int count = 0; 509 for (AccessPoint accessPoint : allAccessPoints) { 510 final String key = accessPoint.getKey(); 511 if (mAccessPointKeys.contains(key)) { 512 result.add(accessPoint); 513 514 count++; 515 // Limits how many count of items could show. 516 if (mShowLimitedItem && count >= MAX_NUMBER_LIST_ITEM) { 517 break; 518 } 519 } 520 } 521 522 // Update related UI buttons 523 if (mShowLimitedItem && (count >= MAX_NUMBER_LIST_ITEM)) { 524 showAllButton(); 525 } 526 if (count > 0) { 527 hideProgressIcon(); 528 } 529 // Enable connect button if there is Accesspoint item, except for the situation that 530 // user click but connected status doesn't come back yet. 531 if (count < 0) { 532 updateConnectButton(false); 533 } else if (!mWaitingConnectCallback) { 534 updateConnectButton(true); 535 } 536 537 return result; 538 } 539 540 private WifiTracker.WifiListener mWifiListener = new WifiTracker.WifiListener() { 541 542 @Override 543 public void onWifiStateChanged(int state) { 544 notifyAdapterRefresh(); 545 } 546 547 @Override 548 public void onConnectedChanged() { 549 notifyAdapterRefresh(); 550 } 551 552 @Override 553 public void onAccessPointsChanged() { 554 notifyAdapterRefresh(); 555 } 556 }; 557 onDestroy()558 public void onDestroy() { 559 if (mWifiTracker != null) { 560 mWifiTracker.onDestroy(); 561 } 562 } 563 onResume()564 public void onResume() { 565 if (mWifiTracker != null) { 566 mWifiTracker.onStart(); 567 } 568 } 569 onPause()570 public void onPause() { 571 if (mWifiTracker != null) { 572 mWifiTracker.onStop(); 573 } 574 } 575 } 576 } 577