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 -> {
396                     final Intent infoIntent = new Intent(Intent.ACTION_VIEW);
397                     infoIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
398                     infoIntent.setData(venueInfoUrl);
399                     mContext.startActivity(infoIntent);
400                 });
401         // Only show the venue website when the network is connected.
402         return mWifiEntry.getConnectedState() == WifiEntry.CONNECTED_STATE_CONNECTED;
403     }
404 
getCaptivePortalVenueInfoUrl()405     private Uri getCaptivePortalVenueInfoUrl() {
406         final LinkProperties lp = mLinkProperties;
407         if (lp == null) {
408             return null;
409         }
410         final CaptivePortalData data = lp.getCaptivePortalData();
411         if (data == null) {
412             return null;
413         }
414         return data.getVenueInfoUrl();
415     }
416 
setupEntityHeader(PreferenceScreen screen)417     private void setupEntityHeader(PreferenceScreen screen) {
418         LayoutPreference headerPref = screen.findPreference(KEY_HEADER);
419 
420         mEntityHeaderController =
421                 EntityHeaderController.newInstance(
422                         mFragment.getActivity(), mFragment,
423                         headerPref.findViewById(R.id.entity_header));
424 
425         ImageView iconView = headerPref.findViewById(R.id.entity_header_icon);
426 
427         iconView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
428     }
429 
getExpiryTimeSummary()430     private String getExpiryTimeSummary() {
431         if (mLinkProperties == null || mLinkProperties.getCaptivePortalData() == null) {
432             return null;
433         }
434 
435         final long expiryTimeMillis = mLinkProperties.getCaptivePortalData().getExpiryTimeMillis();
436         if (expiryTimeMillis <= 0) {
437             return null;
438         }
439         final ZonedDateTime now = mClock.now();
440         final ZonedDateTime expiryTime = ZonedDateTime.ofInstant(
441                 Instant.ofEpochMilli(expiryTimeMillis),
442                 now.getZone());
443 
444         if (now.isAfter(expiryTime)) {
445             return null;
446         }
447 
448         if (now.plusDays(2).isAfter(expiryTime)) {
449             // Expiration within 2 days: show a duration
450             return mContext.getString(R.string.wifi_time_remaining, StringUtil.formatElapsedTime(
451                     mContext,
452                     Duration.between(now, expiryTime).getSeconds() * 1000,
453                     false /* withSeconds */, false /* collapseTimeUnit */));
454         }
455 
456         // For more than 2 days, show the expiry date
457         return mContext.getString(R.string.wifi_expiry_time,
458                 DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).format(expiryTime));
459     }
460 
refreshEntityHeader()461     private void refreshEntityHeader() {
462         mEntityHeaderController
463                 .setLabel(mWifiEntry.getTitle())
464                 .setSummary(mWifiEntry.getSummary())
465                 .setSecondSummary(getExpiryTimeSummary())
466                 .done(true /* rebind */);
467     }
468 
469     @VisibleForTesting
updateNetworkInfo()470     void updateNetworkInfo() {
471         if (mWifiEntry.getConnectedState() == WifiEntry.CONNECTED_STATE_CONNECTED) {
472             mNetwork = mWifiManager.getCurrentNetwork();
473             mLinkProperties = mConnectivityManager.getLinkProperties(mNetwork);
474             mNetworkCapabilities = mConnectivityManager.getNetworkCapabilities(mNetwork);
475         } else {
476             mNetwork = null;
477             mLinkProperties = null;
478             mNetworkCapabilities = null;
479         }
480     }
481 
482     @Override
onResume()483     public void onResume() {
484         // Disable the animation of the EntityHeaderController
485         final RecyclerView recyclerView = mFragment.getListView();
486         if (recyclerView != null) {
487             recyclerView.setItemAnimator(null);
488         }
489 
490         // Ensure mNetwork is set before any callbacks above are delivered, since our
491         // NetworkCallback only looks at changes to mNetwork.
492         updateNetworkInfo();
493         refreshPage();
494         mConnectivityManager.registerNetworkCallback(mNetworkRequest, mNetworkCallback,
495                 mHandler);
496     }
497 
498     @Override
onPause()499     public void onPause() {
500         mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
501     }
502 
refreshPage()503     private void refreshPage() {
504         Log.d(TAG, "Update UI!");
505 
506         // refresh header icon
507         refreshEntryHeaderIcon();
508         // refresh header
509         refreshEntityHeader();
510 
511         // refresh Buttons
512         refreshButtons();
513 
514         // Update Connection Header icon and Signal Strength Preference
515         refreshRssiViews();
516         // Frequency Pref
517         refreshFrequency();
518         // Security Pref
519         refreshSecurity();
520         // Transmit Link Speed Pref
521         refreshTxSpeed();
522         // Receive Link Speed Pref
523         refreshRxSpeed();
524         // IP related information
525         refreshIpLayerInfo();
526         // SSID Pref
527         refreshSsid();
528         // EAP SIM subscription
529         refreshEapSimSubscription();
530         // MAC Address Pref
531         refreshMacAddress();
532         // Wifi Type
533         refreshWifiType();
534     }
535 
536     @VisibleForTesting
refreshEntryHeaderIcon()537     void refreshEntryHeaderIcon() {
538         if (mEntityHeaderController == null) {
539             return;
540         }
541         Drawable drawable = getWifiDrawable(mWifiEntry);
542         mEntityHeaderController
543                 .setIcon(redrawIconForHeader(drawable))
544                 .done(true /* rebind */);
545     }
546 
547     /**
548      * Returns a Wi-Fi icon {@link Drawable}.
549      *
550      * @param wifiEntry {@link WifiEntry}
551      */
552     @VisibleForTesting
getWifiDrawable(WifiEntry wifiEntry)553     Drawable getWifiDrawable(WifiEntry wifiEntry) {
554         if (wifiEntry instanceof HotspotNetworkEntry) {
555             int deviceType = ((HotspotNetworkEntry) wifiEntry).getDeviceType();
556             return mContext.getDrawable(getHotspotIconResource(deviceType));
557         }
558         if (mWifiEntry.getLevel() == WifiEntry.WIFI_LEVEL_UNREACHABLE) {
559             return mContext.getDrawable(R.drawable.empty_icon);
560         }
561         return mIconInjector.getIcon(wifiEntry.shouldShowXLevelIcon(), wifiEntry.getLevel());
562     }
563 
refreshRssiViews()564     private void refreshRssiViews() {
565         int signalLevel = mWifiEntry.getLevel();
566 
567         // Disappears signal view if not in range. e.g. for saved networks.
568         if (signalLevel == WifiEntry.WIFI_LEVEL_UNREACHABLE) {
569             mSignalStrengthPref.setVisible(false);
570             mRssiSignalLevel = -1;
571             return;
572         }
573 
574         boolean showX = mWifiEntry.shouldShowXLevelIcon();
575         if (mRssiSignalLevel == signalLevel && mShowX == showX) {
576             return;
577         }
578         mRssiSignalLevel = signalLevel;
579         mShowX = showX;
580         Drawable wifiIcon = mIconInjector.getIcon(mShowX, mRssiSignalLevel);
581         Drawable wifiIconDark = wifiIcon.getConstantState().newDrawable().mutate();
582         wifiIconDark.setTintList(Utils.getColorAttr(mContext, android.R.attr.colorControlNormal));
583         mSignalStrengthPref.setIcon(wifiIconDark);
584 
585         mSignalStrengthPref.setSummary(mSignalStr[mRssiSignalLevel]);
586         mSignalStrengthPref.setVisible(true);
587     }
588 
redrawIconForHeader(Drawable original)589     private Drawable redrawIconForHeader(Drawable original) {
590         final int iconSize = mContext.getResources().getDimensionPixelSize(
591                 R.dimen.wifi_detail_page_header_image_size);
592         final int actualWidth = original.getMinimumWidth();
593         final int actualHeight = original.getMinimumHeight();
594 
595         if ((actualWidth == iconSize && actualHeight == iconSize)
596                 || !VectorDrawable.class.isInstance(original)) {
597             return original;
598         }
599 
600         // clear tint list to make sure can set 87% black after enlarge
601         original.setTintList(null);
602 
603         // enlarge icon size
604         final Bitmap bitmap = Utils.createBitmap(original,
605                 iconSize /*width*/,
606                 iconSize /*height*/);
607         Drawable newIcon = new BitmapDrawable(null /*resource*/, bitmap);
608 
609         // config color for 87% black after enlarge
610         newIcon.setTintList(Utils.getColorAttr(mContext, android.R.attr.textColorPrimary));
611 
612         return newIcon;
613     }
614 
refreshFrequency()615     private void refreshFrequency() {
616         final String bandString = mWifiEntry.getBandString();
617         if (TextUtils.isEmpty(bandString)) {
618             mFrequencyPref.setVisible(false);
619             return;
620         }
621         mFrequencyPref.setSummary(bandString);
622         mFrequencyPref.setVisible(true);
623     }
624 
refreshSecurity()625     private void refreshSecurity() {
626         mSecurityPref.setSummary(mWifiEntry.getSecurityString(false /* concise */));
627     }
628 
refreshTxSpeed()629     private void refreshTxSpeed() {
630         String summary = mWifiEntry.getTxSpeedString();
631         if (TextUtils.isEmpty(summary)) {
632             mTxLinkSpeedPref.setVisible(false);
633             return;
634         }
635         mTxLinkSpeedPref.setVisible(true);
636         mTxLinkSpeedPref.setSummary(summary);
637     }
638 
refreshRxSpeed()639     private void refreshRxSpeed() {
640         String summary = mWifiEntry.getRxSpeedString();
641         if (TextUtils.isEmpty(summary)) {
642             mRxLinkSpeedPref.setVisible(false);
643             return;
644         }
645         mRxLinkSpeedPref.setVisible(true);
646         mRxLinkSpeedPref.setSummary(summary);
647     }
648 
refreshSsid()649     private void refreshSsid() {
650         if (mWifiEntry.shouldShowSsid() && mWifiEntry.getSsid() != null) {
651             mSsidPref.setVisible(true);
652             mSsidPref.setSummary(mWifiEntry.getSsid());
653         } else {
654             mSsidPref.setVisible(false);
655         }
656     }
657 
refreshEapSimSubscription()658     private void refreshEapSimSubscription() {
659         mEapSimSubscriptionPref.setVisible(false);
660 
661         if (mWifiEntry.getSecurity() != WifiEntry.SECURITY_EAP) {
662             return;
663         }
664         final WifiConfiguration config = mWifiEntry.getWifiConfiguration();
665         if (config == null || config.enterpriseConfig == null) {
666             return;
667         }
668         if (!config.enterpriseConfig.isAuthenticationSimBased()) {
669             return;
670         }
671 
672         mEapSimSubscriptionPref.setVisible(true);
673 
674         // Checks if the SIM subscription is active.
675         final List<SubscriptionInfo> activeSubscriptionInfos = mContext
676                 .getSystemService(SubscriptionManager.class).getActiveSubscriptionInfoList();
677         if (activeSubscriptionInfos != null) {
678             SubscriptionInfo info = fineSubscriptionInfo(config.carrierId, activeSubscriptionInfos,
679                     SubscriptionManager.getDefaultDataSubscriptionId());
680             if (info != null) {
681                 mEapSimSubscriptionPref.setSummary(
682                         SubscriptionUtil.getUniqueSubscriptionDisplayName(info, mContext));
683                 return;
684             }
685         }
686 
687         if (config.carrierId == UNKNOWN_CARRIER_ID) {
688             mEapSimSubscriptionPref.setSummary(R.string.wifi_no_related_sim_card);
689             return;
690         }
691 
692         // The Wi-Fi network has specified carrier id, query carrier name from CarrierIdProvider.
693         if (mCarrierIdAsyncQueryHandler == null) {
694             mCarrierIdAsyncQueryHandler = new CarrierIdAsyncQueryHandler(mContext);
695         }
696         mCarrierIdAsyncQueryHandler.cancelOperation(TOKEN_QUERY_CARRIER_ID_AND_UPDATE_SIM_SUMMARY);
697         mCarrierIdAsyncQueryHandler.startQuery(TOKEN_QUERY_CARRIER_ID_AND_UPDATE_SIM_SUMMARY,
698                 null /* cookie */,
699                 CarrierId.All.CONTENT_URI,
700                 new String[]{CarrierId.CARRIER_NAME},
701                 CarrierId.CARRIER_ID + "=?",
702                 new String[] {Integer.toString(config.carrierId)},
703                 null /* orderBy */);
704     }
705 
706     @VisibleForTesting
fineSubscriptionInfo(int carrierId, List<SubscriptionInfo> activeSubscriptionInfos, int defaultDataSubscriptionId)707     SubscriptionInfo fineSubscriptionInfo(int carrierId,
708             List<SubscriptionInfo> activeSubscriptionInfos, int defaultDataSubscriptionId) {
709         SubscriptionInfo firstMatchedInfo = null;
710         for (SubscriptionInfo info : activeSubscriptionInfos) {
711             // When it's UNKNOWN_CARRIER_ID or matched with configured CarrierId,
712             // devices connects it with the SIM subscription of defaultDataSubscriptionId.
713             if (defaultDataSubscriptionId == info.getSubscriptionId()
714                     && (carrierId == info.getCarrierId() || carrierId == UNKNOWN_CARRIER_ID)) {
715                 return info;
716             }
717 
718             if (firstMatchedInfo == null && carrierId == info.getCarrierId()) {
719                 firstMatchedInfo = info;
720             }
721         }
722         return firstMatchedInfo;
723     }
724 
refreshMacAddress()725     private void refreshMacAddress() {
726         final String macAddress = mWifiEntry.getMacAddress();
727         if (TextUtils.isEmpty(macAddress)) {
728             mMacAddressPref.setVisible(false);
729             return;
730         }
731 
732         mMacAddressPref.setVisible(true);
733         mMacAddressPref.setTitle(getMacAddressTitle());
734 
735         if (macAddress.equals(WifiInfo.DEFAULT_MAC_ADDRESS)) {
736             mMacAddressPref.setSummary(R.string.device_info_not_available);
737         } else {
738             mMacAddressPref.setSummary(macAddress);
739         }
740     }
741 
refreshWifiType()742     private void refreshWifiType() {
743         final String typeString = mWifiEntry.getStandardString();
744         if (!TextUtils.isEmpty(typeString)) {
745             mTypePref.setSummary(typeString);
746             mTypePref.setVisible(true);
747         } else {
748             mTypePref.setVisible(false);
749         }
750     }
751 
getMacAddressTitle()752     private int getMacAddressTitle() {
753         if (mWifiEntry.getPrivacy() == WifiEntry.PRIVACY_RANDOMIZED_MAC) {
754             return mWifiEntry.getConnectedState() == WifiEntry.CONNECTED_STATE_CONNECTED
755                     ? R.string.wifi_advanced_randomized_mac_address_title
756                     : R.string.wifi_advanced_randomized_mac_address_disconnected_title;
757         }
758         return R.string.wifi_advanced_device_mac_address_title;
759     }
760 
updatePreference(Preference pref, String detailText)761     private void updatePreference(Preference pref, String detailText) {
762         if (!TextUtils.isEmpty(detailText)) {
763             pref.setSummary(detailText);
764             pref.setVisible(true);
765         } else {
766             pref.setVisible(false);
767         }
768     }
769 
refreshButtons()770     private void refreshButtons() {
771         final boolean canForgetNetwork = canForgetNetwork();
772         final boolean showCaptivePortalButton = updateCaptivePortalButton();
773         final boolean canConnectDisconnectNetwork = mWifiEntry.canConnect()
774                 || mWifiEntry.canDisconnect();
775         final boolean canShareNetwork = canShareNetwork();
776 
777         mButtonsPref.setButton1Visible(canForgetNetwork);
778         mButtonsPref.setButton2Visible(showCaptivePortalButton);
779         // Keep the connect/disconnected button visible if we can connect/disconnect, or if we are
780         // in the middle of connecting (greyed out).
781         mButtonsPref.setButton3Visible(canConnectDisconnectNetwork
782                 || mWifiEntry.getConnectedState() == WifiEntry.CONNECTED_STATE_CONNECTING);
783         mButtonsPref.setButton3Enabled(canConnectDisconnectNetwork);
784         mButtonsPref.setButton3Text(getConnectDisconnectButtonTextResource());
785         mButtonsPref.setButton3Icon(getConnectDisconnectButtonIconResource());
786         mButtonsPref.setButton4Visible(canShareNetwork);
787         mButtonsPref.setVisible(canForgetNetwork
788                 || showCaptivePortalButton
789                 || canConnectDisconnectNetwork
790                 || canShareNetwork);
791     }
792 
getConnectDisconnectButtonTextResource()793     private int getConnectDisconnectButtonTextResource() {
794         switch (mWifiEntry.getConnectedState()) {
795             case WifiEntry.CONNECTED_STATE_DISCONNECTED:
796                 return R.string.wifi_connect;
797             case WifiEntry.CONNECTED_STATE_CONNECTED:
798                 return R.string.wifi_disconnect_button_text;
799             case WifiEntry.CONNECTED_STATE_CONNECTING:
800                 return R.string.wifi_connecting;
801             default:
802                 throw new IllegalStateException("Invalid WifiEntry connected state");
803         }
804     }
805 
getConnectDisconnectButtonIconResource()806     private int getConnectDisconnectButtonIconResource() {
807         switch (mWifiEntry.getConnectedState()) {
808             case WifiEntry.CONNECTED_STATE_DISCONNECTED:
809             case WifiEntry.CONNECTED_STATE_CONNECTING:
810                 return R.drawable.ic_settings_wireless;
811             case WifiEntry.CONNECTED_STATE_CONNECTED:
812                 return R.drawable.ic_settings_close;
813             default:
814                 throw new IllegalStateException("Invalid WifiEntry connected state");
815         }
816     }
817 
refreshIpLayerInfo()818     private void refreshIpLayerInfo() {
819         // Hide IP layer info if not a connected network.
820         if (mWifiEntry.getConnectedState() != WifiEntry.CONNECTED_STATE_CONNECTED
821                 || mNetwork == null || mLinkProperties == null) {
822             mIpAddressPref.setVisible(false);
823             mSubnetPref.setVisible(false);
824             mGatewayPref.setVisible(false);
825             mDnsPref.setVisible(false);
826             mIpv6AddressPref.setVisible(false);
827             return;
828         }
829 
830         // Find IPv4 and IPv6 addresses.
831         String ipv4Address = null;
832         String subnet = null;
833         StringJoiner ipv6Addresses = new StringJoiner("\n");
834 
835         for (LinkAddress addr : mLinkProperties.getLinkAddresses()) {
836             if (addr.getAddress() instanceof Inet4Address) {
837                 ipv4Address = addr.getAddress().getHostAddress();
838                 subnet = ipv4PrefixLengthToSubnetMask(addr.getPrefixLength());
839             } else if (addr.getAddress() instanceof Inet6Address) {
840                 ipv6Addresses.add(addr.getAddress().getHostAddress());
841             }
842         }
843 
844         // Find IPv4 default gateway.
845         String gateway = null;
846         for (RouteInfo routeInfo : mLinkProperties.getRoutes()) {
847             if (routeInfo.hasGateway() && routeInfo.isDefaultRoute()
848                     && routeInfo.getDestination().getAddress() instanceof Inet4Address) {
849                 gateway = routeInfo.getGateway().getHostAddress();
850                 break;
851             }
852         }
853 
854         // Find all (IPv4 and IPv6) DNS addresses.
855         String dnsServers = mLinkProperties.getDnsServers().stream()
856                 .map(InetAddress::getHostAddress)
857                 .collect(Collectors.joining("\n"));
858 
859         // Update UI.
860         updatePreference(mIpAddressPref, ipv4Address);
861         updatePreference(mSubnetPref, subnet);
862         updatePreference(mGatewayPref, gateway);
863         updatePreference(mDnsPref, dnsServers);
864 
865         if (ipv6Addresses.length() > 0) {
866             mIpv6AddressPref.setVisible(true);
867             mIpv6AddressPref.setSummary(
868                     BidiFormatter.getInstance().unicodeWrap(ipv6Addresses.toString()));
869         } else {
870             mIpv6AddressPref.setVisible(false);
871         }
872     }
873 
ipv4PrefixLengthToSubnetMask(int prefixLength)874     private static String ipv4PrefixLengthToSubnetMask(int prefixLength) {
875         try {
876             return Inet4AddressUtils.getPrefixMaskAsInet4Address(prefixLength).getHostAddress();
877         } catch (IllegalArgumentException e) {
878             return null;
879         }
880     }
881 
882     /**
883      * Returns whether the network represented by this preference can be modified.
884      */
canModifyNetwork()885     public boolean canModifyNetwork() {
886         return mWifiEntry.isSaved()
887                 && !WifiUtils.isNetworkLockedDown(mContext, mWifiEntry.getWifiConfiguration());
888     }
889 
890     /**
891      * Returns whether the network represented by this preference can be forgotten.
892      */
canForgetNetwork()893     public boolean canForgetNetwork() {
894         return mWifiEntry.canForget()
895                 && !WifiUtils.isNetworkLockedDown(mContext, mWifiEntry.getWifiConfiguration());
896     }
897 
898     /**
899      * Returns whether the user can sign into the network represented by this preference.
900      */
canSignIntoNetwork()901     private boolean canSignIntoNetwork() {
902         return mWifiEntry.canSignIn();
903     }
904 
905     /**
906      * Returns whether the user can share the network represented by this preference with QR code.
907      */
canShareNetwork()908     private boolean canShareNetwork() {
909         return mWifiEntry.canShare();
910     }
911 
912     /**
913      * Forgets the wifi network associated with this preference.
914      */
forgetNetwork()915     private void forgetNetwork() {
916         if (mWifiEntry.isSubscription()) {
917             // Post a dialog to confirm if user really want to forget the passpoint network.
918             showConfirmForgetDialog();
919             return;
920         } else {
921             mWifiEntry.forget(this);
922         }
923 
924         final Activity activity = mFragment.getActivity();
925         if (activity != null) {
926             mMetricsFeatureProvider.action(activity, SettingsEnums.ACTION_WIFI_FORGET);
927             activity.finish();
928         }
929     }
930 
931     @VisibleForTesting
showConfirmForgetDialog()932     protected void showConfirmForgetDialog() {
933         final AlertDialog dialog = new AlertDialog.Builder(mContext)
934                 .setPositiveButton(R.string.forget, ((dialog1, which) -> {
935                     try {
936                         mWifiEntry.forget(this);
937                     } catch (RuntimeException e) {
938                         Log.e(TAG, "Failed to remove Passpoint configuration: " + e);
939                     }
940                     mMetricsFeatureProvider.action(
941                             mFragment.getActivity(), SettingsEnums.ACTION_WIFI_FORGET);
942                     mFragment.getActivity().finish();
943                 }))
944                 .setNegativeButton(R.string.cancel, null /* listener */)
945                 .setTitle(R.string.wifi_forget_dialog_title)
946                 .setMessage(R.string.forget_passpoint_dialog_message)
947                 .create();
948         dialog.show();
949     }
950 
951     /**
952      * Show QR code to share the network represented by this preference.
953      */
launchWifiDppConfiguratorActivity()954     private void launchWifiDppConfiguratorActivity() {
955         final Intent intent = WifiDppUtils.getConfiguratorQrCodeGeneratorIntentOrNull(mContext,
956                 mWifiManager, mWifiEntry);
957 
958         if (intent == null) {
959             Log.e(TAG, "Launch Wi-Fi DPP QR code generator with a wrong Wi-Fi network!");
960         } else {
961             mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN,
962                     SettingsEnums.ACTION_SETTINGS_SHARE_WIFI_QR_CODE,
963                     SettingsEnums.SETTINGS_WIFI_DPP_CONFIGURATOR,
964                     /* key */ null,
965                     /* value */ Integer.MIN_VALUE);
966 
967             mContext.startActivity(intent);
968         }
969     }
970 
971     /**
972      * Share the wifi network with QR code.
973      */
shareNetwork()974     private void shareNetwork() {
975         WifiDppUtils.showLockScreen(mContext, () -> launchWifiDppConfiguratorActivity());
976     }
977 
978     /**
979      * Sign in to the captive portal found on this wifi network associated with this preference.
980      */
signIntoNetwork()981     private void signIntoNetwork() {
982         mMetricsFeatureProvider.action(
983                 mFragment.getActivity(), SettingsEnums.ACTION_WIFI_SIGNIN);
984         mWifiEntry.signIn(this);
985     }
986 
987     @Override
onSubmit(WifiDialog2 dialog)988     public void onSubmit(WifiDialog2 dialog) {
989         if (dialog.getController() != null) {
990             mWifiManager.save(dialog.getController().getConfig(), new WifiManager.ActionListener() {
991                 @Override
992                 public void onSuccess() {
993                 }
994 
995                 @Override
996                 public void onFailure(int reason) {
997                     Activity activity = mFragment.getActivity();
998                     if (activity != null) {
999                         Toast.makeText(activity,
1000                                 R.string.wifi_failed_save_message,
1001                                 Toast.LENGTH_SHORT).show();
1002                     }
1003                 }
1004             });
1005         }
1006     }
1007 
1008     /**
1009      * Wrapper for testing compatibility.
1010      */
1011     @VisibleForTesting
1012     static class IconInjector {
1013         private final Context mContext;
1014 
IconInjector(Context context)1015         IconInjector(Context context) {
1016             mContext = context;
1017         }
1018 
getIcon(boolean showX, int level)1019         public Drawable getIcon(boolean showX, int level) {
1020             return mContext.getDrawable(WifiUtils.getInternetIconResource(level, showX)).mutate();
1021         }
1022     }
1023 
1024     @VisibleForTesting
1025     static class Clock {
now()1026         public ZonedDateTime now() {
1027             return ZonedDateTime.now();
1028         }
1029     }
1030 
1031     @VisibleForTesting
connectDisconnectNetwork()1032     void connectDisconnectNetwork() {
1033         if (mWifiEntry.getConnectedState() == WifiEntry.CONNECTED_STATE_DISCONNECTED) {
1034             mWifiEntry.connect(this);
1035         } else {
1036             mWifiEntry.disconnect(this);
1037         }
1038     }
1039 
1040     /**
1041      * Indicates the state of the WifiEntry has changed and clients may retrieve updates through
1042      * the WifiEntry getter methods.
1043      */
1044     @Override
onUpdated()1045     public void onUpdated() {
1046         updateNetworkInfo();
1047         refreshPage();
1048 
1049         // Refresh the Preferences in fragment.
1050         ((WifiNetworkDetailsFragment) mFragment).refreshPreferences();
1051     }
1052 
1053     /**
1054      * Result of the connect request indicated by the CONNECT_STATUS constants.
1055      */
1056     @Override
onConnectResult(@onnectStatus int status)1057     public void onConnectResult(@ConnectStatus int status) {
1058         if (status == ConnectCallback.CONNECT_STATUS_SUCCESS) {
1059             Toast.makeText(mContext,
1060                     mContext.getString(R.string.wifi_connected_to_message, mWifiEntry.getTitle()),
1061                     Toast.LENGTH_SHORT).show();
1062         } else if (mWifiEntry.getLevel() == WifiEntry.WIFI_LEVEL_UNREACHABLE) {
1063             Toast.makeText(mContext,
1064                     R.string.wifi_not_in_range_message,
1065                     Toast.LENGTH_SHORT).show();
1066         } else {
1067             Toast.makeText(mContext,
1068                     R.string.wifi_failed_connect_message,
1069                     Toast.LENGTH_SHORT).show();
1070         }
1071     }
1072 
1073     /**
1074      * Result of the disconnect request indicated by the DISCONNECT_STATUS constants.
1075      */
1076     @Override
onDisconnectResult(@isconnectStatus int status)1077     public void onDisconnectResult(@DisconnectStatus int status) {
1078         if (status == DisconnectCallback.DISCONNECT_STATUS_SUCCESS) {
1079             final Activity activity = mFragment.getActivity();
1080             if (activity != null) {
1081                 Toast.makeText(activity,
1082                         activity.getString(R.string.wifi_disconnected_from, mWifiEntry.getTitle()),
1083                         Toast.LENGTH_SHORT).show();
1084             }
1085         } else {
1086             Log.e(TAG, "Disconnect Wi-Fi network failed");
1087         }
1088     }
1089 
1090     /**
1091      * Result of the forget request indicated by the FORGET_STATUS constants.
1092      */
1093     @Override
onForgetResult(@orgetStatus int status)1094     public void onForgetResult(@ForgetStatus int status) {
1095         if (status != ForgetCallback.FORGET_STATUS_SUCCESS) {
1096             Log.e(TAG, "Forget Wi-Fi network failed");
1097         }
1098 
1099         final Activity activity = mFragment.getActivity();
1100         if (activity != null) {
1101             mMetricsFeatureProvider.action(activity, SettingsEnums.ACTION_WIFI_FORGET);
1102             activity.finish();
1103         }
1104     }
1105 
1106     /**
1107      * Result of the sign-in request indicated by the SIGNIN_STATUS constants.
1108      */
1109     @Override
onSignInResult(@ignInStatus int status)1110     public void onSignInResult(@SignInStatus int status) {
1111         refreshPage();
1112     }
1113 
1114     /** Sets signal strength title */
setSignalStrengthTitle(int titleResId)1115     public void setSignalStrengthTitle(int titleResId) {
1116         if (mSignalStrengthPref != null) {
1117             mSignalStrengthPref.setTitle(titleResId);
1118         }
1119     }
1120 }
1121