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.settings.wifi.details; 17 18 import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL; 19 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; 20 import static android.net.NetworkCapabilities.TRANSPORT_WIFI; 21 22 import android.app.Activity; 23 import android.app.Fragment; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.graphics.drawable.Drawable; 29 import android.net.ConnectivityManager; 30 import android.net.ConnectivityManager.NetworkCallback; 31 import android.net.LinkAddress; 32 import android.net.LinkProperties; 33 import android.net.Network; 34 import android.net.NetworkCapabilities; 35 import android.net.NetworkInfo; 36 import android.net.NetworkRequest; 37 import android.net.NetworkUtils; 38 import android.net.RouteInfo; 39 import android.net.wifi.WifiConfiguration; 40 import android.net.wifi.WifiInfo; 41 import android.net.wifi.WifiManager; 42 import android.os.Handler; 43 import android.support.v4.text.BidiFormatter; 44 import android.support.v7.preference.Preference; 45 import android.support.v7.preference.PreferenceCategory; 46 import android.support.v7.preference.PreferenceScreen; 47 import android.text.TextUtils; 48 import android.util.Log; 49 import android.widget.ImageView; 50 import android.widget.Toast; 51 52 import com.android.internal.annotations.VisibleForTesting; 53 import com.android.internal.logging.nano.MetricsProto; 54 import com.android.settings.R; 55 import com.android.settings.Utils; 56 import com.android.settings.applications.LayoutPreference; 57 import com.android.settings.core.PreferenceControllerMixin; 58 import com.android.settings.widget.ActionButtonPreference; 59 import com.android.settings.widget.EntityHeaderController; 60 import com.android.settings.wifi.WifiDetailPreference; 61 import com.android.settings.wifi.WifiDialog; 62 import com.android.settings.wifi.WifiDialog.WifiDialogListener; 63 import com.android.settings.wifi.WifiUtils; 64 import com.android.settingslib.core.AbstractPreferenceController; 65 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 66 import com.android.settingslib.core.lifecycle.Lifecycle; 67 import com.android.settingslib.core.lifecycle.LifecycleObserver; 68 import com.android.settingslib.core.lifecycle.events.OnPause; 69 import com.android.settingslib.core.lifecycle.events.OnResume; 70 import com.android.settingslib.wifi.AccessPoint; 71 72 import java.net.Inet4Address; 73 import java.net.Inet6Address; 74 import java.net.InetAddress; 75 import java.net.UnknownHostException; 76 import java.util.StringJoiner; 77 import java.util.stream.Collectors; 78 79 /** 80 * Controller for logic pertaining to displaying Wifi information for the 81 * {@link WifiNetworkDetailsFragment}. 82 */ 83 public class WifiDetailPreferenceController extends AbstractPreferenceController 84 implements PreferenceControllerMixin, WifiDialogListener, LifecycleObserver, OnPause, 85 OnResume { 86 87 private static final String TAG = "WifiDetailsPrefCtrl"; 88 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 89 90 @VisibleForTesting 91 static final String KEY_HEADER = "connection_header"; 92 @VisibleForTesting 93 static final String KEY_BUTTONS_PREF = "buttons"; 94 @VisibleForTesting 95 static final String KEY_SIGNAL_STRENGTH_PREF = "signal_strength"; 96 @VisibleForTesting 97 static final String KEY_LINK_SPEED = "link_speed"; 98 @VisibleForTesting 99 static final String KEY_FREQUENCY_PREF = "frequency"; 100 @VisibleForTesting 101 static final String KEY_SECURITY_PREF = "security"; 102 @VisibleForTesting 103 static final String KEY_MAC_ADDRESS_PREF = "mac_address"; 104 @VisibleForTesting 105 static final String KEY_IP_ADDRESS_PREF = "ip_address"; 106 @VisibleForTesting 107 static final String KEY_GATEWAY_PREF = "gateway"; 108 @VisibleForTesting 109 static final String KEY_SUBNET_MASK_PREF = "subnet_mask"; 110 @VisibleForTesting 111 static final String KEY_DNS_PREF = "dns"; 112 @VisibleForTesting 113 static final String KEY_IPV6_CATEGORY = "ipv6_category"; 114 @VisibleForTesting 115 static final String KEY_IPV6_ADDRESSES_PREF = "ipv6_addresses"; 116 117 private AccessPoint mAccessPoint; 118 private final ConnectivityManager mConnectivityManager; 119 private final Fragment mFragment; 120 private final Handler mHandler; 121 private LinkProperties mLinkProperties; 122 private Network mNetwork; 123 private NetworkInfo mNetworkInfo; 124 private NetworkCapabilities mNetworkCapabilities; 125 private int mRssiSignalLevel = -1; 126 private String[] mSignalStr; 127 private WifiConfiguration mWifiConfig; 128 private WifiInfo mWifiInfo; 129 private final WifiManager mWifiManager; 130 private final MetricsFeatureProvider mMetricsFeatureProvider; 131 132 // UI elements - in order of appearance 133 private ActionButtonPreference mButtonsPref; 134 private EntityHeaderController mEntityHeaderController; 135 private WifiDetailPreference mSignalStrengthPref; 136 private WifiDetailPreference mLinkSpeedPref; 137 private WifiDetailPreference mFrequencyPref; 138 private WifiDetailPreference mSecurityPref; 139 private WifiDetailPreference mMacAddressPref; 140 private WifiDetailPreference mIpAddressPref; 141 private WifiDetailPreference mGatewayPref; 142 private WifiDetailPreference mSubnetPref; 143 private WifiDetailPreference mDnsPref; 144 private PreferenceCategory mIpv6Category; 145 private Preference mIpv6AddressPref; 146 147 private final IconInjector mIconInjector; 148 private final IntentFilter mFilter; 149 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 150 @Override 151 public void onReceive(Context context, Intent intent) { 152 switch (intent.getAction()) { 153 case WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION: 154 if (!intent.getBooleanExtra(WifiManager.EXTRA_MULTIPLE_NETWORKS_CHANGED, 155 false /* defaultValue */)) { 156 // only one network changed 157 WifiConfiguration wifiConfiguration = intent 158 .getParcelableExtra(WifiManager.EXTRA_WIFI_CONFIGURATION); 159 if (mAccessPoint.matches(wifiConfiguration)) { 160 mWifiConfig = wifiConfiguration; 161 } 162 } 163 // fall through 164 case WifiManager.NETWORK_STATE_CHANGED_ACTION: 165 case WifiManager.RSSI_CHANGED_ACTION: 166 updateInfo(); 167 break; 168 } 169 } 170 }; 171 172 private final NetworkRequest mNetworkRequest = new NetworkRequest.Builder() 173 .clearCapabilities().addTransportType(TRANSPORT_WIFI).build(); 174 175 // Must be run on the UI thread since it directly manipulates UI state. 176 private final NetworkCallback mNetworkCallback = new NetworkCallback() { 177 @Override 178 public void onLinkPropertiesChanged(Network network, LinkProperties lp) { 179 if (network.equals(mNetwork) && !lp.equals(mLinkProperties)) { 180 mLinkProperties = lp; 181 updateIpLayerInfo(); 182 } 183 } 184 185 private boolean hasCapabilityChanged(NetworkCapabilities nc, int cap) { 186 // If this is the first time we get NetworkCapabilities, report that something changed. 187 if (mNetworkCapabilities == null) return true; 188 189 // nc can never be null, see ConnectivityService#callCallbackForRequest. 190 return mNetworkCapabilities.hasCapability(cap) != nc.hasCapability(cap); 191 } 192 193 @Override 194 public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) { 195 // If the network just validated or lost Internet access, refresh network state. 196 // Don't do this on every NetworkCapabilities change because refreshNetworkState 197 // sends IPCs to the system server from the UI thread, which can cause jank. 198 if (network.equals(mNetwork) && !nc.equals(mNetworkCapabilities)) { 199 if (hasCapabilityChanged(nc, NET_CAPABILITY_VALIDATED) || 200 hasCapabilityChanged(nc, NET_CAPABILITY_CAPTIVE_PORTAL)) { 201 refreshNetworkState(); 202 } 203 mNetworkCapabilities = nc; 204 updateIpLayerInfo(); 205 } 206 } 207 208 @Override 209 public void onLost(Network network) { 210 if (network.equals(mNetwork)) { 211 exitActivity(); 212 } 213 } 214 }; 215 216 public static WifiDetailPreferenceController newInstance( 217 AccessPoint accessPoint, 218 ConnectivityManager connectivityManager, 219 Context context, 220 Fragment fragment, 221 Handler handler, 222 Lifecycle lifecycle, 223 WifiManager wifiManager, 224 MetricsFeatureProvider metricsFeatureProvider) { 225 return new WifiDetailPreferenceController( 226 accessPoint, connectivityManager, context, fragment, handler, lifecycle, 227 wifiManager, metricsFeatureProvider, new IconInjector(context)); 228 } 229 230 @VisibleForTesting 231 /* package */ WifiDetailPreferenceController( 232 AccessPoint accessPoint, 233 ConnectivityManager connectivityManager, 234 Context context, 235 Fragment fragment, 236 Handler handler, 237 Lifecycle lifecycle, 238 WifiManager wifiManager, 239 MetricsFeatureProvider metricsFeatureProvider, 240 IconInjector injector) { 241 super(context); 242 243 mAccessPoint = accessPoint; 244 mConnectivityManager = connectivityManager; 245 mFragment = fragment; 246 mHandler = handler; 247 mSignalStr = context.getResources().getStringArray(R.array.wifi_signal); 248 mWifiConfig = accessPoint.getConfig(); 249 mWifiManager = wifiManager; 250 mMetricsFeatureProvider = metricsFeatureProvider; 251 mIconInjector = injector; 252 253 mFilter = new IntentFilter(); 254 mFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); 255 mFilter.addAction(WifiManager.RSSI_CHANGED_ACTION); 256 mFilter.addAction(WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION); 257 258 lifecycle.addObserver(this); 259 } 260 261 @Override 262 public boolean isAvailable() { 263 return true; 264 } 265 266 @Override 267 public String getPreferenceKey() { 268 // Returns null since this controller contains more than one Preference 269 return null; 270 } 271 272 @Override 273 public void displayPreference(PreferenceScreen screen) { 274 super.displayPreference(screen); 275 276 setupEntityHeader(screen); 277 278 mButtonsPref = ((ActionButtonPreference) screen.findPreference(KEY_BUTTONS_PREF)) 279 .setButton1Text(R.string.forget) 280 .setButton1Positive(false) 281 .setButton1OnClickListener(view -> forgetNetwork()) 282 .setButton2Text(R.string.wifi_sign_in_button_text) 283 .setButton2Positive(true) 284 .setButton2OnClickListener(view -> signIntoNetwork()); 285 286 mSignalStrengthPref = 287 (WifiDetailPreference) screen.findPreference(KEY_SIGNAL_STRENGTH_PREF); 288 mLinkSpeedPref = (WifiDetailPreference) screen.findPreference(KEY_LINK_SPEED); 289 mFrequencyPref = (WifiDetailPreference) screen.findPreference(KEY_FREQUENCY_PREF); 290 mSecurityPref = (WifiDetailPreference) screen.findPreference(KEY_SECURITY_PREF); 291 292 mMacAddressPref = (WifiDetailPreference) screen.findPreference(KEY_MAC_ADDRESS_PREF); 293 mIpAddressPref = (WifiDetailPreference) screen.findPreference(KEY_IP_ADDRESS_PREF); 294 mGatewayPref = (WifiDetailPreference) screen.findPreference(KEY_GATEWAY_PREF); 295 mSubnetPref = (WifiDetailPreference) screen.findPreference(KEY_SUBNET_MASK_PREF); 296 mDnsPref = (WifiDetailPreference) screen.findPreference(KEY_DNS_PREF); 297 298 mIpv6Category = (PreferenceCategory) screen.findPreference(KEY_IPV6_CATEGORY); 299 mIpv6AddressPref = screen.findPreference(KEY_IPV6_ADDRESSES_PREF); 300 301 mSecurityPref.setDetailText(mAccessPoint.getSecurityString(false /* concise */)); 302 } 303 304 private void setupEntityHeader(PreferenceScreen screen) { 305 LayoutPreference headerPref = (LayoutPreference) screen.findPreference(KEY_HEADER); 306 mEntityHeaderController = 307 EntityHeaderController.newInstance( 308 mFragment.getActivity(), mFragment, 309 headerPref.findViewById(R.id.entity_header)); 310 311 ImageView iconView = headerPref.findViewById(R.id.entity_header_icon); 312 iconView.setBackground( 313 mContext.getDrawable(R.drawable.ic_settings_widget_background)); 314 iconView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); 315 316 mEntityHeaderController.setLabel(mAccessPoint.getSsidStr()); 317 } 318 319 @Override 320 public void onResume() { 321 // Ensure mNetwork is set before any callbacks above are delivered, since our 322 // NetworkCallback only looks at changes to mNetwork. 323 mNetwork = mWifiManager.getCurrentNetwork(); 324 mLinkProperties = mConnectivityManager.getLinkProperties(mNetwork); 325 mNetworkCapabilities = mConnectivityManager.getNetworkCapabilities(mNetwork); 326 updateInfo(); 327 mContext.registerReceiver(mReceiver, mFilter); 328 mConnectivityManager.registerNetworkCallback(mNetworkRequest, mNetworkCallback, 329 mHandler); 330 } 331 332 @Override 333 public void onPause() { 334 mNetwork = null; 335 mLinkProperties = null; 336 mNetworkCapabilities = null; 337 mNetworkInfo = null; 338 mWifiInfo = null; 339 mContext.unregisterReceiver(mReceiver); 340 mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); 341 } 342 343 private void updateInfo() { 344 // No need to fetch LinkProperties and NetworkCapabilities, they are updated by the 345 // callbacks. mNetwork doesn't change except in onResume. 346 mNetworkInfo = mConnectivityManager.getNetworkInfo(mNetwork); 347 mWifiInfo = mWifiManager.getConnectionInfo(); 348 if (mNetwork == null || mNetworkInfo == null || mWifiInfo == null) { 349 exitActivity(); 350 return; 351 } 352 353 // Update whether the forget button should be displayed. 354 mButtonsPref.setButton1Visible(canForgetNetwork()); 355 356 refreshNetworkState(); 357 358 // Update Connection Header icon and Signal Strength Preference 359 refreshRssiViews(); 360 361 // MAC Address Pref 362 mMacAddressPref.setDetailText(mWifiInfo.getMacAddress()); 363 364 // Link Speed Pref 365 int linkSpeedMbps = mWifiInfo.getLinkSpeed(); 366 mLinkSpeedPref.setVisible(linkSpeedMbps >= 0); 367 mLinkSpeedPref.setDetailText(mContext.getString( 368 R.string.link_speed, mWifiInfo.getLinkSpeed())); 369 370 // Frequency Pref 371 final int frequency = mWifiInfo.getFrequency(); 372 String band = null; 373 if (frequency >= AccessPoint.LOWER_FREQ_24GHZ 374 && frequency < AccessPoint.HIGHER_FREQ_24GHZ) { 375 band = mContext.getResources().getString(R.string.wifi_band_24ghz); 376 } else if (frequency >= AccessPoint.LOWER_FREQ_5GHZ 377 && frequency < AccessPoint.HIGHER_FREQ_5GHZ) { 378 band = mContext.getResources().getString(R.string.wifi_band_5ghz); 379 } else { 380 Log.e(TAG, "Unexpected frequency " + frequency); 381 } 382 mFrequencyPref.setDetailText(band); 383 384 updateIpLayerInfo(); 385 } 386 387 private void exitActivity() { 388 if (DEBUG) { 389 Log.d(TAG, "Exiting the WifiNetworkDetailsPage"); 390 } 391 mFragment.getActivity().finish(); 392 } 393 394 private void refreshNetworkState() { 395 mAccessPoint.update(mWifiConfig, mWifiInfo, mNetworkInfo); 396 mEntityHeaderController.setSummary(mAccessPoint.getSettingsSummary()) 397 .done(mFragment.getActivity(), true /* rebind */); 398 } 399 400 private void refreshRssiViews() { 401 int signalLevel = mAccessPoint.getLevel(); 402 403 if (mRssiSignalLevel == signalLevel) { 404 return; 405 } 406 mRssiSignalLevel = signalLevel; 407 Drawable wifiIcon = mIconInjector.getIcon(mRssiSignalLevel); 408 409 wifiIcon.setTint(Utils.getColorAccent(mContext)); 410 mEntityHeaderController.setIcon(wifiIcon).done(mFragment.getActivity(), true /* rebind */); 411 412 Drawable wifiIconDark = wifiIcon.getConstantState().newDrawable().mutate(); 413 wifiIconDark.setTint(mContext.getResources().getColor( 414 R.color.wifi_details_icon_color, mContext.getTheme())); 415 mSignalStrengthPref.setIcon(wifiIconDark); 416 417 mSignalStrengthPref.setDetailText(mSignalStr[mRssiSignalLevel]); 418 } 419 420 private void updatePreference(WifiDetailPreference pref, String detailText) { 421 if (!TextUtils.isEmpty(detailText)) { 422 pref.setDetailText(detailText); 423 pref.setVisible(true); 424 } else { 425 pref.setVisible(false); 426 } 427 } 428 429 private void updateIpLayerInfo() { 430 mButtonsPref.setButton2Visible(canSignIntoNetwork()); 431 mButtonsPref.setVisible(canSignIntoNetwork() || canForgetNetwork()); 432 433 if (mNetwork == null || mLinkProperties == null) { 434 mIpAddressPref.setVisible(false); 435 mSubnetPref.setVisible(false); 436 mGatewayPref.setVisible(false); 437 mDnsPref.setVisible(false); 438 mIpv6Category.setVisible(false); 439 return; 440 } 441 442 // Find IPv4 and IPv6 addresses. 443 String ipv4Address = null; 444 String subnet = null; 445 StringJoiner ipv6Addresses = new StringJoiner("\n"); 446 447 for (LinkAddress addr : mLinkProperties.getLinkAddresses()) { 448 if (addr.getAddress() instanceof Inet4Address) { 449 ipv4Address = addr.getAddress().getHostAddress(); 450 subnet = ipv4PrefixLengthToSubnetMask(addr.getPrefixLength()); 451 } else if (addr.getAddress() instanceof Inet6Address) { 452 ipv6Addresses.add(addr.getAddress().getHostAddress()); 453 } 454 } 455 456 // Find IPv4 default gateway. 457 String gateway = null; 458 for (RouteInfo routeInfo : mLinkProperties.getRoutes()) { 459 if (routeInfo.isIPv4Default() && routeInfo.hasGateway()) { 460 gateway = routeInfo.getGateway().getHostAddress(); 461 break; 462 } 463 } 464 465 // Find all (IPv4 and IPv6) DNS addresses. 466 String dnsServers = mLinkProperties.getDnsServers().stream() 467 .map(InetAddress::getHostAddress) 468 .collect(Collectors.joining("\n")); 469 470 // Update UI. 471 updatePreference(mIpAddressPref, ipv4Address); 472 updatePreference(mSubnetPref, subnet); 473 updatePreference(mGatewayPref, gateway); 474 updatePreference(mDnsPref, dnsServers); 475 476 if (ipv6Addresses.length() > 0) { 477 mIpv6AddressPref.setSummary( 478 BidiFormatter.getInstance().unicodeWrap(ipv6Addresses.toString())); 479 mIpv6Category.setVisible(true); 480 } else { 481 mIpv6Category.setVisible(false); 482 } 483 } 484 485 private static String ipv4PrefixLengthToSubnetMask(int prefixLength) { 486 try { 487 InetAddress all = InetAddress.getByAddress( 488 new byte[] {(byte) 255, (byte) 255, (byte) 255, (byte) 255}); 489 return NetworkUtils.getNetworkPart(all, prefixLength).getHostAddress(); 490 } catch (UnknownHostException e) { 491 return null; 492 } 493 } 494 495 /** 496 * Returns whether the network represented by this preference can be forgotten. 497 */ 498 private boolean canForgetNetwork() { 499 return (mWifiInfo != null && mWifiInfo.isEphemeral()) || canModifyNetwork(); 500 } 501 502 /** 503 * Returns whether the network represented by this preference can be modified. 504 */ 505 public boolean canModifyNetwork() { 506 return mWifiConfig != null && !WifiUtils.isNetworkLockedDown(mContext, mWifiConfig); 507 } 508 509 /** 510 * Returns whether the user can sign into the network represented by this preference. 511 */ 512 private boolean canSignIntoNetwork() { 513 return WifiUtils.canSignIntoNetwork(mNetworkCapabilities); 514 } 515 516 /** 517 * Forgets the wifi network associated with this preference. 518 */ 519 private void forgetNetwork() { 520 if (mWifiInfo != null && mWifiInfo.isEphemeral()) { 521 mWifiManager.disableEphemeralNetwork(mWifiInfo.getSSID()); 522 } else if (mWifiConfig != null) { 523 if (mWifiConfig.isPasspoint()) { 524 mWifiManager.removePasspointConfiguration(mWifiConfig.FQDN); 525 } else { 526 mWifiManager.forget(mWifiConfig.networkId, null /* action listener */); 527 } 528 } 529 mMetricsFeatureProvider.action( 530 mFragment.getActivity(), MetricsProto.MetricsEvent.ACTION_WIFI_FORGET); 531 mFragment.getActivity().finish(); 532 } 533 534 /** 535 * Sign in to the captive portal found on this wifi network associated with this preference. 536 */ 537 private void signIntoNetwork() { 538 mMetricsFeatureProvider.action( 539 mFragment.getActivity(), MetricsProto.MetricsEvent.ACTION_WIFI_SIGNIN); 540 mConnectivityManager.startCaptivePortalApp(mNetwork); 541 } 542 543 @Override 544 public void onForget(WifiDialog dialog) { 545 // can't forget network from a 'modify' dialog 546 } 547 548 @Override 549 public void onSubmit(WifiDialog dialog) { 550 if (dialog.getController() != null) { 551 mWifiManager.save(dialog.getController().getConfig(), new WifiManager.ActionListener() { 552 @Override 553 public void onSuccess() { 554 } 555 556 @Override 557 public void onFailure(int reason) { 558 Activity activity = mFragment.getActivity(); 559 if (activity != null) { 560 Toast.makeText(activity, 561 R.string.wifi_failed_save_message, 562 Toast.LENGTH_SHORT).show(); 563 } 564 } 565 }); 566 } 567 } 568 569 /** 570 * Wrapper for testing compatibility. 571 */ 572 @VisibleForTesting 573 static class IconInjector { 574 private final Context mContext; 575 576 public IconInjector(Context context) { 577 mContext = context; 578 } 579 580 public Drawable getIcon(int level) { 581 return mContext.getDrawable(Utils.getWifiIconResource(level)).mutate(); 582 } 583 } 584 } 585