/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.wifi.details2; import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL; import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY; import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID; import static com.android.settingslib.wifi.WifiUtils.getHotspotIconResource; import android.app.Activity; import android.app.AlertDialog; import android.app.settings.SettingsEnums; import android.content.AsyncQueryHandler; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.VectorDrawable; import android.net.CaptivePortalData; import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; import android.net.LinkAddress; import android.net.LinkProperties; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.net.RouteInfo; import android.net.Uri; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.os.Handler; import android.provider.Telephony.CarrierId; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.text.TextUtils; import android.util.Log; import android.widget.ImageView; import android.widget.Toast; import androidx.annotation.VisibleForTesting; import androidx.core.text.BidiFormatter; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceScreen; import androidx.recyclerview.widget.RecyclerView; import com.android.net.module.util.Inet4AddressUtils; import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.network.SubscriptionUtil; import com.android.settings.widget.EntityHeaderController; import com.android.settings.wifi.WifiDialog2; import com.android.settings.wifi.WifiDialog2.WifiDialog2Listener; import com.android.settings.wifi.WifiUtils; import com.android.settings.wifi.details.WifiNetworkDetailsFragment; import com.android.settings.wifi.dpp.WifiDppUtils; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnResume; import com.android.settingslib.utils.StringUtil; import com.android.settingslib.widget.ActionButtonsPreference; import com.android.settingslib.widget.LayoutPreference; import com.android.wifitrackerlib.HotspotNetworkEntry; import com.android.wifitrackerlib.WifiEntry; import com.android.wifitrackerlib.WifiEntry.ConnectCallback; import com.android.wifitrackerlib.WifiEntry.DisconnectCallback; import com.android.wifitrackerlib.WifiEntry.ForgetCallback; import com.android.wifitrackerlib.WifiEntry.SignInCallback; import com.android.wifitrackerlib.WifiEntry.WifiEntryCallback; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.time.Duration; import java.time.Instant; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; import java.util.List; import java.util.StringJoiner; import java.util.stream.Collectors; // TODO(b/151133650): Replace AbstractPreferenceController with BasePreferenceController. /** * Controller for logic pertaining to displaying Wifi information for the * {@link WifiNetworkDetailsFragment}. */ public class WifiDetailPreferenceController2 extends AbstractPreferenceController implements PreferenceControllerMixin, WifiDialog2Listener, LifecycleObserver, OnPause, OnResume, WifiEntryCallback, ConnectCallback, DisconnectCallback, ForgetCallback, SignInCallback { private static final String TAG = "WifiDetailsPrefCtrl2"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @VisibleForTesting static final String KEY_HEADER = "connection_header"; @VisibleForTesting static final String KEY_DATA_USAGE_HEADER = "status_header"; @VisibleForTesting static final String KEY_BUTTONS_PREF = "buttons"; @VisibleForTesting static final String KEY_SIGNAL_STRENGTH_PREF = "signal_strength"; @VisibleForTesting static final String KEY_TX_LINK_SPEED = "tx_link_speed"; @VisibleForTesting static final String KEY_RX_LINK_SPEED = "rx_link_speed"; @VisibleForTesting static final String KEY_FREQUENCY_PREF = "frequency"; @VisibleForTesting static final String KEY_SECURITY_PREF = "security"; @VisibleForTesting static final String KEY_SSID_PREF = "ssid"; @VisibleForTesting static final String KEY_EAP_SIM_SUBSCRIPTION_PREF = "eap_sim_subscription"; @VisibleForTesting static final String KEY_MAC_ADDRESS_PREF = "mac_address"; @VisibleForTesting static final String KEY_IP_ADDRESS_PREF = "ip_address"; @VisibleForTesting static final String KEY_GATEWAY_PREF = "gateway"; @VisibleForTesting static final String KEY_SUBNET_MASK_PREF = "subnet_mask"; @VisibleForTesting static final String KEY_DNS_PREF = "dns"; @VisibleForTesting static final String KEY_IPV6_CATEGORY = "ipv6_category"; @VisibleForTesting static final String KEY_IPV6_ADDRESSES_PREF = "ipv6_addresses"; @VisibleForTesting static final String KEY_WIFI_TYPE_PREF = "type"; private final WifiEntry mWifiEntry; private final ConnectivityManager mConnectivityManager; private final PreferenceFragmentCompat mFragment; private final Handler mHandler; private LinkProperties mLinkProperties; private Network mNetwork; private NetworkCapabilities mNetworkCapabilities; private int mRssiSignalLevel = -1; @VisibleForTesting boolean mShowX; // Shows the Wi-Fi signal icon of Pie+x when it's true. private String[] mSignalStr; private final WifiManager mWifiManager; private final MetricsFeatureProvider mMetricsFeatureProvider; // UI elements - in order of appearance private ActionButtonsPreference mButtonsPref; @VisibleForTesting EntityHeaderController mEntityHeaderController; private Preference mSignalStrengthPref; private Preference mTxLinkSpeedPref; private Preference mRxLinkSpeedPref; private Preference mFrequencyPref; private Preference mSecurityPref; private Preference mSsidPref; private Preference mEapSimSubscriptionPref; private Preference mMacAddressPref; private Preference mIpAddressPref; private Preference mGatewayPref; private Preference mSubnetPref; private Preference mDnsPref; private Preference mTypePref; private Preference mIpv6AddressPref; private final IconInjector mIconInjector; private final Clock mClock; private final NetworkRequest mNetworkRequest = new NetworkRequest.Builder() .clearCapabilities().addTransportType(TRANSPORT_WIFI).build(); private CarrierIdAsyncQueryHandler mCarrierIdAsyncQueryHandler; private static final int TOKEN_QUERY_CARRIER_ID_AND_UPDATE_SIM_SUMMARY = 1; private static final int COLUMN_CARRIER_NAME = 0; private class CarrierIdAsyncQueryHandler extends AsyncQueryHandler { private CarrierIdAsyncQueryHandler(Context context) { super(context.getContentResolver()); } @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { if (token == TOKEN_QUERY_CARRIER_ID_AND_UPDATE_SIM_SUMMARY) { if (mContext == null || cursor == null || !cursor.moveToFirst()) { if (cursor != null) { cursor.close(); } mEapSimSubscriptionPref.setSummary(R.string.wifi_require_sim_card_to_connect); return; } mEapSimSubscriptionPref.setSummary(mContext.getString( R.string.wifi_require_specific_sim_card_to_connect, cursor.getString(COLUMN_CARRIER_NAME))); cursor.close(); return; } } } // Must be run on the UI thread since it directly manipulates UI state. private final NetworkCallback mNetworkCallback = new NetworkCallback() { @Override public void onLinkPropertiesChanged(Network network, LinkProperties lp) { if (network.equals(mNetwork) && !lp.equals(mLinkProperties)) { mLinkProperties = lp; refreshEntityHeader(); refreshButtons(); refreshIpLayerInfo(); } } private boolean hasCapabilityChanged(NetworkCapabilities nc, int cap) { // If this is the first time we get NetworkCapabilities, report that something changed. if (mNetworkCapabilities == null) return true; // nc can never be null, see ConnectivityService#callCallbackForRequest. return mNetworkCapabilities.hasCapability(cap) != nc.hasCapability(cap); } private boolean hasPrivateDnsStatusChanged(NetworkCapabilities nc) { // If this is the first time that WifiDetailPreferenceController2 gets // NetworkCapabilities, report that something has changed and assign nc to // mNetworkCapabilities in onCapabilitiesChanged. Note that the NetworkCapabilities // from onCapabilitiesChanged() will never be null, so calling // mNetworkCapabilities.isPrivateDnsBroken() would be safe next time. if (mNetworkCapabilities == null) { return true; } return mNetworkCapabilities.isPrivateDnsBroken() != nc.isPrivateDnsBroken(); } @Override public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) { // If the network just validated or lost Internet access or detected partial internet // connectivity or private dns was broken, refresh network state. Don't do this on // every NetworkCapabilities change because refreshEntityHeader sends IPCs to the // system server from the UI thread, which can cause jank. if (network.equals(mNetwork) && !nc.equals(mNetworkCapabilities)) { if (hasPrivateDnsStatusChanged(nc) || hasCapabilityChanged(nc, NET_CAPABILITY_VALIDATED) || hasCapabilityChanged(nc, NET_CAPABILITY_CAPTIVE_PORTAL) || hasCapabilityChanged(nc, NET_CAPABILITY_PARTIAL_CONNECTIVITY)) { refreshEntityHeader(); } mNetworkCapabilities = nc; refreshButtons(); refreshIpLayerInfo(); } } @Override public void onLost(Network network) { // Ephemeral network not a saved network, leave detail page once disconnected if (!mWifiEntry.isSaved() && network.equals(mNetwork)) { if (DEBUG) { Log.d(TAG, "OnLost and exit WifiNetworkDetailsPage"); } mFragment.getActivity().finish(); } } }; /** * To get an instance of {@link WifiDetailPreferenceController2} */ public static WifiDetailPreferenceController2 newInstance( WifiEntry wifiEntry, ConnectivityManager connectivityManager, Context context, PreferenceFragmentCompat fragment, Handler handler, Lifecycle lifecycle, WifiManager wifiManager, MetricsFeatureProvider metricsFeatureProvider) { return new WifiDetailPreferenceController2( wifiEntry, connectivityManager, context, fragment, handler, lifecycle, wifiManager, metricsFeatureProvider, new IconInjector(context), new Clock()); } @VisibleForTesting /* package */ WifiDetailPreferenceController2( WifiEntry wifiEntry, ConnectivityManager connectivityManager, Context context, PreferenceFragmentCompat fragment, Handler handler, Lifecycle lifecycle, WifiManager wifiManager, MetricsFeatureProvider metricsFeatureProvider, IconInjector injector, Clock clock) { super(context); mWifiEntry = wifiEntry; mWifiEntry.setListener(this); mConnectivityManager = connectivityManager; mFragment = fragment; mHandler = handler; mSignalStr = context.getResources().getStringArray(R.array.wifi_signal); mWifiManager = wifiManager; mMetricsFeatureProvider = metricsFeatureProvider; mIconInjector = injector; mClock = clock; lifecycle.addObserver(this); } @Override public boolean isAvailable() { return true; } @Override public String getPreferenceKey() { // Returns null since this controller contains more than one Preference return null; } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); setupEntityHeader(screen); mButtonsPref = ((ActionButtonsPreference) screen.findPreference(KEY_BUTTONS_PREF)) .setButton1Text(R.string.forget) .setButton1Icon(R.drawable.ic_settings_delete) .setButton1OnClickListener(view -> forgetNetwork()) .setButton2Text(R.string.wifi_sign_in_button_text) .setButton2Icon(R.drawable.ic_settings_sign_in) .setButton2OnClickListener(view -> signIntoNetwork()) .setButton3Text(getConnectDisconnectButtonTextResource()) .setButton3Icon(getConnectDisconnectButtonIconResource()) .setButton3OnClickListener(view -> connectDisconnectNetwork()) .setButton4Text(R.string.share) .setButton4Icon(R.drawable.ic_qrcode_24dp) .setButton4OnClickListener(view -> shareNetwork()); updateCaptivePortalButton(); mSignalStrengthPref = screen.findPreference(KEY_SIGNAL_STRENGTH_PREF); mTxLinkSpeedPref = screen.findPreference(KEY_TX_LINK_SPEED); mRxLinkSpeedPref = screen.findPreference(KEY_RX_LINK_SPEED); mFrequencyPref = screen.findPreference(KEY_FREQUENCY_PREF); mSecurityPref = screen.findPreference(KEY_SECURITY_PREF); mSsidPref = screen.findPreference(KEY_SSID_PREF); mEapSimSubscriptionPref = screen.findPreference(KEY_EAP_SIM_SUBSCRIPTION_PREF); mMacAddressPref = screen.findPreference(KEY_MAC_ADDRESS_PREF); mIpAddressPref = screen.findPreference(KEY_IP_ADDRESS_PREF); mGatewayPref = screen.findPreference(KEY_GATEWAY_PREF); mSubnetPref = screen.findPreference(KEY_SUBNET_MASK_PREF); mDnsPref = screen.findPreference(KEY_DNS_PREF); mTypePref = screen.findPreference(KEY_WIFI_TYPE_PREF); mIpv6AddressPref = screen.findPreference(KEY_IPV6_ADDRESSES_PREF); } /** * Update text, icon and listener of the captive portal button. * @return True if the button should be shown. */ private boolean updateCaptivePortalButton() { final Uri venueInfoUrl = getCaptivePortalVenueInfoUrl(); if (venueInfoUrl == null) { mButtonsPref.setButton2Text(R.string.wifi_sign_in_button_text) .setButton2Icon(R.drawable.ic_settings_sign_in) .setButton2OnClickListener(view -> signIntoNetwork()); return canSignIntoNetwork(); } mButtonsPref.setButton2Text(R.string.wifi_venue_website_button_text) .setButton2Icon(R.drawable.ic_settings_sign_in) .setButton2OnClickListener(view -> { final Intent infoIntent = new Intent(Intent.ACTION_VIEW); infoIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); infoIntent.setData(venueInfoUrl); mContext.startActivity(infoIntent); }); // Only show the venue website when the network is connected. return mWifiEntry.getConnectedState() == WifiEntry.CONNECTED_STATE_CONNECTED; } private Uri getCaptivePortalVenueInfoUrl() { final LinkProperties lp = mLinkProperties; if (lp == null) { return null; } final CaptivePortalData data = lp.getCaptivePortalData(); if (data == null) { return null; } return data.getVenueInfoUrl(); } private void setupEntityHeader(PreferenceScreen screen) { LayoutPreference headerPref = screen.findPreference(KEY_HEADER); mEntityHeaderController = EntityHeaderController.newInstance( mFragment.getActivity(), mFragment, headerPref.findViewById(R.id.entity_header)); ImageView iconView = headerPref.findViewById(R.id.entity_header_icon); iconView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); } private String getExpiryTimeSummary() { if (mLinkProperties == null || mLinkProperties.getCaptivePortalData() == null) { return null; } final long expiryTimeMillis = mLinkProperties.getCaptivePortalData().getExpiryTimeMillis(); if (expiryTimeMillis <= 0) { return null; } final ZonedDateTime now = mClock.now(); final ZonedDateTime expiryTime = ZonedDateTime.ofInstant( Instant.ofEpochMilli(expiryTimeMillis), now.getZone()); if (now.isAfter(expiryTime)) { return null; } if (now.plusDays(2).isAfter(expiryTime)) { // Expiration within 2 days: show a duration return mContext.getString(R.string.wifi_time_remaining, StringUtil.formatElapsedTime( mContext, Duration.between(now, expiryTime).getSeconds() * 1000, false /* withSeconds */, false /* collapseTimeUnit */)); } // For more than 2 days, show the expiry date return mContext.getString(R.string.wifi_expiry_time, DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).format(expiryTime)); } private void refreshEntityHeader() { mEntityHeaderController .setLabel(mWifiEntry.getTitle()) .setSummary(mWifiEntry.getSummary()) .setSecondSummary(getExpiryTimeSummary()) .done(true /* rebind */); } @VisibleForTesting void updateNetworkInfo() { if (mWifiEntry.getConnectedState() == WifiEntry.CONNECTED_STATE_CONNECTED) { mNetwork = mWifiManager.getCurrentNetwork(); mLinkProperties = mConnectivityManager.getLinkProperties(mNetwork); mNetworkCapabilities = mConnectivityManager.getNetworkCapabilities(mNetwork); } else { mNetwork = null; mLinkProperties = null; mNetworkCapabilities = null; } } @Override public void onResume() { // Disable the animation of the EntityHeaderController final RecyclerView recyclerView = mFragment.getListView(); if (recyclerView != null) { recyclerView.setItemAnimator(null); } // Ensure mNetwork is set before any callbacks above are delivered, since our // NetworkCallback only looks at changes to mNetwork. updateNetworkInfo(); refreshPage(); mConnectivityManager.registerNetworkCallback(mNetworkRequest, mNetworkCallback, mHandler); } @Override public void onPause() { mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); } private void refreshPage() { Log.d(TAG, "Update UI!"); // refresh header icon refreshEntryHeaderIcon(); // refresh header refreshEntityHeader(); // refresh Buttons refreshButtons(); // Update Connection Header icon and Signal Strength Preference refreshRssiViews(); // Frequency Pref refreshFrequency(); // Security Pref refreshSecurity(); // Transmit Link Speed Pref refreshTxSpeed(); // Receive Link Speed Pref refreshRxSpeed(); // IP related information refreshIpLayerInfo(); // SSID Pref refreshSsid(); // EAP SIM subscription refreshEapSimSubscription(); // MAC Address Pref refreshMacAddress(); // Wifi Type refreshWifiType(); } @VisibleForTesting void refreshEntryHeaderIcon() { if (mEntityHeaderController == null) { return; } Drawable drawable = getWifiDrawable(mWifiEntry); mEntityHeaderController .setIcon(redrawIconForHeader(drawable)) .done(true /* rebind */); } /** * Returns a Wi-Fi icon {@link Drawable}. * * @param wifiEntry {@link WifiEntry} */ @VisibleForTesting Drawable getWifiDrawable(WifiEntry wifiEntry) { if (wifiEntry instanceof HotspotNetworkEntry) { int deviceType = ((HotspotNetworkEntry) wifiEntry).getDeviceType(); return mContext.getDrawable(getHotspotIconResource(deviceType)); } if (mWifiEntry.getLevel() == WifiEntry.WIFI_LEVEL_UNREACHABLE) { return mContext.getDrawable(R.drawable.empty_icon); } return mIconInjector.getIcon(wifiEntry.shouldShowXLevelIcon(), wifiEntry.getLevel()); } private void refreshRssiViews() { int signalLevel = mWifiEntry.getLevel(); // Disappears signal view if not in range. e.g. for saved networks. if (signalLevel == WifiEntry.WIFI_LEVEL_UNREACHABLE) { mSignalStrengthPref.setVisible(false); mRssiSignalLevel = -1; return; } boolean showX = mWifiEntry.shouldShowXLevelIcon(); if (mRssiSignalLevel == signalLevel && mShowX == showX) { return; } mRssiSignalLevel = signalLevel; mShowX = showX; Drawable wifiIcon = mIconInjector.getIcon(mShowX, mRssiSignalLevel); Drawable wifiIconDark = wifiIcon.getConstantState().newDrawable().mutate(); wifiIconDark.setTintList(Utils.getColorAttr(mContext, android.R.attr.colorControlNormal)); mSignalStrengthPref.setIcon(wifiIconDark); mSignalStrengthPref.setSummary(mSignalStr[mRssiSignalLevel]); mSignalStrengthPref.setVisible(true); } private Drawable redrawIconForHeader(Drawable original) { final int iconSize = mContext.getResources().getDimensionPixelSize( R.dimen.wifi_detail_page_header_image_size); final int actualWidth = original.getMinimumWidth(); final int actualHeight = original.getMinimumHeight(); if ((actualWidth == iconSize && actualHeight == iconSize) || !VectorDrawable.class.isInstance(original)) { return original; } // clear tint list to make sure can set 87% black after enlarge original.setTintList(null); // enlarge icon size final Bitmap bitmap = Utils.createBitmap(original, iconSize /*width*/, iconSize /*height*/); Drawable newIcon = new BitmapDrawable(null /*resource*/, bitmap); // config color for 87% black after enlarge newIcon.setTintList(Utils.getColorAttr(mContext, android.R.attr.textColorPrimary)); return newIcon; } private void refreshFrequency() { final String bandString = mWifiEntry.getBandString(); if (TextUtils.isEmpty(bandString)) { mFrequencyPref.setVisible(false); return; } mFrequencyPref.setSummary(bandString); mFrequencyPref.setVisible(true); } private void refreshSecurity() { mSecurityPref.setSummary(mWifiEntry.getSecurityString(false /* concise */)); } private void refreshTxSpeed() { String summary = mWifiEntry.getTxSpeedString(); if (TextUtils.isEmpty(summary)) { mTxLinkSpeedPref.setVisible(false); return; } mTxLinkSpeedPref.setVisible(true); mTxLinkSpeedPref.setSummary(summary); } private void refreshRxSpeed() { String summary = mWifiEntry.getRxSpeedString(); if (TextUtils.isEmpty(summary)) { mRxLinkSpeedPref.setVisible(false); return; } mRxLinkSpeedPref.setVisible(true); mRxLinkSpeedPref.setSummary(summary); } private void refreshSsid() { if (mWifiEntry.shouldShowSsid() && mWifiEntry.getSsid() != null) { mSsidPref.setVisible(true); mSsidPref.setSummary(mWifiEntry.getSsid()); } else { mSsidPref.setVisible(false); } } private void refreshEapSimSubscription() { mEapSimSubscriptionPref.setVisible(false); if (mWifiEntry.getSecurity() != WifiEntry.SECURITY_EAP) { return; } final WifiConfiguration config = mWifiEntry.getWifiConfiguration(); if (config == null || config.enterpriseConfig == null) { return; } if (!config.enterpriseConfig.isAuthenticationSimBased()) { return; } mEapSimSubscriptionPref.setVisible(true); // Checks if the SIM subscription is active. final List activeSubscriptionInfos = mContext .getSystemService(SubscriptionManager.class).getActiveSubscriptionInfoList(); if (activeSubscriptionInfos != null) { SubscriptionInfo info = fineSubscriptionInfo(config.carrierId, activeSubscriptionInfos, SubscriptionManager.getDefaultDataSubscriptionId()); if (info != null) { mEapSimSubscriptionPref.setSummary( SubscriptionUtil.getUniqueSubscriptionDisplayName(info, mContext)); return; } } if (config.carrierId == UNKNOWN_CARRIER_ID) { mEapSimSubscriptionPref.setSummary(R.string.wifi_no_related_sim_card); return; } // The Wi-Fi network has specified carrier id, query carrier name from CarrierIdProvider. if (mCarrierIdAsyncQueryHandler == null) { mCarrierIdAsyncQueryHandler = new CarrierIdAsyncQueryHandler(mContext); } mCarrierIdAsyncQueryHandler.cancelOperation(TOKEN_QUERY_CARRIER_ID_AND_UPDATE_SIM_SUMMARY); mCarrierIdAsyncQueryHandler.startQuery(TOKEN_QUERY_CARRIER_ID_AND_UPDATE_SIM_SUMMARY, null /* cookie */, CarrierId.All.CONTENT_URI, new String[]{CarrierId.CARRIER_NAME}, CarrierId.CARRIER_ID + "=?", new String[] {Integer.toString(config.carrierId)}, null /* orderBy */); } @VisibleForTesting SubscriptionInfo fineSubscriptionInfo(int carrierId, List activeSubscriptionInfos, int defaultDataSubscriptionId) { SubscriptionInfo firstMatchedInfo = null; for (SubscriptionInfo info : activeSubscriptionInfos) { // When it's UNKNOWN_CARRIER_ID or matched with configured CarrierId, // devices connects it with the SIM subscription of defaultDataSubscriptionId. if (defaultDataSubscriptionId == info.getSubscriptionId() && (carrierId == info.getCarrierId() || carrierId == UNKNOWN_CARRIER_ID)) { return info; } if (firstMatchedInfo == null && carrierId == info.getCarrierId()) { firstMatchedInfo = info; } } return firstMatchedInfo; } private void refreshMacAddress() { final String macAddress = mWifiEntry.getMacAddress(); if (TextUtils.isEmpty(macAddress)) { mMacAddressPref.setVisible(false); return; } mMacAddressPref.setVisible(true); mMacAddressPref.setTitle(getMacAddressTitle()); if (macAddress.equals(WifiInfo.DEFAULT_MAC_ADDRESS)) { mMacAddressPref.setSummary(R.string.device_info_not_available); } else { mMacAddressPref.setSummary(macAddress); } } private void refreshWifiType() { final String typeString = mWifiEntry.getStandardString(); if (!TextUtils.isEmpty(typeString)) { mTypePref.setSummary(typeString); mTypePref.setVisible(true); } else { mTypePref.setVisible(false); } } private int getMacAddressTitle() { if (mWifiEntry.getPrivacy() == WifiEntry.PRIVACY_RANDOMIZED_MAC) { return mWifiEntry.getConnectedState() == WifiEntry.CONNECTED_STATE_CONNECTED ? R.string.wifi_advanced_randomized_mac_address_title : R.string.wifi_advanced_randomized_mac_address_disconnected_title; } return R.string.wifi_advanced_device_mac_address_title; } private void updatePreference(Preference pref, String detailText) { if (!TextUtils.isEmpty(detailText)) { pref.setSummary(detailText); pref.setVisible(true); } else { pref.setVisible(false); } } private void refreshButtons() { final boolean canForgetNetwork = canForgetNetwork(); final boolean showCaptivePortalButton = updateCaptivePortalButton(); final boolean canConnectDisconnectNetwork = mWifiEntry.canConnect() || mWifiEntry.canDisconnect(); final boolean canShareNetwork = canShareNetwork(); mButtonsPref.setButton1Visible(canForgetNetwork); mButtonsPref.setButton2Visible(showCaptivePortalButton); // Keep the connect/disconnected button visible if we can connect/disconnect, or if we are // in the middle of connecting (greyed out). mButtonsPref.setButton3Visible(canConnectDisconnectNetwork || mWifiEntry.getConnectedState() == WifiEntry.CONNECTED_STATE_CONNECTING); mButtonsPref.setButton3Enabled(canConnectDisconnectNetwork); mButtonsPref.setButton3Text(getConnectDisconnectButtonTextResource()); mButtonsPref.setButton3Icon(getConnectDisconnectButtonIconResource()); mButtonsPref.setButton4Visible(canShareNetwork); mButtonsPref.setVisible(canForgetNetwork || showCaptivePortalButton || canConnectDisconnectNetwork || canShareNetwork); } private int getConnectDisconnectButtonTextResource() { switch (mWifiEntry.getConnectedState()) { case WifiEntry.CONNECTED_STATE_DISCONNECTED: return R.string.wifi_connect; case WifiEntry.CONNECTED_STATE_CONNECTED: return R.string.wifi_disconnect_button_text; case WifiEntry.CONNECTED_STATE_CONNECTING: return R.string.wifi_connecting; default: throw new IllegalStateException("Invalid WifiEntry connected state"); } } private int getConnectDisconnectButtonIconResource() { switch (mWifiEntry.getConnectedState()) { case WifiEntry.CONNECTED_STATE_DISCONNECTED: case WifiEntry.CONNECTED_STATE_CONNECTING: return R.drawable.ic_settings_wireless; case WifiEntry.CONNECTED_STATE_CONNECTED: return R.drawable.ic_settings_close; default: throw new IllegalStateException("Invalid WifiEntry connected state"); } } private void refreshIpLayerInfo() { // Hide IP layer info if not a connected network. if (mWifiEntry.getConnectedState() != WifiEntry.CONNECTED_STATE_CONNECTED || mNetwork == null || mLinkProperties == null) { mIpAddressPref.setVisible(false); mSubnetPref.setVisible(false); mGatewayPref.setVisible(false); mDnsPref.setVisible(false); mIpv6AddressPref.setVisible(false); return; } // Find IPv4 and IPv6 addresses. String ipv4Address = null; String subnet = null; StringJoiner ipv6Addresses = new StringJoiner("\n"); for (LinkAddress addr : mLinkProperties.getLinkAddresses()) { if (addr.getAddress() instanceof Inet4Address) { ipv4Address = addr.getAddress().getHostAddress(); subnet = ipv4PrefixLengthToSubnetMask(addr.getPrefixLength()); } else if (addr.getAddress() instanceof Inet6Address) { ipv6Addresses.add(addr.getAddress().getHostAddress()); } } // Find IPv4 default gateway. String gateway = null; for (RouteInfo routeInfo : mLinkProperties.getRoutes()) { if (routeInfo.hasGateway() && routeInfo.isDefaultRoute() && routeInfo.getDestination().getAddress() instanceof Inet4Address) { gateway = routeInfo.getGateway().getHostAddress(); break; } } // Find all (IPv4 and IPv6) DNS addresses. String dnsServers = mLinkProperties.getDnsServers().stream() .map(InetAddress::getHostAddress) .collect(Collectors.joining("\n")); // Update UI. updatePreference(mIpAddressPref, ipv4Address); updatePreference(mSubnetPref, subnet); updatePreference(mGatewayPref, gateway); updatePreference(mDnsPref, dnsServers); if (ipv6Addresses.length() > 0) { mIpv6AddressPref.setVisible(true); mIpv6AddressPref.setSummary( BidiFormatter.getInstance().unicodeWrap(ipv6Addresses.toString())); } else { mIpv6AddressPref.setVisible(false); } } private static String ipv4PrefixLengthToSubnetMask(int prefixLength) { try { return Inet4AddressUtils.getPrefixMaskAsInet4Address(prefixLength).getHostAddress(); } catch (IllegalArgumentException e) { return null; } } /** * Returns whether the network represented by this preference can be modified. */ public boolean canModifyNetwork() { return mWifiEntry.isSaved() && !WifiUtils.isNetworkLockedDown(mContext, mWifiEntry.getWifiConfiguration()); } /** * Returns whether the network represented by this preference can be forgotten. */ public boolean canForgetNetwork() { return mWifiEntry.canForget() && !WifiUtils.isNetworkLockedDown(mContext, mWifiEntry.getWifiConfiguration()); } /** * Returns whether the user can sign into the network represented by this preference. */ private boolean canSignIntoNetwork() { return mWifiEntry.canSignIn(); } /** * Returns whether the user can share the network represented by this preference with QR code. */ private boolean canShareNetwork() { return mWifiEntry.canShare(); } /** * Forgets the wifi network associated with this preference. */ private void forgetNetwork() { if (mWifiEntry.isSubscription()) { // Post a dialog to confirm if user really want to forget the passpoint network. showConfirmForgetDialog(); return; } else { mWifiEntry.forget(this); } final Activity activity = mFragment.getActivity(); if (activity != null) { mMetricsFeatureProvider.action(activity, SettingsEnums.ACTION_WIFI_FORGET); activity.finish(); } } @VisibleForTesting protected void showConfirmForgetDialog() { final AlertDialog dialog = new AlertDialog.Builder(mContext) .setPositiveButton(R.string.forget, ((dialog1, which) -> { try { mWifiEntry.forget(this); } catch (RuntimeException e) { Log.e(TAG, "Failed to remove Passpoint configuration: " + e); } mMetricsFeatureProvider.action( mFragment.getActivity(), SettingsEnums.ACTION_WIFI_FORGET); mFragment.getActivity().finish(); })) .setNegativeButton(R.string.cancel, null /* listener */) .setTitle(R.string.wifi_forget_dialog_title) .setMessage(R.string.forget_passpoint_dialog_message) .create(); dialog.show(); } /** * Show QR code to share the network represented by this preference. */ private void launchWifiDppConfiguratorActivity() { final Intent intent = WifiDppUtils.getConfiguratorQrCodeGeneratorIntentOrNull(mContext, mWifiManager, mWifiEntry); if (intent == null) { Log.e(TAG, "Launch Wi-Fi DPP QR code generator with a wrong Wi-Fi network!"); } else { mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, SettingsEnums.ACTION_SETTINGS_SHARE_WIFI_QR_CODE, SettingsEnums.SETTINGS_WIFI_DPP_CONFIGURATOR, /* key */ null, /* value */ Integer.MIN_VALUE); mContext.startActivity(intent); } } /** * Share the wifi network with QR code. */ private void shareNetwork() { WifiDppUtils.showLockScreen(mContext, () -> launchWifiDppConfiguratorActivity()); } /** * Sign in to the captive portal found on this wifi network associated with this preference. */ private void signIntoNetwork() { mMetricsFeatureProvider.action( mFragment.getActivity(), SettingsEnums.ACTION_WIFI_SIGNIN); mWifiEntry.signIn(this); } @Override public void onSubmit(WifiDialog2 dialog) { if (dialog.getController() != null) { mWifiManager.save(dialog.getController().getConfig(), new WifiManager.ActionListener() { @Override public void onSuccess() { } @Override public void onFailure(int reason) { Activity activity = mFragment.getActivity(); if (activity != null) { Toast.makeText(activity, R.string.wifi_failed_save_message, Toast.LENGTH_SHORT).show(); } } }); } } /** * Wrapper for testing compatibility. */ @VisibleForTesting static class IconInjector { private final Context mContext; IconInjector(Context context) { mContext = context; } public Drawable getIcon(boolean showX, int level) { return mContext.getDrawable(WifiUtils.getInternetIconResource(level, showX)).mutate(); } } @VisibleForTesting static class Clock { public ZonedDateTime now() { return ZonedDateTime.now(); } } @VisibleForTesting void connectDisconnectNetwork() { if (mWifiEntry.getConnectedState() == WifiEntry.CONNECTED_STATE_DISCONNECTED) { mWifiEntry.connect(this); } else { mWifiEntry.disconnect(this); } } /** * Indicates the state of the WifiEntry has changed and clients may retrieve updates through * the WifiEntry getter methods. */ @Override public void onUpdated() { updateNetworkInfo(); refreshPage(); // Refresh the Preferences in fragment. ((WifiNetworkDetailsFragment) mFragment).refreshPreferences(); } /** * Result of the connect request indicated by the CONNECT_STATUS constants. */ @Override public void onConnectResult(@ConnectStatus int status) { if (status == ConnectCallback.CONNECT_STATUS_SUCCESS) { Toast.makeText(mContext, mContext.getString(R.string.wifi_connected_to_message, mWifiEntry.getTitle()), Toast.LENGTH_SHORT).show(); } else if (mWifiEntry.getLevel() == WifiEntry.WIFI_LEVEL_UNREACHABLE) { Toast.makeText(mContext, R.string.wifi_not_in_range_message, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(mContext, R.string.wifi_failed_connect_message, Toast.LENGTH_SHORT).show(); } } /** * Result of the disconnect request indicated by the DISCONNECT_STATUS constants. */ @Override public void onDisconnectResult(@DisconnectStatus int status) { if (status == DisconnectCallback.DISCONNECT_STATUS_SUCCESS) { final Activity activity = mFragment.getActivity(); if (activity != null) { Toast.makeText(activity, activity.getString(R.string.wifi_disconnected_from, mWifiEntry.getTitle()), Toast.LENGTH_SHORT).show(); } } else { Log.e(TAG, "Disconnect Wi-Fi network failed"); } } /** * Result of the forget request indicated by the FORGET_STATUS constants. */ @Override public void onForgetResult(@ForgetStatus int status) { if (status != ForgetCallback.FORGET_STATUS_SUCCESS) { Log.e(TAG, "Forget Wi-Fi network failed"); } final Activity activity = mFragment.getActivity(); if (activity != null) { mMetricsFeatureProvider.action(activity, SettingsEnums.ACTION_WIFI_FORGET); activity.finish(); } } /** * Result of the sign-in request indicated by the SIGNIN_STATUS constants. */ @Override public void onSignInResult(@SignInStatus int status) { refreshPage(); } /** Sets signal strength title */ public void setSignalStrengthTitle(int titleResId) { if (mSignalStrengthPref != null) { mSignalStrengthPref.setTitle(titleResId); } } }