• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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