blob: 249544b7de738eb96d169c2c9f0d71ee585bb4db [file] [log] [blame]
/*
* 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.wifitrackerlib;
import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.DISABLED_AUTHENTICATION_FAILURE;
import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.DISABLED_AUTHENTICATION_NO_CREDENTIALS;
import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.DISABLED_BY_WRONG_PASSWORD;
import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLED;
import static android.net.wifi.WifiInfo.DEFAULT_MAC_ADDRESS;
import static android.net.wifi.WifiInfo.SECURITY_TYPE_EAP;
import static android.net.wifi.WifiInfo.SECURITY_TYPE_EAP_WPA3_ENTERPRISE;
import static android.net.wifi.WifiInfo.SECURITY_TYPE_EAP_WPA3_ENTERPRISE_192_BIT;
import static android.net.wifi.WifiInfo.SECURITY_TYPE_OPEN;
import static android.net.wifi.WifiInfo.SECURITY_TYPE_OWE;
import static android.net.wifi.WifiInfo.SECURITY_TYPE_PASSPOINT_R1_R2;
import static android.net.wifi.WifiInfo.SECURITY_TYPE_PASSPOINT_R3;
import static android.net.wifi.WifiInfo.SECURITY_TYPE_PSK;
import static android.net.wifi.WifiInfo.SECURITY_TYPE_SAE;
import static android.net.wifi.WifiInfo.SECURITY_TYPE_UNKNOWN;
import static android.net.wifi.WifiInfo.SECURITY_TYPE_WEP;
import static android.net.wifi.WifiInfo.sanitizeSsid;
import static com.android.wifitrackerlib.Utils.getAutoConnectDescription;
import static com.android.wifitrackerlib.Utils.getBestScanResultByLevel;
import static com.android.wifitrackerlib.Utils.getConnectedDescription;
import static com.android.wifitrackerlib.Utils.getConnectingDescription;
import static com.android.wifitrackerlib.Utils.getDisconnectedDescription;
import static com.android.wifitrackerlib.Utils.getImsiProtectionDescription;
import static com.android.wifitrackerlib.Utils.getMeteredDescription;
import static com.android.wifitrackerlib.Utils.getSecurityTypesFromScanResult;
import static com.android.wifitrackerlib.Utils.getSecurityTypesFromWifiConfiguration;
import static com.android.wifitrackerlib.Utils.getSingleSecurityTypeFromMultipleSecurityTypes;
import static com.android.wifitrackerlib.Utils.getVerboseLoggingDescription;
import android.annotation.SuppressLint;
import android.app.admin.DevicePolicyManager;
import android.app.admin.WifiSsidPolicy;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiConfiguration.NetworkSelectionStatus;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiSsid;
import android.os.Handler;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.core.os.BuildCompat;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;
import java.util.stream.Collectors;
/**
* WifiEntry representation of a logical Wi-Fi network, uniquely identified by SSID and security.
*
* This type of WifiEntry can represent both open and saved networks.
*/
@VisibleForTesting
public class StandardWifiEntry extends WifiEntry {
static final String TAG = "StandardWifiEntry";
public static final String KEY_PREFIX = "StandardWifiEntry:";
@NonNull private final StandardWifiEntryKey mKey;
@NonNull private final WifiTrackerInjector mInjector;
@NonNull private final Context mContext;
// Map of security type to matching scan results
@NonNull private final Map<Integer, List<ScanResult>> mMatchingScanResults = new HashMap<>();
// Map of security type to matching WifiConfiguration
// TODO: Change this to single WifiConfiguration once we can get multiple security type configs.
@NonNull private final Map<Integer, WifiConfiguration> mMatchingWifiConfigs = new HashMap<>();
// List of the target scan results to be displayed. This should match the highest available
// security from all of the matched WifiConfigurations.
// If no WifiConfigurations are available, then these should match the most appropriate security
// type (e.g. PSK for an PSK/SAE entry, OWE for an Open/OWE entry).
@NonNull private final List<ScanResult> mTargetScanResults = new ArrayList<>();
// Target WifiConfiguration for connection and displaying WifiConfiguration info
private WifiConfiguration mTargetWifiConfig;
private List<Integer> mTargetSecurityTypes = new ArrayList<>();
private boolean mIsUserShareable = false;
private boolean mShouldAutoOpenCaptivePortal = false;
private boolean mIsAdminRestricted = false;
private boolean mHasAddConfigUserRestriction = false;
private final boolean mIsWpa3SaeSupported;
private final boolean mIsWpa3SuiteBSupported;
private final boolean mIsEnhancedOpenSupported;
private final UserManager mUserManager;
private final DevicePolicyManager mDevicePolicyManager;
StandardWifiEntry(
@NonNull WifiTrackerInjector injector,
@NonNull Context context, @NonNull Handler callbackHandler,
@NonNull StandardWifiEntryKey key, @NonNull WifiManager wifiManager,
boolean forSavedNetworksPage) {
super(callbackHandler, wifiManager, forSavedNetworksPage);
mInjector = injector;
mContext = context;
mKey = key;
mIsWpa3SaeSupported = wifiManager.isWpa3SaeSupported();
mIsWpa3SuiteBSupported = wifiManager.isWpa3SuiteBSupported();
mIsEnhancedOpenSupported = wifiManager.isEnhancedOpenSupported();
mUserManager = injector.getUserManager();
mDevicePolicyManager = injector.getDevicePolicyManager();
updateSecurityTypes();
updateAdminRestrictions();
}
StandardWifiEntry(
@NonNull WifiTrackerInjector injector,
@NonNull Context context, @NonNull Handler callbackHandler,
@NonNull StandardWifiEntryKey key,
@Nullable List<WifiConfiguration> configs,
@Nullable List<ScanResult> scanResults,
@NonNull WifiManager wifiManager,
boolean forSavedNetworksPage) throws IllegalArgumentException {
this(injector, context, callbackHandler, key, wifiManager,
forSavedNetworksPage);
if (configs != null && !configs.isEmpty()) {
updateConfig(configs);
}
if (scanResults != null && !scanResults.isEmpty()) {
updateScanResultInfo(scanResults);
}
}
@Override
public String getKey() {
return mKey.toString();
}
StandardWifiEntryKey getStandardWifiEntryKey() {
return mKey;
}
@Override
public String getTitle() {
return mKey.getScanResultKey().getSsid();
}
@Override
public synchronized String getSummary(boolean concise) {
if (hasAdminRestrictions()) {
return mContext.getString(R.string.wifitrackerlib_admin_restricted_network);
}
StringJoiner sj = new StringJoiner(mContext.getString(
R.string.wifitrackerlib_summary_separator));
final String connectedStateDescription;
final @ConnectedState int connectedState = getConnectedState();
switch (connectedState) {
case CONNECTED_STATE_DISCONNECTED:
connectedStateDescription = getDisconnectedDescription(mInjector, mContext,
mTargetWifiConfig,
mForSavedNetworksPage,
concise);
break;
case CONNECTED_STATE_CONNECTING:
connectedStateDescription = getConnectingDescription(mContext, mNetworkInfo);
break;
case CONNECTED_STATE_CONNECTED:
connectedStateDescription = getConnectedDescription(mContext,
mTargetWifiConfig,
mNetworkCapabilities,
mIsDefaultNetwork,
mIsLowQuality);
break;
default:
Log.e(TAG, "getConnectedState() returned unknown state: " + connectedState);
connectedStateDescription = null;
}
if (!TextUtils.isEmpty(connectedStateDescription)) {
sj.add(connectedStateDescription);
}
final String autoConnectDescription = getAutoConnectDescription(mContext, this);
if (!TextUtils.isEmpty(autoConnectDescription)) {
sj.add(autoConnectDescription);
}
final String meteredDescription = getMeteredDescription(mContext, this);
if (!TextUtils.isEmpty(meteredDescription)) {
sj.add(meteredDescription);
}
if (!concise) {
final String verboseLoggingDescription = getVerboseLoggingDescription(this);
if (!TextUtils.isEmpty(verboseLoggingDescription)) {
sj.add(verboseLoggingDescription);
}
}
return sj.toString();
}
@Override
public CharSequence getSecondSummary() {
return getConnectedState() == CONNECTED_STATE_CONNECTED
? getImsiProtectionDescription(mContext, getWifiConfiguration()) : "";
}
@Override
public String getSsid() {
return mKey.getScanResultKey().getSsid();
}
@Override
public synchronized List<Integer> getSecurityTypes() {
return new ArrayList<>(mTargetSecurityTypes);
}
@Override
public synchronized String getMacAddress() {
if (mWifiInfo != null) {
final String wifiInfoMac = mWifiInfo.getMacAddress();
if (!TextUtils.isEmpty(wifiInfoMac)
&& !TextUtils.equals(wifiInfoMac, DEFAULT_MAC_ADDRESS)) {
return wifiInfoMac;
}
}
if (mTargetWifiConfig == null || getPrivacy() != PRIVACY_RANDOMIZED_MAC) {
final String[] factoryMacs = mWifiManager.getFactoryMacAddresses();
if (factoryMacs.length > 0) {
return factoryMacs[0];
}
return null;
}
return mTargetWifiConfig.getRandomizedMacAddress().toString();
}
@Override
public synchronized boolean isMetered() {
return getMeteredChoice() == METERED_CHOICE_METERED
|| (mTargetWifiConfig != null && mTargetWifiConfig.meteredHint);
}
@Override
public synchronized boolean isSaved() {
return mTargetWifiConfig != null && !mTargetWifiConfig.fromWifiNetworkSuggestion
&& !mTargetWifiConfig.isEphemeral();
}
@Override
public synchronized boolean isSuggestion() {
return mTargetWifiConfig != null && mTargetWifiConfig.fromWifiNetworkSuggestion;
}
@Override
public synchronized WifiConfiguration getWifiConfiguration() {
if (!isSaved()) {
return null;
}
return mTargetWifiConfig;
}
@Override
public synchronized boolean canConnect() {
if (mLevel == WIFI_LEVEL_UNREACHABLE
|| getConnectedState() != CONNECTED_STATE_DISCONNECTED) {
return false;
}
if (hasAdminRestrictions()) return false;
// Allow connection for EAP SIM dependent methods if the SIM of specified carrier ID is
// active in the device.
if (mTargetSecurityTypes.contains(SECURITY_TYPE_EAP) && mTargetWifiConfig != null
&& mTargetWifiConfig.enterpriseConfig != null) {
if (!mTargetWifiConfig.enterpriseConfig.isAuthenticationSimBased()) {
return true;
}
List<SubscriptionInfo> activeSubscriptionInfos = mContext
.getSystemService(SubscriptionManager.class).getActiveSubscriptionInfoList();
if (activeSubscriptionInfos == null || activeSubscriptionInfos.size() == 0) {
return false;
}
if (mTargetWifiConfig.carrierId == TelephonyManager.UNKNOWN_CARRIER_ID) {
// To connect via default subscription.
return true;
}
for (SubscriptionInfo subscriptionInfo : activeSubscriptionInfos) {
if (subscriptionInfo.getCarrierId() == mTargetWifiConfig.carrierId) {
return true;
}
}
return false;
}
return true;
}
@Override
public synchronized void connect(@Nullable ConnectCallback callback) {
mConnectCallback = callback;
// We should flag this network to auto-open captive portal since this method represents
// the user manually connecting to a network (i.e. not auto-join).
mShouldAutoOpenCaptivePortal = true;
mWifiManager.stopRestrictingAutoJoinToSubscriptionId();
if (isSaved() || isSuggestion()) {
if (Utils.isSimCredential(mTargetWifiConfig)
&& !Utils.isSimPresent(mContext, mTargetWifiConfig.carrierId)) {
if (callback != null) {
mCallbackHandler.post(() ->
callback.onConnectResult(
ConnectCallback.CONNECT_STATUS_FAILURE_SIM_ABSENT));
}
return;
}
// Saved/suggested network
mWifiManager.connect(mTargetWifiConfig.networkId, new ConnectActionListener());
} else {
if (mTargetSecurityTypes.contains(SECURITY_TYPE_OWE)) {
// OWE network
final WifiConfiguration oweConfig = new WifiConfiguration();
oweConfig.SSID = "\"" + mKey.getScanResultKey().getSsid() + "\"";
oweConfig.setSecurityParams(WifiConfiguration.SECURITY_TYPE_OWE);
mWifiManager.connect(oweConfig, new ConnectActionListener());
if (mTargetSecurityTypes.contains(SECURITY_TYPE_OPEN)) {
// Add an extra Open config for OWE transition networks
final WifiConfiguration openConfig = new WifiConfiguration();
openConfig.SSID = "\"" + mKey.getScanResultKey().getSsid() + "\"";
openConfig.setSecurityParams(WifiConfiguration.SECURITY_TYPE_OPEN);
mWifiManager.save(openConfig, null);
}
} else if (mTargetSecurityTypes.contains(SECURITY_TYPE_OPEN)) {
// Open network
final WifiConfiguration openConfig = new WifiConfiguration();
openConfig.SSID = "\"" + mKey.getScanResultKey().getSsid() + "\"";
openConfig.setSecurityParams(WifiConfiguration.SECURITY_TYPE_OPEN);
mWifiManager.connect(openConfig, new ConnectActionListener());
} else {
// Secure network
if (callback != null) {
mCallbackHandler.post(() ->
callback.onConnectResult(
ConnectCallback.CONNECT_STATUS_FAILURE_NO_CONFIG));
}
}
}
}
@Override
public boolean canDisconnect() {
return getConnectedState() == CONNECTED_STATE_CONNECTED;
}
@Override
public synchronized void disconnect(@Nullable DisconnectCallback callback) {
if (canDisconnect()) {
mCalledDisconnect = true;
mDisconnectCallback = callback;
mCallbackHandler.postDelayed(() -> {
if (callback != null && mCalledDisconnect) {
callback.onDisconnectResult(
DisconnectCallback.DISCONNECT_STATUS_FAILURE_UNKNOWN);
}
}, 10_000 /* delayMillis */);
mWifiManager.disableEphemeralNetwork("\"" + mKey.getScanResultKey().getSsid() + "\"");
mWifiManager.disconnect();
}
}
@Override
public boolean canForget() {
return getWifiConfiguration() != null;
}
@Override
public synchronized void forget(@Nullable ForgetCallback callback) {
if (canForget()) {
mForgetCallback = callback;
mWifiManager.forget(mTargetWifiConfig.networkId, new ForgetActionListener());
}
}
@Override
public synchronized boolean canSignIn() {
return mNetworkCapabilities != null
&& mNetworkCapabilities.hasCapability(
NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL);
}
@Override
public void signIn(@Nullable SignInCallback callback) {
if (canSignIn()) {
// canSignIn() implies that this WifiEntry is the currently connected network, so use
// getCurrentNetwork() to start the captive portal app.
NonSdkApiWrapper.startCaptivePortalApp(
mContext.getSystemService(ConnectivityManager.class),
mWifiManager.getCurrentNetwork());
}
}
/**
* Returns whether the network can be shared via QR code.
* See https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11
*/
@Override
public synchronized boolean canShare() {
if (mInjector.isDemoMode()) {
return false;
}
WifiConfiguration wifiConfig = getWifiConfiguration();
if (wifiConfig == null) {
return false;
}
if (BuildCompat.isAtLeastT() && mUserManager.hasUserRestrictionForUser(
UserManager.DISALLOW_SHARING_ADMIN_CONFIGURED_WIFI,
UserHandle.getUserHandleForUid(wifiConfig.creatorUid))
&& Utils.isDeviceOrProfileOwner(wifiConfig.creatorUid,
wifiConfig.creatorName, mContext)) {
return false;
}
for (int securityType : mTargetSecurityTypes) {
switch (securityType) {
case SECURITY_TYPE_OPEN:
case SECURITY_TYPE_OWE:
case SECURITY_TYPE_WEP:
case SECURITY_TYPE_PSK:
case SECURITY_TYPE_SAE:
return true;
}
}
return false;
}
/**
* Returns whether the user can use Easy Connect to onboard a device to the network.
* See https://www.wi-fi.org/discover-wi-fi/wi-fi-easy-connect
*/
@Override
public synchronized boolean canEasyConnect() {
if (mInjector.isDemoMode()) {
return false;
}
WifiConfiguration wifiConfig = getWifiConfiguration();
if (wifiConfig == null) {
return false;
}
if (!mWifiManager.isEasyConnectSupported()) {
return false;
}
if (BuildCompat.isAtLeastT() && mUserManager.hasUserRestrictionForUser(
UserManager.DISALLOW_SHARING_ADMIN_CONFIGURED_WIFI,
UserHandle.getUserHandleForUid(wifiConfig.creatorUid))
&& Utils.isDeviceOrProfileOwner(wifiConfig.creatorUid,
wifiConfig.creatorName, mContext)) {
return false;
}
// DPP 1.0 only supports WPA2 and WPA3.
return mTargetSecurityTypes.contains(SECURITY_TYPE_PSK)
|| mTargetSecurityTypes.contains(SECURITY_TYPE_SAE);
}
@Override
@MeteredChoice
public synchronized int getMeteredChoice() {
if (!isSuggestion() && mTargetWifiConfig != null) {
final int meteredOverride = mTargetWifiConfig.meteredOverride;
if (meteredOverride == WifiConfiguration.METERED_OVERRIDE_METERED) {
return METERED_CHOICE_METERED;
} else if (meteredOverride == WifiConfiguration.METERED_OVERRIDE_NOT_METERED) {
return METERED_CHOICE_UNMETERED;
}
}
return METERED_CHOICE_AUTO;
}
@Override
public boolean canSetMeteredChoice() {
return getWifiConfiguration() != null;
}
@Override
public synchronized void setMeteredChoice(int meteredChoice) {
if (!canSetMeteredChoice()) {
return;
}
if (meteredChoice == METERED_CHOICE_AUTO) {
mTargetWifiConfig.meteredOverride = WifiConfiguration.METERED_OVERRIDE_NONE;
} else if (meteredChoice == METERED_CHOICE_METERED) {
mTargetWifiConfig.meteredOverride = WifiConfiguration.METERED_OVERRIDE_METERED;
} else if (meteredChoice == METERED_CHOICE_UNMETERED) {
mTargetWifiConfig.meteredOverride = WifiConfiguration.METERED_OVERRIDE_NOT_METERED;
}
mWifiManager.save(mTargetWifiConfig, null /* listener */);
}
@Override
public boolean canSetPrivacy() {
return isSaved();
}
@Override
@Privacy
public synchronized int getPrivacy() {
if (mTargetWifiConfig != null
&& mTargetWifiConfig.macRandomizationSetting
== WifiConfiguration.RANDOMIZATION_NONE) {
return PRIVACY_DEVICE_MAC;
} else {
return PRIVACY_RANDOMIZED_MAC;
}
}
@Override
public synchronized void setPrivacy(int privacy) {
if (!canSetPrivacy()) {
return;
}
mTargetWifiConfig.macRandomizationSetting = privacy == PRIVACY_RANDOMIZED_MAC
? WifiConfiguration.RANDOMIZATION_AUTO : WifiConfiguration.RANDOMIZATION_NONE;
mWifiManager.save(mTargetWifiConfig, null /* listener */);
}
@Override
public synchronized boolean isAutoJoinEnabled() {
if (mTargetWifiConfig == null) {
return false;
}
return mTargetWifiConfig.allowAutojoin;
}
@Override
public boolean canSetAutoJoinEnabled() {
return isSaved() || isSuggestion();
}
@Override
public synchronized void setAutoJoinEnabled(boolean enabled) {
if (mTargetWifiConfig == null || !canSetAutoJoinEnabled()) {
return;
}
mWifiManager.allowAutojoin(mTargetWifiConfig.networkId, enabled);
}
@Override
public synchronized String getSecurityString(boolean concise) {
if (mTargetSecurityTypes.size() == 0) {
return concise ? "" : mContext.getString(R.string.wifitrackerlib_wifi_security_none);
}
if (mTargetSecurityTypes.size() == 1) {
final int security = mTargetSecurityTypes.get(0);
switch(security) {
case SECURITY_TYPE_EAP:
return concise ? mContext.getString(
R.string.wifitrackerlib_wifi_security_short_eap_wpa_wpa2) :
mContext.getString(
R.string.wifitrackerlib_wifi_security_eap_wpa_wpa2);
case SECURITY_TYPE_EAP_WPA3_ENTERPRISE:
return concise ? mContext.getString(
R.string.wifitrackerlib_wifi_security_short_eap_wpa3) :
mContext.getString(
R.string.wifitrackerlib_wifi_security_eap_wpa3);
case SECURITY_TYPE_EAP_WPA3_ENTERPRISE_192_BIT:
return concise ? mContext.getString(
R.string.wifitrackerlib_wifi_security_short_eap_suiteb) :
mContext.getString(R.string.wifitrackerlib_wifi_security_eap_suiteb);
case SECURITY_TYPE_PSK:
return concise ? mContext.getString(
R.string.wifitrackerlib_wifi_security_short_wpa_wpa2) :
mContext.getString(
R.string.wifitrackerlib_wifi_security_wpa_wpa2);
case SECURITY_TYPE_WEP:
return mContext.getString(R.string.wifitrackerlib_wifi_security_wep);
case SECURITY_TYPE_SAE:
return concise ? mContext.getString(
R.string.wifitrackerlib_wifi_security_short_sae) :
mContext.getString(R.string.wifitrackerlib_wifi_security_sae);
case SECURITY_TYPE_OWE:
return concise ? mContext.getString(
R.string.wifitrackerlib_wifi_security_short_owe) :
mContext.getString(R.string.wifitrackerlib_wifi_security_owe);
case SECURITY_TYPE_OPEN:
return concise ? "" : mContext.getString(
R.string.wifitrackerlib_wifi_security_none);
}
}
if (mTargetSecurityTypes.size() == 2) {
if (mTargetSecurityTypes.contains(SECURITY_TYPE_OPEN)
&& mTargetSecurityTypes.contains(SECURITY_TYPE_OWE)) {
StringJoiner sj = new StringJoiner("/");
sj.add(mContext.getString(R.string.wifitrackerlib_wifi_security_none));
sj.add(concise ? mContext.getString(
R.string.wifitrackerlib_wifi_security_short_owe) :
mContext.getString(R.string.wifitrackerlib_wifi_security_owe));
return sj.toString();
}
if (mTargetSecurityTypes.contains(SECURITY_TYPE_PSK)
&& mTargetSecurityTypes.contains(SECURITY_TYPE_SAE)) {
return concise ? mContext.getString(
R.string.wifitrackerlib_wifi_security_short_wpa_wpa2_wpa3) :
mContext.getString(
R.string.wifitrackerlib_wifi_security_wpa_wpa2_wpa3);
}
if (mTargetSecurityTypes.contains(SECURITY_TYPE_EAP)
&& mTargetSecurityTypes.contains(SECURITY_TYPE_EAP_WPA3_ENTERPRISE)) {
return concise ? mContext.getString(
R.string.wifitrackerlib_wifi_security_short_eap_wpa_wpa2_wpa3) :
mContext.getString(
R.string.wifitrackerlib_wifi_security_eap_wpa_wpa2_wpa3);
}
}
// Unknown security types
Log.e(TAG, "Couldn't get string for security types: " + mTargetSecurityTypes);
return concise ? "" : mContext.getString(R.string.wifitrackerlib_wifi_security_none);
}
@Override
public synchronized String getStandardString() {
if (mWifiInfo != null) {
return Utils.getStandardString(mContext, mWifiInfo.getWifiStandard());
}
if (!mTargetScanResults.isEmpty()) {
return Utils.getStandardString(mContext, mTargetScanResults.get(0).getWifiStandard());
}
return "";
}
@Override
public synchronized boolean shouldEditBeforeConnect() {
WifiConfiguration wifiConfig = getWifiConfiguration();
if (wifiConfig == null) {
return false;
}
// The network is disabled because of one of the authentication problems.
NetworkSelectionStatus networkSelectionStatus = wifiConfig.getNetworkSelectionStatus();
if (networkSelectionStatus.getNetworkSelectionStatus() != NETWORK_SELECTION_ENABLED) {
if (networkSelectionStatus.getDisableReasonCounter(DISABLED_AUTHENTICATION_FAILURE) > 0
|| networkSelectionStatus.getDisableReasonCounter(
DISABLED_BY_WRONG_PASSWORD) > 0
|| networkSelectionStatus.getDisableReasonCounter(
DISABLED_AUTHENTICATION_NO_CREDENTIALS) > 0) {
return true;
}
}
return false;
}
@WorkerThread
synchronized void updateScanResultInfo(@Nullable List<ScanResult> scanResults)
throws IllegalArgumentException {
if (scanResults == null) scanResults = new ArrayList<>();
final String ssid = mKey.getScanResultKey().getSsid();
for (ScanResult scan : scanResults) {
if (!TextUtils.equals(scan.SSID, ssid)) {
throw new IllegalArgumentException(
"Attempted to update with wrong SSID! Expected: "
+ ssid + ", Actual: " + scan.SSID + ", ScanResult: " + scan);
}
}
// Populate the cached scan result map
mMatchingScanResults.clear();
final Set<Integer> keySecurityTypes = mKey.getScanResultKey().getSecurityTypes();
for (ScanResult scan : scanResults) {
for (int security : getSecurityTypesFromScanResult(scan)) {
if (!keySecurityTypes.contains(security) || !isSecurityTypeSupported(security)) {
continue;
}
if (!mMatchingScanResults.containsKey(security)) {
mMatchingScanResults.put(security, new ArrayList<>());
}
mMatchingScanResults.get(security).add(scan);
}
}
updateSecurityTypes();
updateTargetScanResultInfo();
notifyOnUpdated();
}
private synchronized void updateTargetScanResultInfo() {
// Update the level using the scans matching the target security type
final ScanResult bestScanResult = getBestScanResultByLevel(mTargetScanResults);
if (getConnectedState() == CONNECTED_STATE_DISCONNECTED) {
mLevel = bestScanResult != null
? mWifiManager.calculateSignalLevel(bestScanResult.level)
: WIFI_LEVEL_UNREACHABLE;
}
}
@WorkerThread
@Override
synchronized void updateNetworkCapabilities(@Nullable NetworkCapabilities capabilities) {
super.updateNetworkCapabilities(capabilities);
// Auto-open an available captive portal if the user manually connected to this network.
if (canSignIn() && mShouldAutoOpenCaptivePortal) {
mShouldAutoOpenCaptivePortal = false;
signIn(null /* callback */);
}
}
@WorkerThread
synchronized void updateConfig(@Nullable List<WifiConfiguration> wifiConfigs)
throws IllegalArgumentException {
if (wifiConfigs == null) {
wifiConfigs = Collections.emptyList();
}
final ScanResultKey scanResultKey = mKey.getScanResultKey();
final String ssid = scanResultKey.getSsid();
final Set<Integer> securityTypes = scanResultKey.getSecurityTypes();
mMatchingWifiConfigs.clear();
for (WifiConfiguration config : wifiConfigs) {
if (!TextUtils.equals(ssid, sanitizeSsid(config.SSID))) {
throw new IllegalArgumentException(
"Attempted to update with wrong SSID!"
+ " Expected: " + ssid
+ ", Actual: " + sanitizeSsid(config.SSID)
+ ", Config: " + config);
}
for (int securityType : getSecurityTypesFromWifiConfiguration(config)) {
if (!securityTypes.contains(securityType)) {
throw new IllegalArgumentException(
"Attempted to update with wrong security!"
+ " Expected one of: " + securityTypes
+ ", Actual: " + securityType
+ ", Config: " + config);
}
if (isSecurityTypeSupported(securityType)) {
mMatchingWifiConfigs.put(securityType, config);
}
}
}
updateSecurityTypes();
updateTargetScanResultInfo();
notifyOnUpdated();
}
private boolean isSecurityTypeSupported(int security) {
switch (security) {
case SECURITY_TYPE_SAE:
return mIsWpa3SaeSupported;
case SECURITY_TYPE_EAP_WPA3_ENTERPRISE_192_BIT:
return mIsWpa3SuiteBSupported;
case SECURITY_TYPE_OWE:
return mIsEnhancedOpenSupported;
default:
return true;
}
}
@Override
protected synchronized void updateSecurityTypes() {
mTargetSecurityTypes.clear();
if (mWifiInfo != null) {
final int wifiInfoSecurity = mWifiInfo.getCurrentSecurityType();
if (wifiInfoSecurity != SECURITY_TYPE_UNKNOWN) {
mTargetSecurityTypes.add(mWifiInfo.getCurrentSecurityType());
}
}
Set<Integer> configSecurityTypes = mMatchingWifiConfigs.keySet();
if (mTargetSecurityTypes.isEmpty() && mKey.isTargetingNewNetworks()) {
// If we are targeting new networks for configuration, then we should select the
// security type of all visible scan results if we don't have any configs that
// can connect to them. This will let us configure this entry as a new network.
boolean configMatchesScans = false;
Set<Integer> scanSecurityTypes = mMatchingScanResults.keySet();
for (int configSecurity : configSecurityTypes) {
if (scanSecurityTypes.contains(configSecurity)) {
configMatchesScans = true;
break;
}
}
if (!configMatchesScans) {
mTargetSecurityTypes.addAll(scanSecurityTypes);
}
}
// Use security types of any configs we have
if (mTargetSecurityTypes.isEmpty()) {
mTargetSecurityTypes.addAll(configSecurityTypes);
}
// Default to the key security types. This shouldn't happen since we should always have
// scans or configs.
if (mTargetSecurityTypes.isEmpty()) {
mTargetSecurityTypes.addAll(mKey.getScanResultKey().getSecurityTypes());
}
// The target wifi config should match the security type we return in getSecurity(), since
// clients (QR code/DPP, modify network page) may expect them to match.
mTargetWifiConfig = mMatchingWifiConfigs.get(
getSingleSecurityTypeFromMultipleSecurityTypes(mTargetSecurityTypes));
// Collect target scan results in a set to remove duplicates when one scan matches multiple
// security types.
Set<ScanResult> targetScanResultSet = new ArraySet<>();
for (int security : mTargetSecurityTypes) {
if (mMatchingScanResults.containsKey(security)) {
targetScanResultSet.addAll(mMatchingScanResults.get(security));
}
}
mTargetScanResults.clear();
mTargetScanResults.addAll(targetScanResultSet);
}
/**
* Sets whether the suggested config for this entry is shareable to the user or not.
*/
@WorkerThread
synchronized void setUserShareable(boolean isUserShareable) {
mIsUserShareable = isUserShareable;
}
/**
* Returns whether the suggested config for this entry is shareable to the user or not.
*/
@WorkerThread
synchronized boolean isUserShareable() {
return mIsUserShareable;
}
@WorkerThread
protected synchronized boolean connectionInfoMatches(@NonNull WifiInfo wifiInfo,
@NonNull NetworkInfo networkInfo) {
if (wifiInfo.isPasspointAp() || wifiInfo.isOsuAp()) {
return false;
}
for (WifiConfiguration config : mMatchingWifiConfigs.values()) {
if (config.networkId == wifiInfo.getNetworkId()) {
return true;
}
}
return false;
}
@NonNull
static StandardWifiEntryKey ssidAndSecurityTypeToStandardWifiEntryKey(
@NonNull String ssid, int security) {
return ssidAndSecurityTypeToStandardWifiEntryKey(
ssid, security, false /* isTargetingNewNetworks */);
}
@NonNull
static StandardWifiEntryKey ssidAndSecurityTypeToStandardWifiEntryKey(
@NonNull String ssid, int security, boolean isTargetingNewNetworks) {
return new StandardWifiEntryKey(
new ScanResultKey(ssid, Collections.singletonList(security)),
isTargetingNewNetworks);
}
@Override
protected synchronized String getScanResultDescription() {
if (mTargetScanResults.size() == 0) {
return "";
}
final StringBuilder description = new StringBuilder();
description.append("[");
description.append(getScanResultDescription(MIN_FREQ_24GHZ, MAX_FREQ_24GHZ)).append(";");
description.append(getScanResultDescription(MIN_FREQ_5GHZ, MAX_FREQ_5GHZ)).append(";");
description.append(getScanResultDescription(MIN_FREQ_6GHZ, MAX_FREQ_6GHZ)).append(";");
description.append(getScanResultDescription(MIN_FREQ_60GHZ, MAX_FREQ_60GHZ));
description.append("]");
return description.toString();
}
private synchronized String getScanResultDescription(int minFrequency, int maxFrequency) {
final List<ScanResult> scanResults = mTargetScanResults.stream()
.filter(scanResult -> scanResult.frequency >= minFrequency
&& scanResult.frequency <= maxFrequency)
.sorted(Comparator.comparingInt(scanResult -> -1 * scanResult.level))
.collect(Collectors.toList());
final int scanResultCount = scanResults.size();
if (scanResultCount == 0) {
return "";
}
final StringBuilder description = new StringBuilder();
description.append("(").append(scanResultCount).append(")");
if (scanResultCount > MAX_VERBOSE_LOG_DISPLAY_SCANRESULT_COUNT) {
final int maxLavel = scanResults.stream()
.mapToInt(scanResult -> scanResult.level).max().getAsInt();
description.append("max=").append(maxLavel).append(",");
}
final long nowMs = SystemClock.elapsedRealtime();
scanResults.forEach(scanResult ->
description.append(getScanResultDescription(scanResult, nowMs)));
return description.toString();
}
// TODO(b/227622961): Remove the suppression once the linter recognizes BuildCompat.isAtLeastT()
@SuppressLint("NewApi")
private synchronized String getScanResultDescription(ScanResult scanResult, long nowMs) {
final StringBuilder description = new StringBuilder();
description.append(" \n{");
description.append(scanResult.BSSID);
if (mWifiInfo != null && scanResult.BSSID.equals(mWifiInfo.getBSSID())) {
description.append("*");
}
description.append("=").append(scanResult.frequency);
description.append(",").append(scanResult.level);
int wifiStandard = scanResult.getWifiStandard();
description.append(",").append(Utils.getStandardString(mContext, wifiStandard));
if (BuildCompat.isAtLeastT() && wifiStandard == ScanResult.WIFI_STANDARD_11BE) {
description.append(",mldMac=").append(scanResult.getApMldMacAddress());
description.append(",linkId=").append(scanResult.getApMloLinkId());
description.append(",affLinks=").append(scanResult.getAffiliatedMloLinks());
}
final int ageSeconds = (int) (nowMs - scanResult.timestamp / 1000) / 1000;
description.append(",").append(ageSeconds).append("s");
description.append("}");
return description.toString();
}
@Override
String getNetworkSelectionDescription() {
return Utils.getNetworkSelectionDescription(getWifiConfiguration());
}
// TODO(b/227622961): Remove the suppression once the linter recognizes BuildCompat.isAtLeastT()
@SuppressLint("NewApi")
void updateAdminRestrictions() {
if (!BuildCompat.isAtLeastT()) {
return;
}
if (mUserManager != null) {
mHasAddConfigUserRestriction = mUserManager.hasUserRestriction(
UserManager.DISALLOW_ADD_WIFI_CONFIG);
}
if (mDevicePolicyManager != null) {
//check minimum security level restriction
int adminMinimumSecurityLevel =
mDevicePolicyManager.getMinimumRequiredWifiSecurityLevel();
if (adminMinimumSecurityLevel != DevicePolicyManager.WIFI_SECURITY_OPEN) {
boolean securityRestrictionPassed = false;
for (int type : getSecurityTypes()) {
int securityLevel = Utils.convertSecurityTypeToDpmWifiSecurity(type);
// Skip unknown security type since security level cannot be determined.
// If all the security types are unknown when the minimum security level
// restriction is set, the device cannot connect to this network.
if (securityLevel == Utils.DPM_SECURITY_TYPE_UNKNOWN) continue;
if (adminMinimumSecurityLevel <= securityLevel) {
securityRestrictionPassed = true;
break;
}
}
if (!securityRestrictionPassed) {
mIsAdminRestricted = true;
return;
}
}
//check SSID restriction
WifiSsidPolicy policy = mDevicePolicyManager.getWifiSsidPolicy();
if (policy != null) {
int policyType = policy.getPolicyType();
Set<WifiSsid> ssids = policy.getSsids();
if (policyType == WifiSsidPolicy.WIFI_SSID_POLICY_TYPE_ALLOWLIST
&& !ssids.contains(
WifiSsid.fromBytes(getSsid().getBytes(StandardCharsets.UTF_8)))) {
mIsAdminRestricted = true;
return;
}
if (policyType == WifiSsidPolicy.WIFI_SSID_POLICY_TYPE_DENYLIST
&& ssids.contains(
WifiSsid.fromBytes(getSsid().getBytes(StandardCharsets.UTF_8)))) {
mIsAdminRestricted = true;
return;
}
}
}
mIsAdminRestricted = false;
}
private boolean hasAdminRestrictions() {
if ((mHasAddConfigUserRestriction && !(isSaved() || isSuggestion()))
|| mIsAdminRestricted) {
return true;
}
return false;
}
/**
* Class that identifies a unique StandardWifiEntry by the following identifiers
* 1) ScanResult key (SSID + grouped security types)
* 2) Suggestion profile key
* 3) Is network request or not
* 4) Should prioritize configuring a new network (i.e. target the security type of an
* in-range unsaved network, rather than a config that has no scans)
*/
static class StandardWifiEntryKey {
private static final String KEY_SCAN_RESULT_KEY = "SCAN_RESULT_KEY";
private static final String KEY_SUGGESTION_PROFILE_KEY = "SUGGESTION_PROFILE_KEY";
private static final String KEY_IS_NETWORK_REQUEST = "IS_NETWORK_REQUEST";
private static final String KEY_IS_TARGETING_NEW_NETWORKS = "IS_TARGETING_NEW_NETWORKS";
@NonNull private ScanResultKey mScanResultKey;
@Nullable private String mSuggestionProfileKey;
private boolean mIsNetworkRequest;
private boolean mIsTargetingNewNetworks = false;
/**
* Creates a StandardWifiEntryKey matching a ScanResultKey
*/
StandardWifiEntryKey(@NonNull ScanResultKey scanResultKey) {
this(scanResultKey, false /* isTargetingNewNetworks */);
}
/**
* Creates a StandardWifiEntryKey matching a ScanResultKey and sets whether the entry
* should target new networks or not.
*/
StandardWifiEntryKey(@NonNull ScanResultKey scanResultKey, boolean isTargetingNewNetworks) {
mScanResultKey = scanResultKey;
mIsTargetingNewNetworks = isTargetingNewNetworks;
}
/**
* Creates a StandardWifiEntryKey matching a WifiConfiguration
*/
StandardWifiEntryKey(@NonNull WifiConfiguration config) {
this(config, false /* isTargetingNewNetworks */);
}
/**
* Creates a StandardWifiEntryKey matching a WifiConfiguration and sets whether the entry
* should target new networks or not.
*/
StandardWifiEntryKey(@NonNull WifiConfiguration config, boolean isTargetingNewNetworks) {
mScanResultKey = new ScanResultKey(config);
if (config.fromWifiNetworkSuggestion) {
mSuggestionProfileKey = new StringJoiner(",")
.add(config.creatorName)
.add(String.valueOf(config.carrierId))
.add(String.valueOf(config.subscriptionId))
.toString();
} else if (config.fromWifiNetworkSpecifier) {
mIsNetworkRequest = true;
}
mIsTargetingNewNetworks = isTargetingNewNetworks;
}
/**
* Creates a StandardWifiEntryKey from its String representation.
*/
StandardWifiEntryKey(@NonNull String string) {
mScanResultKey = new ScanResultKey();
if (!string.startsWith(KEY_PREFIX)) {
Log.e(TAG, "String key does not start with key prefix!");
return;
}
try {
final JSONObject keyJson = new JSONObject(string.substring(KEY_PREFIX.length()));
if (keyJson.has(KEY_SCAN_RESULT_KEY)) {
mScanResultKey = new ScanResultKey(keyJson.getString(KEY_SCAN_RESULT_KEY));
}
if (keyJson.has(KEY_SUGGESTION_PROFILE_KEY)) {
mSuggestionProfileKey = keyJson.getString(KEY_SUGGESTION_PROFILE_KEY);
}
if (keyJson.has(KEY_IS_NETWORK_REQUEST)) {
mIsNetworkRequest = keyJson.getBoolean(KEY_IS_NETWORK_REQUEST);
}
if (keyJson.has(KEY_IS_TARGETING_NEW_NETWORKS)) {
mIsTargetingNewNetworks = keyJson.getBoolean(
KEY_IS_TARGETING_NEW_NETWORKS);
}
} catch (JSONException e) {
Log.e(TAG, "JSONException while converting StandardWifiEntryKey to string: " + e);
}
}
/**
* Returns the JSON String representation of this StandardWifiEntryKey.
*/
@Override
public String toString() {
final JSONObject keyJson = new JSONObject();
try {
if (mScanResultKey != null) {
keyJson.put(KEY_SCAN_RESULT_KEY, mScanResultKey.toString());
}
if (mSuggestionProfileKey != null) {
keyJson.put(KEY_SUGGESTION_PROFILE_KEY, mSuggestionProfileKey);
}
if (mIsNetworkRequest) {
keyJson.put(KEY_IS_NETWORK_REQUEST, mIsNetworkRequest);
}
if (mIsTargetingNewNetworks) {
keyJson.put(KEY_IS_TARGETING_NEW_NETWORKS, mIsTargetingNewNetworks);
}
} catch (JSONException e) {
Log.wtf(TAG, "JSONException while converting StandardWifiEntryKey to string: " + e);
}
return KEY_PREFIX + keyJson.toString();
}
/**
* Returns the ScanResultKey of this StandardWifiEntryKey to match against ScanResults
*/
@NonNull ScanResultKey getScanResultKey() {
return mScanResultKey;
}
@Nullable String getSuggestionProfileKey() {
return mSuggestionProfileKey;
}
boolean isNetworkRequest() {
return mIsNetworkRequest;
}
boolean isTargetingNewNetworks() {
return mIsTargetingNewNetworks;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StandardWifiEntryKey that = (StandardWifiEntryKey) o;
return Objects.equals(mScanResultKey, that.mScanResultKey)
&& TextUtils.equals(mSuggestionProfileKey, that.mSuggestionProfileKey)
&& mIsNetworkRequest == that.mIsNetworkRequest;
}
@Override
public int hashCode() {
return Objects.hash(mScanResultKey, mSuggestionProfileKey, mIsNetworkRequest);
}
}
/**
* Class for matching ScanResults to StandardWifiEntry by SSID and security type grouping.
*/
static class ScanResultKey {
private static final String KEY_SSID = "SSID";
private static final String KEY_SECURITY_TYPES = "SECURITY_TYPES";
@Nullable private String mSsid;
@NonNull private Set<Integer> mSecurityTypes = new ArraySet<>();
ScanResultKey() {
}
ScanResultKey(@Nullable String ssid, List<Integer> securityTypes) {
mSsid = ssid;
for (int security : securityTypes) {
// Add any security types that merge to the same WifiEntry
switch (security) {
case SECURITY_TYPE_PASSPOINT_R1_R2:
case SECURITY_TYPE_PASSPOINT_R3:
// Filter out Passpoint security type from key.
continue;
// Group OPEN and OWE networks together
case SECURITY_TYPE_OPEN:
mSecurityTypes.add(SECURITY_TYPE_OWE);
break;
case SECURITY_TYPE_OWE:
mSecurityTypes.add(SECURITY_TYPE_OPEN);
break;
// Group PSK and SAE networks together
case SECURITY_TYPE_PSK:
mSecurityTypes.add(SECURITY_TYPE_SAE);
break;
case SECURITY_TYPE_SAE:
mSecurityTypes.add(SECURITY_TYPE_PSK);
break;
// Group EAP and EAP_WPA3_ENTERPRISE networks together
case SECURITY_TYPE_EAP:
mSecurityTypes.add(SECURITY_TYPE_EAP_WPA3_ENTERPRISE);
break;
case SECURITY_TYPE_EAP_WPA3_ENTERPRISE:
mSecurityTypes.add(SECURITY_TYPE_EAP);
break;
}
mSecurityTypes.add(security);
}
}
/**
* Creates a ScanResultKey from a ScanResult's SSID and security type grouping.
* @param scanResult
*/
ScanResultKey(@NonNull ScanResult scanResult) {
this(scanResult.SSID, getSecurityTypesFromScanResult(scanResult));
}
/**
* Creates a ScanResultKey from a WifiConfiguration's SSID and security type grouping.
*/
ScanResultKey(@NonNull WifiConfiguration wifiConfiguration) {
this(sanitizeSsid(wifiConfiguration.SSID),
getSecurityTypesFromWifiConfiguration(wifiConfiguration));
}
/**
* Creates a ScanResultKey from its String representation.
*/
ScanResultKey(@NonNull String string) {
try {
final JSONObject keyJson = new JSONObject(string);
mSsid = keyJson.getString(KEY_SSID);
final JSONArray securityTypesJson =
keyJson.getJSONArray(KEY_SECURITY_TYPES);
for (int i = 0; i < securityTypesJson.length(); i++) {
mSecurityTypes.add(securityTypesJson.getInt(i));
}
} catch (JSONException e) {
Log.wtf(TAG, "JSONException while constructing ScanResultKey from string: " + e);
}
}
/**
* Returns the JSON String representation of this ScanResultEntry.
*/
@Override
public String toString() {
final JSONObject keyJson = new JSONObject();
try {
if (mSsid != null) {
keyJson.put(KEY_SSID, mSsid);
}
if (!mSecurityTypes.isEmpty()) {
final JSONArray securityTypesJson = new JSONArray();
for (int security : mSecurityTypes) {
securityTypesJson.put(security);
}
keyJson.put(KEY_SECURITY_TYPES, securityTypesJson);
}
} catch (JSONException e) {
Log.e(TAG, "JSONException while converting ScanResultKey to string: " + e);
}
return keyJson.toString();
}
@Nullable String getSsid() {
return mSsid;
}
@NonNull Set<Integer> getSecurityTypes() {
return mSecurityTypes;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ScanResultKey that = (ScanResultKey) o;
return TextUtils.equals(mSsid, that.mSsid)
&& mSecurityTypes.equals(that.mSecurityTypes);
}
@Override
public int hashCode() {
return Objects.hash(mSsid, mSecurityTypes);
}
}
}