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 newInstance( AccessPoint accessPoint, ConnectivityManager connectivityManager, Context context, Fragment fragment, Handler handler, Lifecycle lifecycle, WifiManager wifiManager, MetricsFeatureProvider metricsFeatureProvider)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 WifiDetailPreferenceController( AccessPoint accessPoint, ConnectivityManager connectivityManager, Context context, Fragment fragment, Handler handler, Lifecycle lifecycle, WifiManager wifiManager, MetricsFeatureProvider metricsFeatureProvider, IconInjector injector)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 isAvailable()262 public boolean isAvailable() { 263 return true; 264 } 265 266 @Override getPreferenceKey()267 public String getPreferenceKey() { 268 // Returns null since this controller contains more than one Preference 269 return null; 270 } 271 272 @Override displayPreference(PreferenceScreen screen)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 setupEntityHeader(PreferenceScreen screen)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 onResume()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 onPause()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 updateInfo()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 exitActivity()387 private void exitActivity() { 388 if (DEBUG) { 389 Log.d(TAG, "Exiting the WifiNetworkDetailsPage"); 390 } 391 mFragment.getActivity().finish(); 392 } 393 refreshNetworkState()394 private void refreshNetworkState() { 395 mAccessPoint.update(mWifiConfig, mWifiInfo, mNetworkInfo); 396 mEntityHeaderController.setSummary(mAccessPoint.getSettingsSummary()) 397 .done(mFragment.getActivity(), true /* rebind */); 398 } 399 refreshRssiViews()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 updatePreference(WifiDetailPreference pref, String detailText)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 updateIpLayerInfo()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 ipv4PrefixLengthToSubnetMask(int prefixLength)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 */ canForgetNetwork()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 */ canModifyNetwork()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 */ canSignIntoNetwork()512 private boolean canSignIntoNetwork() { 513 return WifiUtils.canSignIntoNetwork(mNetworkCapabilities); 514 } 515 516 /** 517 * Forgets the wifi network associated with this preference. 518 */ forgetNetwork()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 */ signIntoNetwork()537 private void signIntoNetwork() { 538 mMetricsFeatureProvider.action( 539 mFragment.getActivity(), MetricsProto.MetricsEvent.ACTION_WIFI_SIGNIN); 540 mConnectivityManager.startCaptivePortalApp(mNetwork); 541 } 542 543 @Override onForget(WifiDialog dialog)544 public void onForget(WifiDialog dialog) { 545 // can't forget network from a 'modify' dialog 546 } 547 548 @Override onSubmit(WifiDialog dialog)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 IconInjector(Context context)576 public IconInjector(Context context) { 577 mContext = context; 578 } 579 getIcon(int level)580 public Drawable getIcon(int level) { 581 return mContext.getDrawable(Utils.getWifiIconResource(level)).mutate(); 582 } 583 } 584 } 585