1 /* 2 * Copyright (C) 2019 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.settings.wifi.details2; 17 18 import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL; 19 import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY; 20 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; 21 import static android.net.NetworkCapabilities.TRANSPORT_WIFI; 22 import static android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID; 23 24 import static com.android.settingslib.wifi.WifiUtils.getHotspotIconResource; 25 26 import android.app.Activity; 27 import android.app.AlertDialog; 28 import android.app.settings.SettingsEnums; 29 import android.content.AsyncQueryHandler; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.database.Cursor; 33 import android.graphics.Bitmap; 34 import android.graphics.drawable.BitmapDrawable; 35 import android.graphics.drawable.Drawable; 36 import android.graphics.drawable.VectorDrawable; 37 import android.net.CaptivePortalData; 38 import android.net.ConnectivityManager; 39 import android.net.ConnectivityManager.NetworkCallback; 40 import android.net.LinkAddress; 41 import android.net.LinkProperties; 42 import android.net.Network; 43 import android.net.NetworkCapabilities; 44 import android.net.NetworkRequest; 45 import android.net.RouteInfo; 46 import android.net.Uri; 47 import android.net.wifi.WifiConfiguration; 48 import android.net.wifi.WifiInfo; 49 import android.net.wifi.WifiManager; 50 import android.os.Handler; 51 import android.provider.Telephony.CarrierId; 52 import android.telephony.SubscriptionInfo; 53 import android.telephony.SubscriptionManager; 54 import android.text.TextUtils; 55 import android.util.Log; 56 import android.widget.ImageView; 57 import android.widget.Toast; 58 59 import androidx.annotation.VisibleForTesting; 60 import androidx.core.text.BidiFormatter; 61 import androidx.preference.Preference; 62 import androidx.preference.PreferenceFragmentCompat; 63 import androidx.preference.PreferenceScreen; 64 import androidx.recyclerview.widget.RecyclerView; 65 66 import com.android.net.module.util.Inet4AddressUtils; 67 import com.android.settings.R; 68 import com.android.settings.Utils; 69 import com.android.settings.core.PreferenceControllerMixin; 70 import com.android.settings.network.SubscriptionUtil; 71 import com.android.settings.widget.EntityHeaderController; 72 import com.android.settings.wifi.WifiDialog2; 73 import com.android.settings.wifi.WifiDialog2.WifiDialog2Listener; 74 import com.android.settings.wifi.WifiUtils; 75 import com.android.settings.wifi.details.WifiNetworkDetailsFragment; 76 import com.android.settings.wifi.dpp.WifiDppUtils; 77 import com.android.settingslib.core.AbstractPreferenceController; 78 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 79 import com.android.settingslib.core.lifecycle.Lifecycle; 80 import com.android.settingslib.core.lifecycle.LifecycleObserver; 81 import com.android.settingslib.core.lifecycle.events.OnPause; 82 import com.android.settingslib.core.lifecycle.events.OnResume; 83 import com.android.settingslib.utils.StringUtil; 84 import com.android.settingslib.widget.ActionButtonsPreference; 85 import com.android.settingslib.widget.LayoutPreference; 86 import com.android.wifitrackerlib.HotspotNetworkEntry; 87 import com.android.wifitrackerlib.WifiEntry; 88 import com.android.wifitrackerlib.WifiEntry.ConnectCallback; 89 import com.android.wifitrackerlib.WifiEntry.DisconnectCallback; 90 import com.android.wifitrackerlib.WifiEntry.ForgetCallback; 91 import com.android.wifitrackerlib.WifiEntry.SignInCallback; 92 import com.android.wifitrackerlib.WifiEntry.WifiEntryCallback; 93 94 import java.net.Inet4Address; 95 import java.net.Inet6Address; 96 import java.net.InetAddress; 97 import java.time.Duration; 98 import java.time.Instant; 99 import java.time.ZonedDateTime; 100 import java.time.format.DateTimeFormatter; 101 import java.time.format.FormatStyle; 102 import java.util.List; 103 import java.util.StringJoiner; 104 import java.util.stream.Collectors; 105 106 // TODO(b/151133650): Replace AbstractPreferenceController with BasePreferenceController. 107 /** 108 * Controller for logic pertaining to displaying Wifi information for the 109 * {@link WifiNetworkDetailsFragment}. 110 */ 111 public class WifiDetailPreferenceController2 extends AbstractPreferenceController 112 implements PreferenceControllerMixin, WifiDialog2Listener, LifecycleObserver, OnPause, 113 OnResume, WifiEntryCallback, ConnectCallback, DisconnectCallback, ForgetCallback, 114 SignInCallback { 115 116 private static final String TAG = "WifiDetailsPrefCtrl2"; 117 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 118 119 @VisibleForTesting 120 static final String KEY_HEADER = "connection_header"; 121 @VisibleForTesting 122 static final String KEY_DATA_USAGE_HEADER = "status_header"; 123 @VisibleForTesting 124 static final String KEY_BUTTONS_PREF = "buttons"; 125 @VisibleForTesting 126 static final String KEY_SIGNAL_STRENGTH_PREF = "signal_strength"; 127 @VisibleForTesting 128 static final String KEY_TX_LINK_SPEED = "tx_link_speed"; 129 @VisibleForTesting 130 static final String KEY_RX_LINK_SPEED = "rx_link_speed"; 131 @VisibleForTesting 132 static final String KEY_FREQUENCY_PREF = "frequency"; 133 @VisibleForTesting 134 static final String KEY_SECURITY_PREF = "security"; 135 @VisibleForTesting 136 static final String KEY_SSID_PREF = "ssid"; 137 @VisibleForTesting 138 static final String KEY_EAP_SIM_SUBSCRIPTION_PREF = "eap_sim_subscription"; 139 @VisibleForTesting 140 static final String KEY_MAC_ADDRESS_PREF = "mac_address"; 141 @VisibleForTesting 142 static final String KEY_IP_ADDRESS_PREF = "ip_address"; 143 @VisibleForTesting 144 static final String KEY_GATEWAY_PREF = "gateway"; 145 @VisibleForTesting 146 static final String KEY_SUBNET_MASK_PREF = "subnet_mask"; 147 @VisibleForTesting 148 static final String KEY_DNS_PREF = "dns"; 149 @VisibleForTesting 150 static final String KEY_IPV6_CATEGORY = "ipv6_category"; 151 @VisibleForTesting 152 static final String KEY_IPV6_ADDRESSES_PREF = "ipv6_addresses"; 153 @VisibleForTesting 154 static final String KEY_WIFI_TYPE_PREF = "type"; 155 156 private final WifiEntry mWifiEntry; 157 private final ConnectivityManager mConnectivityManager; 158 private final PreferenceFragmentCompat mFragment; 159 private final Handler mHandler; 160 private LinkProperties mLinkProperties; 161 private Network mNetwork; 162 private NetworkCapabilities mNetworkCapabilities; 163 private int mRssiSignalLevel = -1; 164 @VisibleForTesting boolean mShowX; // Shows the Wi-Fi signal icon of Pie+x when it's true. 165 private String[] mSignalStr; 166 private final WifiManager mWifiManager; 167 private final MetricsFeatureProvider mMetricsFeatureProvider; 168 169 // UI elements - in order of appearance 170 private ActionButtonsPreference mButtonsPref; 171 @VisibleForTesting 172 EntityHeaderController mEntityHeaderController; 173 private Preference mSignalStrengthPref; 174 private Preference mTxLinkSpeedPref; 175 private Preference mRxLinkSpeedPref; 176 private Preference mFrequencyPref; 177 private Preference mSecurityPref; 178 private Preference mSsidPref; 179 private Preference mEapSimSubscriptionPref; 180 private Preference mMacAddressPref; 181 private Preference mIpAddressPref; 182 private Preference mGatewayPref; 183 private Preference mSubnetPref; 184 private Preference mDnsPref; 185 private Preference mTypePref; 186 private Preference mIpv6AddressPref; 187 private final IconInjector mIconInjector; 188 private final Clock mClock; 189 190 private final NetworkRequest mNetworkRequest = new NetworkRequest.Builder() 191 .clearCapabilities().addTransportType(TRANSPORT_WIFI).build(); 192 193 private CarrierIdAsyncQueryHandler mCarrierIdAsyncQueryHandler; 194 private static final int TOKEN_QUERY_CARRIER_ID_AND_UPDATE_SIM_SUMMARY = 1; 195 private static final int COLUMN_CARRIER_NAME = 0; 196 197 private class CarrierIdAsyncQueryHandler extends AsyncQueryHandler { 198 CarrierIdAsyncQueryHandler(Context context)199 private CarrierIdAsyncQueryHandler(Context context) { 200 super(context.getContentResolver()); 201 } 202 203 @Override onQueryComplete(int token, Object cookie, Cursor cursor)204 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 205 if (token == TOKEN_QUERY_CARRIER_ID_AND_UPDATE_SIM_SUMMARY) { 206 if (mContext == null || cursor == null || !cursor.moveToFirst()) { 207 if (cursor != null) { 208 cursor.close(); 209 } 210 mEapSimSubscriptionPref.setSummary(R.string.wifi_require_sim_card_to_connect); 211 return; 212 } 213 mEapSimSubscriptionPref.setSummary(mContext.getString( 214 R.string.wifi_require_specific_sim_card_to_connect, 215 cursor.getString(COLUMN_CARRIER_NAME))); 216 cursor.close(); 217 return; 218 } 219 } 220 } 221 222 // Must be run on the UI thread since it directly manipulates UI state. 223 private final NetworkCallback mNetworkCallback = new NetworkCallback() { 224 @Override 225 public void onLinkPropertiesChanged(Network network, LinkProperties lp) { 226 if (network.equals(mNetwork) && !lp.equals(mLinkProperties)) { 227 mLinkProperties = lp; 228 refreshEntityHeader(); 229 refreshButtons(); 230 refreshIpLayerInfo(); 231 } 232 } 233 234 private boolean hasCapabilityChanged(NetworkCapabilities nc, int cap) { 235 // If this is the first time we get NetworkCapabilities, report that something changed. 236 if (mNetworkCapabilities == null) return true; 237 238 // nc can never be null, see ConnectivityService#callCallbackForRequest. 239 return mNetworkCapabilities.hasCapability(cap) != nc.hasCapability(cap); 240 } 241 242 private boolean hasPrivateDnsStatusChanged(NetworkCapabilities nc) { 243 // If this is the first time that WifiDetailPreferenceController2 gets 244 // NetworkCapabilities, report that something has changed and assign nc to 245 // mNetworkCapabilities in onCapabilitiesChanged. Note that the NetworkCapabilities 246 // from onCapabilitiesChanged() will never be null, so calling 247 // mNetworkCapabilities.isPrivateDnsBroken() would be safe next time. 248 if (mNetworkCapabilities == null) { 249 return true; 250 } 251 252 return mNetworkCapabilities.isPrivateDnsBroken() != nc.isPrivateDnsBroken(); 253 } 254 255 @Override 256 public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) { 257 // If the network just validated or lost Internet access or detected partial internet 258 // connectivity or private dns was broken, refresh network state. Don't do this on 259 // every NetworkCapabilities change because refreshEntityHeader sends IPCs to the 260 // system server from the UI thread, which can cause jank. 261 if (network.equals(mNetwork) && !nc.equals(mNetworkCapabilities)) { 262 if (hasPrivateDnsStatusChanged(nc) 263 || hasCapabilityChanged(nc, NET_CAPABILITY_VALIDATED) 264 || hasCapabilityChanged(nc, NET_CAPABILITY_CAPTIVE_PORTAL) 265 || hasCapabilityChanged(nc, NET_CAPABILITY_PARTIAL_CONNECTIVITY)) { 266 refreshEntityHeader(); 267 } 268 mNetworkCapabilities = nc; 269 refreshButtons(); 270 refreshIpLayerInfo(); 271 } 272 } 273 274 @Override 275 public void onLost(Network network) { 276 // Ephemeral network not a saved network, leave detail page once disconnected 277 if (!mWifiEntry.isSaved() && network.equals(mNetwork)) { 278 if (DEBUG) { 279 Log.d(TAG, "OnLost and exit WifiNetworkDetailsPage"); 280 } 281 mFragment.getActivity().finish(); 282 } 283 } 284 }; 285 286 /** 287 * To get an instance of {@link WifiDetailPreferenceController2} 288 */ newInstance( WifiEntry wifiEntry, ConnectivityManager connectivityManager, Context context, PreferenceFragmentCompat fragment, Handler handler, Lifecycle lifecycle, WifiManager wifiManager, MetricsFeatureProvider metricsFeatureProvider)289 public static WifiDetailPreferenceController2 newInstance( 290 WifiEntry wifiEntry, 291 ConnectivityManager connectivityManager, 292 Context context, 293 PreferenceFragmentCompat fragment, 294 Handler handler, 295 Lifecycle lifecycle, 296 WifiManager wifiManager, 297 MetricsFeatureProvider metricsFeatureProvider) { 298 return new WifiDetailPreferenceController2( 299 wifiEntry, connectivityManager, context, fragment, handler, lifecycle, 300 wifiManager, metricsFeatureProvider, new IconInjector(context), new Clock()); 301 } 302 303 @VisibleForTesting WifiDetailPreferenceController2( WifiEntry wifiEntry, ConnectivityManager connectivityManager, Context context, PreferenceFragmentCompat fragment, Handler handler, Lifecycle lifecycle, WifiManager wifiManager, MetricsFeatureProvider metricsFeatureProvider, IconInjector injector, Clock clock)304 /* package */ WifiDetailPreferenceController2( 305 WifiEntry wifiEntry, 306 ConnectivityManager connectivityManager, 307 Context context, 308 PreferenceFragmentCompat fragment, 309 Handler handler, 310 Lifecycle lifecycle, 311 WifiManager wifiManager, 312 MetricsFeatureProvider metricsFeatureProvider, 313 IconInjector injector, 314 Clock clock) { 315 super(context); 316 317 mWifiEntry = wifiEntry; 318 mWifiEntry.setListener(this); 319 mConnectivityManager = connectivityManager; 320 mFragment = fragment; 321 mHandler = handler; 322 mSignalStr = context.getResources().getStringArray(R.array.wifi_signal); 323 mWifiManager = wifiManager; 324 mMetricsFeatureProvider = metricsFeatureProvider; 325 mIconInjector = injector; 326 mClock = clock; 327 328 lifecycle.addObserver(this); 329 } 330 331 @Override isAvailable()332 public boolean isAvailable() { 333 return true; 334 } 335 336 @Override getPreferenceKey()337 public String getPreferenceKey() { 338 // Returns null since this controller contains more than one Preference 339 return null; 340 } 341 342 @Override displayPreference(PreferenceScreen screen)343 public void displayPreference(PreferenceScreen screen) { 344 super.displayPreference(screen); 345 346 setupEntityHeader(screen); 347 348 mButtonsPref = ((ActionButtonsPreference) screen.findPreference(KEY_BUTTONS_PREF)) 349 .setButton1Text(R.string.forget) 350 .setButton1Icon(R.drawable.ic_settings_delete) 351 .setButton1OnClickListener(view -> forgetNetwork()) 352 .setButton2Text(R.string.wifi_sign_in_button_text) 353 .setButton2Icon(R.drawable.ic_settings_sign_in) 354 .setButton2OnClickListener(view -> signIntoNetwork()) 355 .setButton3Text(getConnectDisconnectButtonTextResource()) 356 .setButton3Icon(getConnectDisconnectButtonIconResource()) 357 .setButton3OnClickListener(view -> connectDisconnectNetwork()) 358 .setButton4Text(R.string.share) 359 .setButton4Icon(R.drawable.ic_qrcode_24dp) 360 .setButton4OnClickListener(view -> shareNetwork()); 361 updateCaptivePortalButton(); 362 363 mSignalStrengthPref = screen.findPreference(KEY_SIGNAL_STRENGTH_PREF); 364 mTxLinkSpeedPref = screen.findPreference(KEY_TX_LINK_SPEED); 365 mRxLinkSpeedPref = screen.findPreference(KEY_RX_LINK_SPEED); 366 mFrequencyPref = screen.findPreference(KEY_FREQUENCY_PREF); 367 mSecurityPref = screen.findPreference(KEY_SECURITY_PREF); 368 369 mSsidPref = screen.findPreference(KEY_SSID_PREF); 370 mEapSimSubscriptionPref = screen.findPreference(KEY_EAP_SIM_SUBSCRIPTION_PREF); 371 mMacAddressPref = screen.findPreference(KEY_MAC_ADDRESS_PREF); 372 mIpAddressPref = screen.findPreference(KEY_IP_ADDRESS_PREF); 373 mGatewayPref = screen.findPreference(KEY_GATEWAY_PREF); 374 mSubnetPref = screen.findPreference(KEY_SUBNET_MASK_PREF); 375 mDnsPref = screen.findPreference(KEY_DNS_PREF); 376 mTypePref = screen.findPreference(KEY_WIFI_TYPE_PREF); 377 mIpv6AddressPref = screen.findPreference(KEY_IPV6_ADDRESSES_PREF); 378 } 379 380 /** 381 * Update text, icon and listener of the captive portal button. 382 * @return True if the button should be shown. 383 */ updateCaptivePortalButton()384 private boolean updateCaptivePortalButton() { 385 final Uri venueInfoUrl = getCaptivePortalVenueInfoUrl(); 386 if (venueInfoUrl == null) { 387 mButtonsPref.setButton2Text(R.string.wifi_sign_in_button_text) 388 .setButton2Icon(R.drawable.ic_settings_sign_in) 389 .setButton2OnClickListener(view -> signIntoNetwork()); 390 return canSignIntoNetwork(); 391 } 392 393 mButtonsPref.setButton2Text(R.string.wifi_venue_website_button_text) 394 .setButton2Icon(R.drawable.ic_settings_sign_in) 395 .setButton2OnClickListener(view -> launchCaptivePortal(venueInfoUrl)); 396 // Only show the venue website when the network is connected. 397 return mWifiEntry.getConnectedState() == WifiEntry.CONNECTED_STATE_CONNECTED; 398 } 399 getCaptivePortalVenueInfoUrl()400 private Uri getCaptivePortalVenueInfoUrl() { 401 final LinkProperties lp = mLinkProperties; 402 if (lp == null) { 403 return null; 404 } 405 final CaptivePortalData data = lp.getCaptivePortalData(); 406 if (data == null) { 407 return null; 408 } 409 return data.getVenueInfoUrl(); 410 } 411 412 @VisibleForTesting launchCaptivePortal(Uri uri)413 void launchCaptivePortal(Uri uri) { 414 if (uri == null) { 415 Log.e(TAG, "Launch captive portal with a null Uri!"); 416 return; 417 } 418 final Intent infoIntent = new Intent(Intent.ACTION_VIEW); 419 infoIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 420 infoIntent.setData(uri); 421 mContext.startActivity(infoIntent); 422 } 423 setupEntityHeader(PreferenceScreen screen)424 private void setupEntityHeader(PreferenceScreen screen) { 425 LayoutPreference headerPref = screen.findPreference(KEY_HEADER); 426 427 mEntityHeaderController = 428 EntityHeaderController.newInstance( 429 mFragment.getActivity(), mFragment, 430 headerPref.findViewById(R.id.entity_header)); 431 432 ImageView iconView = headerPref.findViewById(R.id.entity_header_icon); 433 434 iconView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); 435 } 436 getExpiryTimeSummary()437 private String getExpiryTimeSummary() { 438 if (mLinkProperties == null || mLinkProperties.getCaptivePortalData() == null) { 439 return null; 440 } 441 442 final long expiryTimeMillis = mLinkProperties.getCaptivePortalData().getExpiryTimeMillis(); 443 if (expiryTimeMillis <= 0) { 444 return null; 445 } 446 final ZonedDateTime now = mClock.now(); 447 final ZonedDateTime expiryTime = ZonedDateTime.ofInstant( 448 Instant.ofEpochMilli(expiryTimeMillis), 449 now.getZone()); 450 451 if (now.isAfter(expiryTime)) { 452 return null; 453 } 454 455 if (now.plusDays(2).isAfter(expiryTime)) { 456 // Expiration within 2 days: show a duration 457 return mContext.getString(R.string.wifi_time_remaining, StringUtil.formatElapsedTime( 458 mContext, 459 Duration.between(now, expiryTime).getSeconds() * 1000, 460 false /* withSeconds */, false /* collapseTimeUnit */)); 461 } 462 463 // For more than 2 days, show the expiry date 464 return mContext.getString(R.string.wifi_expiry_time, 465 DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).format(expiryTime)); 466 } 467 refreshEntityHeader()468 private void refreshEntityHeader() { 469 mEntityHeaderController 470 .setLabel(mWifiEntry.getTitle()) 471 .setSummary(mWifiEntry.getSummary()) 472 .setSecondSummary(getExpiryTimeSummary()) 473 .done(true /* rebind */); 474 } 475 476 @VisibleForTesting updateNetworkInfo()477 void updateNetworkInfo() { 478 if (mWifiEntry.getConnectedState() == WifiEntry.CONNECTED_STATE_CONNECTED) { 479 mNetwork = mWifiManager.getCurrentNetwork(); 480 mLinkProperties = mConnectivityManager.getLinkProperties(mNetwork); 481 mNetworkCapabilities = mConnectivityManager.getNetworkCapabilities(mNetwork); 482 } else { 483 mNetwork = null; 484 mLinkProperties = null; 485 mNetworkCapabilities = null; 486 } 487 } 488 489 @Override onResume()490 public void onResume() { 491 // Disable the animation of the EntityHeaderController 492 final RecyclerView recyclerView = mFragment.getListView(); 493 if (recyclerView != null) { 494 recyclerView.setItemAnimator(null); 495 } 496 497 // Ensure mNetwork is set before any callbacks above are delivered, since our 498 // NetworkCallback only looks at changes to mNetwork. 499 updateNetworkInfo(); 500 refreshPage(); 501 mConnectivityManager.registerNetworkCallback(mNetworkRequest, mNetworkCallback, 502 mHandler); 503 } 504 505 @Override onPause()506 public void onPause() { 507 mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); 508 } 509 refreshPage()510 private void refreshPage() { 511 Log.d(TAG, "Update UI!"); 512 513 // refresh header icon 514 refreshEntryHeaderIcon(); 515 // refresh header 516 refreshEntityHeader(); 517 518 // refresh Buttons 519 refreshButtons(); 520 521 // Update Connection Header icon and Signal Strength Preference 522 refreshRssiViews(); 523 // Frequency Pref 524 refreshFrequency(); 525 // Security Pref 526 refreshSecurity(); 527 // Transmit Link Speed Pref 528 refreshTxSpeed(); 529 // Receive Link Speed Pref 530 refreshRxSpeed(); 531 // IP related information 532 refreshIpLayerInfo(); 533 // SSID Pref 534 refreshSsid(); 535 // EAP SIM subscription 536 refreshEapSimSubscription(); 537 // MAC Address Pref 538 refreshMacAddress(); 539 // Wifi Type 540 refreshWifiType(); 541 } 542 543 @VisibleForTesting refreshEntryHeaderIcon()544 void refreshEntryHeaderIcon() { 545 if (mEntityHeaderController == null) { 546 return; 547 } 548 Drawable drawable = getWifiDrawable(mWifiEntry); 549 mEntityHeaderController 550 .setIcon(redrawIconForHeader(drawable)) 551 .done(true /* rebind */); 552 } 553 554 /** 555 * Returns a Wi-Fi icon {@link Drawable}. 556 * 557 * @param wifiEntry {@link WifiEntry} 558 */ 559 @VisibleForTesting getWifiDrawable(WifiEntry wifiEntry)560 Drawable getWifiDrawable(WifiEntry wifiEntry) { 561 if (wifiEntry instanceof HotspotNetworkEntry) { 562 int deviceType = ((HotspotNetworkEntry) wifiEntry).getDeviceType(); 563 return mContext.getDrawable(getHotspotIconResource(deviceType)); 564 } 565 if (mWifiEntry.getLevel() == WifiEntry.WIFI_LEVEL_UNREACHABLE) { 566 Log.w(TAG, "WiFi level is WIFI_LEVEL_UNREACHABLE(-1)"); 567 return mContext.getDrawable(R.drawable.empty_icon); 568 } 569 return mIconInjector.getIcon(wifiEntry.shouldShowXLevelIcon(), wifiEntry.getLevel()); 570 } 571 refreshRssiViews()572 private void refreshRssiViews() { 573 int signalLevel = mWifiEntry.getLevel(); 574 575 // Disappears signal view if not in range. e.g. for saved networks. 576 if (signalLevel == WifiEntry.WIFI_LEVEL_UNREACHABLE) { 577 mSignalStrengthPref.setVisible(false); 578 mRssiSignalLevel = -1; 579 return; 580 } 581 582 boolean showX = mWifiEntry.shouldShowXLevelIcon(); 583 if (mRssiSignalLevel == signalLevel && mShowX == showX) { 584 return; 585 } 586 mRssiSignalLevel = signalLevel; 587 mShowX = showX; 588 Drawable wifiIcon = mIconInjector.getIcon(mShowX, mRssiSignalLevel); 589 Drawable wifiIconDark = wifiIcon.getConstantState().newDrawable().mutate(); 590 wifiIconDark.setTintList(Utils.getColorAttr(mContext, android.R.attr.colorControlNormal)); 591 mSignalStrengthPref.setIcon(wifiIconDark); 592 593 mSignalStrengthPref.setSummary(mSignalStr[mRssiSignalLevel]); 594 mSignalStrengthPref.setVisible(true); 595 } 596 redrawIconForHeader(Drawable original)597 private Drawable redrawIconForHeader(Drawable original) { 598 final int iconSize = mContext.getResources().getDimensionPixelSize( 599 R.dimen.wifi_detail_page_header_image_size); 600 final int actualWidth = original.getMinimumWidth(); 601 final int actualHeight = original.getMinimumHeight(); 602 603 if ((actualWidth == iconSize && actualHeight == iconSize) 604 || !VectorDrawable.class.isInstance(original)) { 605 return original; 606 } 607 608 // clear tint list to make sure can set 87% black after enlarge 609 original.setTintList(null); 610 611 // enlarge icon size 612 final Bitmap bitmap = Utils.createBitmap(original, 613 iconSize /*width*/, 614 iconSize /*height*/); 615 Drawable newIcon = new BitmapDrawable(null /*resource*/, bitmap); 616 617 // config color for 87% black after enlarge 618 newIcon.setTintList(Utils.getColorAttr(mContext, android.R.attr.textColorPrimary)); 619 620 return newIcon; 621 } 622 refreshFrequency()623 private void refreshFrequency() { 624 final String bandString = mWifiEntry.getBandString(); 625 if (TextUtils.isEmpty(bandString)) { 626 mFrequencyPref.setVisible(false); 627 return; 628 } 629 mFrequencyPref.setSummary(bandString); 630 mFrequencyPref.setVisible(true); 631 } 632 refreshSecurity()633 private void refreshSecurity() { 634 mSecurityPref.setSummary(mWifiEntry.getSecurityString(false /* concise */)); 635 } 636 refreshTxSpeed()637 private void refreshTxSpeed() { 638 String summary = mWifiEntry.getTxSpeedString(); 639 if (TextUtils.isEmpty(summary)) { 640 mTxLinkSpeedPref.setVisible(false); 641 return; 642 } 643 mTxLinkSpeedPref.setVisible(true); 644 mTxLinkSpeedPref.setSummary(summary); 645 } 646 refreshRxSpeed()647 private void refreshRxSpeed() { 648 String summary = mWifiEntry.getRxSpeedString(); 649 if (TextUtils.isEmpty(summary)) { 650 mRxLinkSpeedPref.setVisible(false); 651 return; 652 } 653 mRxLinkSpeedPref.setVisible(true); 654 mRxLinkSpeedPref.setSummary(summary); 655 } 656 refreshSsid()657 private void refreshSsid() { 658 if (mWifiEntry.shouldShowSsid() && mWifiEntry.getSsid() != null) { 659 mSsidPref.setVisible(true); 660 mSsidPref.setSummary(mWifiEntry.getSsid()); 661 } else { 662 mSsidPref.setVisible(false); 663 } 664 } 665 refreshEapSimSubscription()666 private void refreshEapSimSubscription() { 667 mEapSimSubscriptionPref.setVisible(false); 668 669 if (mWifiEntry.getSecurity() != WifiEntry.SECURITY_EAP) { 670 return; 671 } 672 final WifiConfiguration config = mWifiEntry.getWifiConfiguration(); 673 if (config == null || config.enterpriseConfig == null) { 674 return; 675 } 676 if (!config.enterpriseConfig.isAuthenticationSimBased()) { 677 return; 678 } 679 680 mEapSimSubscriptionPref.setVisible(true); 681 682 // Checks if the SIM subscription is active. 683 final List<SubscriptionInfo> activeSubscriptionInfos = mContext 684 .getSystemService(SubscriptionManager.class).getActiveSubscriptionInfoList(); 685 if (activeSubscriptionInfos != null) { 686 SubscriptionInfo info = fineSubscriptionInfo(config.carrierId, activeSubscriptionInfos, 687 SubscriptionManager.getDefaultDataSubscriptionId()); 688 if (info != null) { 689 mEapSimSubscriptionPref.setSummary( 690 SubscriptionUtil.getUniqueSubscriptionDisplayName(info, mContext)); 691 return; 692 } 693 } 694 695 if (config.carrierId == UNKNOWN_CARRIER_ID) { 696 mEapSimSubscriptionPref.setSummary(R.string.wifi_no_related_sim_card); 697 return; 698 } 699 700 // The Wi-Fi network has specified carrier id, query carrier name from CarrierIdProvider. 701 if (mCarrierIdAsyncQueryHandler == null) { 702 mCarrierIdAsyncQueryHandler = new CarrierIdAsyncQueryHandler(mContext); 703 } 704 mCarrierIdAsyncQueryHandler.cancelOperation(TOKEN_QUERY_CARRIER_ID_AND_UPDATE_SIM_SUMMARY); 705 mCarrierIdAsyncQueryHandler.startQuery(TOKEN_QUERY_CARRIER_ID_AND_UPDATE_SIM_SUMMARY, 706 null /* cookie */, 707 CarrierId.All.CONTENT_URI, 708 new String[]{CarrierId.CARRIER_NAME}, 709 CarrierId.CARRIER_ID + "=?", 710 new String[] {Integer.toString(config.carrierId)}, 711 null /* orderBy */); 712 } 713 714 @VisibleForTesting fineSubscriptionInfo(int carrierId, List<SubscriptionInfo> activeSubscriptionInfos, int defaultDataSubscriptionId)715 SubscriptionInfo fineSubscriptionInfo(int carrierId, 716 List<SubscriptionInfo> activeSubscriptionInfos, int defaultDataSubscriptionId) { 717 SubscriptionInfo firstMatchedInfo = null; 718 for (SubscriptionInfo info : activeSubscriptionInfos) { 719 // When it's UNKNOWN_CARRIER_ID or matched with configured CarrierId, 720 // devices connects it with the SIM subscription of defaultDataSubscriptionId. 721 if (defaultDataSubscriptionId == info.getSubscriptionId() 722 && (carrierId == info.getCarrierId() || carrierId == UNKNOWN_CARRIER_ID)) { 723 return info; 724 } 725 726 if (firstMatchedInfo == null && carrierId == info.getCarrierId()) { 727 firstMatchedInfo = info; 728 } 729 } 730 return firstMatchedInfo; 731 } 732 refreshMacAddress()733 private void refreshMacAddress() { 734 final String macAddress = mWifiEntry.getMacAddress(); 735 if (TextUtils.isEmpty(macAddress)) { 736 mMacAddressPref.setVisible(false); 737 return; 738 } 739 740 mMacAddressPref.setVisible(true); 741 mMacAddressPref.setTitle(getMacAddressTitle()); 742 743 if (macAddress.equals(WifiInfo.DEFAULT_MAC_ADDRESS)) { 744 mMacAddressPref.setSummary(R.string.device_info_not_available); 745 } else { 746 mMacAddressPref.setSummary(macAddress); 747 } 748 } 749 refreshWifiType()750 private void refreshWifiType() { 751 final String typeString = mWifiEntry.getStandardString(); 752 if (!TextUtils.isEmpty(typeString)) { 753 mTypePref.setSummary(typeString); 754 mTypePref.setVisible(true); 755 } else { 756 mTypePref.setVisible(false); 757 } 758 } 759 getMacAddressTitle()760 private int getMacAddressTitle() { 761 if (mWifiEntry.getPrivacy() == WifiEntry.PRIVACY_RANDOMIZED_MAC) { 762 return mWifiEntry.getConnectedState() == WifiEntry.CONNECTED_STATE_CONNECTED 763 ? R.string.wifi_advanced_randomized_mac_address_title 764 : R.string.wifi_advanced_randomized_mac_address_disconnected_title; 765 } 766 return R.string.wifi_advanced_device_mac_address_title; 767 } 768 updatePreference(Preference pref, String detailText)769 private void updatePreference(Preference pref, String detailText) { 770 if (!TextUtils.isEmpty(detailText)) { 771 pref.setSummary(detailText); 772 pref.setVisible(true); 773 } else { 774 pref.setVisible(false); 775 } 776 } 777 refreshButtons()778 private void refreshButtons() { 779 final boolean canForgetNetwork = canForgetNetwork(); 780 final boolean showCaptivePortalButton = updateCaptivePortalButton(); 781 final boolean canConnectDisconnectNetwork = mWifiEntry.canConnect() 782 || mWifiEntry.canDisconnect(); 783 final boolean canShareNetwork = canShareNetwork(); 784 785 mButtonsPref.setButton1Visible(canForgetNetwork); 786 mButtonsPref.setButton2Visible(showCaptivePortalButton); 787 // Keep the connect/disconnected button visible if we can connect/disconnect, or if we are 788 // in the middle of connecting (greyed out). 789 mButtonsPref.setButton3Visible(canConnectDisconnectNetwork 790 || mWifiEntry.getConnectedState() == WifiEntry.CONNECTED_STATE_CONNECTING); 791 mButtonsPref.setButton3Enabled(canConnectDisconnectNetwork); 792 mButtonsPref.setButton3Text(getConnectDisconnectButtonTextResource()); 793 mButtonsPref.setButton3Icon(getConnectDisconnectButtonIconResource()); 794 mButtonsPref.setButton4Visible(canShareNetwork); 795 mButtonsPref.setVisible(canForgetNetwork 796 || showCaptivePortalButton 797 || canConnectDisconnectNetwork 798 || canShareNetwork); 799 } 800 getConnectDisconnectButtonTextResource()801 private int getConnectDisconnectButtonTextResource() { 802 switch (mWifiEntry.getConnectedState()) { 803 case WifiEntry.CONNECTED_STATE_DISCONNECTED: 804 return R.string.wifi_connect; 805 case WifiEntry.CONNECTED_STATE_CONNECTED: 806 return R.string.wifi_disconnect_button_text; 807 case WifiEntry.CONNECTED_STATE_CONNECTING: 808 return R.string.wifi_connecting; 809 default: 810 throw new IllegalStateException("Invalid WifiEntry connected state"); 811 } 812 } 813 getConnectDisconnectButtonIconResource()814 private int getConnectDisconnectButtonIconResource() { 815 switch (mWifiEntry.getConnectedState()) { 816 case WifiEntry.CONNECTED_STATE_DISCONNECTED: 817 case WifiEntry.CONNECTED_STATE_CONNECTING: 818 return R.drawable.ic_settings_wireless; 819 case WifiEntry.CONNECTED_STATE_CONNECTED: 820 return R.drawable.ic_settings_close; 821 default: 822 throw new IllegalStateException("Invalid WifiEntry connected state"); 823 } 824 } 825 refreshIpLayerInfo()826 private void refreshIpLayerInfo() { 827 // Hide IP layer info if not a connected network. 828 if (mWifiEntry.getConnectedState() != WifiEntry.CONNECTED_STATE_CONNECTED 829 || mNetwork == null || mLinkProperties == null) { 830 mIpAddressPref.setVisible(false); 831 mSubnetPref.setVisible(false); 832 mGatewayPref.setVisible(false); 833 mDnsPref.setVisible(false); 834 mIpv6AddressPref.setVisible(false); 835 return; 836 } 837 838 // Find IPv4 and IPv6 addresses. 839 String ipv4Address = null; 840 String subnet = null; 841 StringJoiner ipv6Addresses = new StringJoiner("\n"); 842 843 for (LinkAddress addr : mLinkProperties.getLinkAddresses()) { 844 if (addr.getAddress() instanceof Inet4Address) { 845 ipv4Address = addr.getAddress().getHostAddress(); 846 subnet = ipv4PrefixLengthToSubnetMask(addr.getPrefixLength()); 847 } else if (addr.getAddress() instanceof Inet6Address) { 848 ipv6Addresses.add(addr.getAddress().getHostAddress()); 849 } 850 } 851 852 // Find IPv4 default gateway. 853 String gateway = null; 854 for (RouteInfo routeInfo : mLinkProperties.getRoutes()) { 855 if (routeInfo.hasGateway() && routeInfo.isDefaultRoute() 856 && routeInfo.getDestination().getAddress() instanceof Inet4Address) { 857 gateway = routeInfo.getGateway().getHostAddress(); 858 break; 859 } 860 } 861 862 // Find all (IPv4 and IPv6) DNS addresses. 863 String dnsServers = mLinkProperties.getDnsServers().stream() 864 .map(InetAddress::getHostAddress) 865 .collect(Collectors.joining("\n")); 866 867 // Update UI. 868 updatePreference(mIpAddressPref, ipv4Address); 869 updatePreference(mSubnetPref, subnet); 870 updatePreference(mGatewayPref, gateway); 871 updatePreference(mDnsPref, dnsServers); 872 873 if (ipv6Addresses.length() > 0) { 874 mIpv6AddressPref.setVisible(true); 875 mIpv6AddressPref.setSummary( 876 BidiFormatter.getInstance().unicodeWrap(ipv6Addresses.toString())); 877 } else { 878 mIpv6AddressPref.setVisible(false); 879 } 880 } 881 ipv4PrefixLengthToSubnetMask(int prefixLength)882 private static String ipv4PrefixLengthToSubnetMask(int prefixLength) { 883 try { 884 return Inet4AddressUtils.getPrefixMaskAsInet4Address(prefixLength).getHostAddress(); 885 } catch (IllegalArgumentException e) { 886 return null; 887 } 888 } 889 890 /** 891 * Returns whether the network represented by this preference can be modified. 892 */ canModifyNetwork()893 public boolean canModifyNetwork() { 894 return mWifiEntry.isSaved() 895 && !WifiUtils.isNetworkLockedDown(mContext, mWifiEntry.getWifiConfiguration()); 896 } 897 898 /** 899 * Returns whether the network represented by this preference can be forgotten. 900 */ canForgetNetwork()901 public boolean canForgetNetwork() { 902 return mWifiEntry.canForget() 903 && !WifiUtils.isNetworkLockedDown(mContext, mWifiEntry.getWifiConfiguration()); 904 } 905 906 /** 907 * Returns whether the user can sign into the network represented by this preference. 908 */ canSignIntoNetwork()909 private boolean canSignIntoNetwork() { 910 return mWifiEntry.canSignIn(); 911 } 912 913 /** 914 * Returns whether the user can share the network represented by this preference with QR code. 915 */ canShareNetwork()916 private boolean canShareNetwork() { 917 return mWifiEntry.canShare(); 918 } 919 920 /** 921 * Forgets the wifi network associated with this preference. 922 */ forgetNetwork()923 private void forgetNetwork() { 924 if (mWifiEntry.isSubscription()) { 925 // Post a dialog to confirm if user really want to forget the passpoint network. 926 showConfirmForgetDialog(); 927 return; 928 } else { 929 mWifiEntry.forget(this); 930 } 931 932 final Activity activity = mFragment.getActivity(); 933 if (activity != null) { 934 mMetricsFeatureProvider.action(activity, SettingsEnums.ACTION_WIFI_FORGET); 935 activity.finish(); 936 } 937 } 938 939 @VisibleForTesting showConfirmForgetDialog()940 protected void showConfirmForgetDialog() { 941 final AlertDialog dialog = new AlertDialog.Builder(mContext) 942 .setPositiveButton(R.string.forget, ((dialog1, which) -> { 943 try { 944 mWifiEntry.forget(this); 945 } catch (RuntimeException e) { 946 Log.e(TAG, "Failed to remove Passpoint configuration: " + e); 947 } 948 mMetricsFeatureProvider.action( 949 mFragment.getActivity(), SettingsEnums.ACTION_WIFI_FORGET); 950 mFragment.getActivity().finish(); 951 })) 952 .setNegativeButton(R.string.cancel, null /* listener */) 953 .setTitle(R.string.wifi_forget_dialog_title) 954 .setMessage(R.string.forget_passpoint_dialog_message) 955 .create(); 956 dialog.show(); 957 } 958 959 /** 960 * Show QR code to share the network represented by this preference. 961 */ launchWifiDppConfiguratorActivity()962 private void launchWifiDppConfiguratorActivity() { 963 final Intent intent = WifiDppUtils.getConfiguratorQrCodeGeneratorIntentOrNull(mContext, 964 mWifiManager, mWifiEntry); 965 966 if (intent == null) { 967 Log.e(TAG, "Launch Wi-Fi DPP QR code generator with a wrong Wi-Fi network!"); 968 } else { 969 mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, 970 SettingsEnums.ACTION_SETTINGS_SHARE_WIFI_QR_CODE, 971 SettingsEnums.SETTINGS_WIFI_DPP_CONFIGURATOR, 972 /* key */ null, 973 /* value */ Integer.MIN_VALUE); 974 975 mContext.startActivity(intent); 976 } 977 } 978 979 /** 980 * Share the wifi network with QR code. 981 */ shareNetwork()982 private void shareNetwork() { 983 WifiDppUtils.showLockScreenForWifiSharing(mContext, 984 () -> launchWifiDppConfiguratorActivity()); 985 } 986 987 /** 988 * Sign in to the captive portal found on this wifi network associated with this preference. 989 */ signIntoNetwork()990 private void signIntoNetwork() { 991 mMetricsFeatureProvider.action( 992 mFragment.getActivity(), SettingsEnums.ACTION_WIFI_SIGNIN); 993 mWifiEntry.signIn(this); 994 } 995 996 @Override onSubmit(WifiDialog2 dialog)997 public void onSubmit(WifiDialog2 dialog) { 998 if (dialog.getController() != null) { 999 mWifiManager.save(dialog.getController().getConfig(), new WifiManager.ActionListener() { 1000 @Override 1001 public void onSuccess() { 1002 } 1003 1004 @Override 1005 public void onFailure(int reason) { 1006 Activity activity = mFragment.getActivity(); 1007 if (activity != null) { 1008 Toast.makeText(activity, 1009 R.string.wifi_failed_save_message, 1010 Toast.LENGTH_SHORT).show(); 1011 } 1012 } 1013 }); 1014 } 1015 } 1016 1017 /** 1018 * Wrapper for testing compatibility. 1019 */ 1020 @VisibleForTesting 1021 static class IconInjector { 1022 private final Context mContext; 1023 IconInjector(Context context)1024 IconInjector(Context context) { 1025 mContext = context; 1026 } 1027 getIcon(boolean showX, int level)1028 public Drawable getIcon(boolean showX, int level) { 1029 return mContext.getDrawable(WifiUtils.getInternetIconResource(level, showX)).mutate(); 1030 } 1031 } 1032 1033 @VisibleForTesting 1034 static class Clock { now()1035 public ZonedDateTime now() { 1036 return ZonedDateTime.now(); 1037 } 1038 } 1039 1040 @VisibleForTesting connectDisconnectNetwork()1041 void connectDisconnectNetwork() { 1042 if (mWifiEntry.getConnectedState() == WifiEntry.CONNECTED_STATE_DISCONNECTED) { 1043 mWifiEntry.connect(this); 1044 } else { 1045 mWifiEntry.disconnect(this); 1046 } 1047 } 1048 1049 /** 1050 * Indicates the state of the WifiEntry has changed and clients may retrieve updates through 1051 * the WifiEntry getter methods. 1052 */ 1053 @Override onUpdated()1054 public void onUpdated() { 1055 updateNetworkInfo(); 1056 refreshPage(); 1057 1058 // Refresh the Preferences in fragment. 1059 ((WifiNetworkDetailsFragment) mFragment).refreshPreferences(); 1060 } 1061 1062 /** 1063 * Result of the connect request indicated by the CONNECT_STATUS constants. 1064 */ 1065 @Override onConnectResult(@onnectStatus int status)1066 public void onConnectResult(@ConnectStatus int status) { 1067 if (status == ConnectCallback.CONNECT_STATUS_SUCCESS) { 1068 Toast.makeText(mContext, 1069 mContext.getString(R.string.wifi_connected_to_message, mWifiEntry.getTitle()), 1070 Toast.LENGTH_SHORT).show(); 1071 } else if (mWifiEntry.getLevel() == WifiEntry.WIFI_LEVEL_UNREACHABLE) { 1072 Toast.makeText(mContext, 1073 R.string.wifi_not_in_range_message, 1074 Toast.LENGTH_SHORT).show(); 1075 } else { 1076 Toast.makeText(mContext, 1077 R.string.wifi_failed_connect_message, 1078 Toast.LENGTH_SHORT).show(); 1079 } 1080 } 1081 1082 /** 1083 * Result of the disconnect request indicated by the DISCONNECT_STATUS constants. 1084 */ 1085 @Override onDisconnectResult(@isconnectStatus int status)1086 public void onDisconnectResult(@DisconnectStatus int status) { 1087 if (status == DisconnectCallback.DISCONNECT_STATUS_SUCCESS) { 1088 final Activity activity = mFragment.getActivity(); 1089 if (activity != null) { 1090 Toast.makeText(activity, 1091 activity.getString(R.string.wifi_disconnected_from, mWifiEntry.getTitle()), 1092 Toast.LENGTH_SHORT).show(); 1093 } 1094 } else { 1095 Log.e(TAG, "Disconnect Wi-Fi network failed"); 1096 } 1097 } 1098 1099 /** 1100 * Result of the forget request indicated by the FORGET_STATUS constants. 1101 */ 1102 @Override onForgetResult(@orgetStatus int status)1103 public void onForgetResult(@ForgetStatus int status) { 1104 if (status != ForgetCallback.FORGET_STATUS_SUCCESS) { 1105 Log.e(TAG, "Forget Wi-Fi network failed"); 1106 } 1107 1108 final Activity activity = mFragment.getActivity(); 1109 if (activity != null) { 1110 mMetricsFeatureProvider.action(activity, SettingsEnums.ACTION_WIFI_FORGET); 1111 activity.finish(); 1112 } 1113 } 1114 1115 /** 1116 * Result of the sign-in request indicated by the SIGNIN_STATUS constants. 1117 */ 1118 @Override onSignInResult(@ignInStatus int status)1119 public void onSignInResult(@SignInStatus int status) { 1120 refreshPage(); 1121 } 1122 1123 /** Sets signal strength title */ setSignalStrengthTitle(int titleResId)1124 public void setSignalStrengthTitle(int titleResId) { 1125 if (mSignalStrengthPref != null) { 1126 mSignalStrengthPref.setTitle(titleResId); 1127 } 1128 } 1129 } 1130