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