blob: d3e20bef27687ef4b5c70e8de788c4b123a6b3ca [file] [log] [blame]
/*
* Copyright (C) 2016 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.server.wifi;
import static android.net.wifi.WifiManager.WIFI_FEATURE_TRUST_ON_FIRST_USE;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.net.DhcpOption;
import android.net.IpConfiguration;
import android.net.MacAddress;
import android.net.ProxyInfo;
import android.net.StaticIpConfiguration;
import android.net.wifi.ScanResult;
import android.net.wifi.SecurityParams;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiConfiguration.NetworkSelectionStatus;
import android.net.wifi.WifiEnterpriseConfig;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiScanner;
import android.net.wifi.WifiSsid;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.LocalLog;
import android.util.Log;
import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.MacAddressUtils;
import com.android.server.wifi.hotspot2.PasspointManager;
import com.android.server.wifi.proto.nano.WifiMetricsProto.UserActionEvent;
import com.android.server.wifi.util.CertificateSubjectInfo;
import com.android.server.wifi.util.LruConnectionTracker;
import com.android.server.wifi.util.MissingCounterTimerLockList;
import com.android.server.wifi.util.WifiPermissionsUtil;
import com.android.wifi.resources.R;
import org.xmlpull.v1.XmlPullParserException;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* This class provides the APIs to manage configured Wi-Fi networks.
* It deals with the following:
* - Maintaining a list of configured networks for quick access.
* - Persisting the configurations to store when required.
* - Supporting WifiManager Public API calls:
* > addOrUpdateNetwork()
* > removeNetwork()
* > enableNetwork()
* > disableNetwork()
* - Handle user switching on multi-user devices.
*
* All network configurations retrieved from this class are copies of the original configuration
* stored in the internal database. So, any updates to the retrieved configuration object are
* meaningless and will not be reflected in the original database.
* This is done on purpose to ensure that only WifiConfigManager can modify configurations stored
* in the internal database. Any configuration updates should be triggered with appropriate helper
* methods of this class using the configuration's unique networkId.
*
* NOTE: These API's are not thread safe and should only be used from the main Wifi thread.
*/
public class WifiConfigManager {
/**
* String used to mask passwords to public interface.
*/
@VisibleForTesting
public static final String PASSWORD_MASK = "*";
/**
* Interface for other modules to listen to the network updated events.
* Note: Credentials are masked to avoid accidentally sending credentials outside the stack.
* Use WifiConfigManager#getConfiguredNetworkWithPassword() to retrieve credentials.
*/
public interface OnNetworkUpdateListener {
/**
* Invoked on network being added.
*/
default void onNetworkAdded(@NonNull WifiConfiguration config) { };
/**
* Invoked on network being enabled.
*/
default void onNetworkEnabled(@NonNull WifiConfiguration config) { };
/**
* Invoked on network being permanently disabled.
*/
default void onNetworkPermanentlyDisabled(@NonNull WifiConfiguration config,
int disableReason) { };
/**
* Invoked on network being removed.
*/
default void onNetworkRemoved(@NonNull WifiConfiguration config) { };
/**
* Invoked on network being temporarily disabled.
*/
default void onNetworkTemporarilyDisabled(@NonNull WifiConfiguration config,
int disableReason) { };
/**
* Invoked on network being updated.
*
* @param newConfig Updated WifiConfiguration object.
* @param oldConfig Prev WifiConfiguration object.
*/
default void onNetworkUpdated(
@NonNull WifiConfiguration newConfig, @NonNull WifiConfiguration oldConfig) { };
/**
* Invoked when user connect choice is set.
* @param networks List of network profiles to set user connect choice.
* @param choiceKey Network key {@link WifiConfiguration#getProfileKey()}
* corresponding to the network which the user chose.
* @param rssi the signal strength of the user selected network
*/
default void onConnectChoiceSet(@NonNull List<WifiConfiguration> networks,
String choiceKey, int rssi) { }
/**
* Invoked when user connect choice is removed.
* @param choiceKey The network profile key of the user connect choice that was removed.
*/
default void onConnectChoiceRemoved(String choiceKey){ }
/**
* Invoke when security params changed, especially when NetworkTransitionDisable event
* received
* @param oldConfig The original WifiConfiguration
* @param securityParams the updated securityParams
*/
default void onSecurityParamsUpdate(@NonNull WifiConfiguration oldConfig,
List<SecurityParams> securityParams) { }
}
/**
* Max size of scan details to cache in {@link #mScanDetailCaches}.
*/
@VisibleForTesting
public static final int SCAN_CACHE_ENTRIES_MAX_SIZE = 192;
/**
* Once the size of the scan details in the cache {@link #mScanDetailCaches} exceeds
* {@link #SCAN_CACHE_ENTRIES_MAX_SIZE}, trim it down to this value so that we have some
* buffer time before the next eviction.
*/
@VisibleForTesting
public static final int SCAN_CACHE_ENTRIES_TRIM_SIZE = 128;
/**
* Link networks only if they have less than this number of scan cache entries.
*/
@VisibleForTesting
public static final int LINK_CONFIGURATION_MAX_SCAN_CACHE_ENTRIES = 6;
/**
* Link networks only if the bssid in scan results for the networks match in the first
* 16 ASCII chars in the bssid string. For example = "af:de:56;34:15:7"
*/
@VisibleForTesting
public static final int LINK_CONFIGURATION_BSSID_MATCH_LENGTH = 16;
/**
* Log tag for this class.
*/
private static final String TAG = "WifiConfigManager";
/**
* Maximum age of scan results that can be used for averaging out RSSI value.
*/
private static final int SCAN_RESULT_MAXIMUM_AGE_MS = 40000;
/**
* Enforce a minimum time to wait after the last disconnect to generate a new randomized MAC,
* since IPv6 networks don't provide the DHCP lease duration.
* 4 hours.
*/
@VisibleForTesting
protected static final long NON_PERSISTENT_MAC_WAIT_AFTER_DISCONNECT_MS = 4 * 60 * 60 * 1000;
@VisibleForTesting
protected static final long NON_PERSISTENT_MAC_REFRESH_MS_MIN = 30 * 60 * 1000; // 30 minutes
@VisibleForTesting
protected static final long NON_PERSISTENT_MAC_REFRESH_MS_MAX = 24 * 60 * 60 * 1000; // 24 hours
private static final MacAddress DEFAULT_MAC_ADDRESS =
MacAddress.fromString(WifiInfo.DEFAULT_MAC_ADDRESS);
private static final String VRRP_MAC_ADDRESS_PREFIX = "00:00:5E:00:01";
/**
* Expiration timeout for user disconnect network. (1 hour)
*/
@VisibleForTesting
public static final long USER_DISCONNECT_NETWORK_BLOCK_EXPIRY_MS = (long) 1000 * 60 * 60;
@VisibleForTesting
public static final int SCAN_RESULT_MISSING_COUNT_THRESHOLD = 1;
@VisibleForTesting
protected static final String NON_PERSISTENT_MAC_RANDOMIZATION_FEATURE_FORCE_ENABLE_FLAG =
"non_persistent_mac_randomization_force_enabled";
private static final int NON_CARRIER_MERGED_NETWORKS_SCAN_CACHE_QUERY_DURATION_MS =
10 * 60 * 1000; // 10 minutes
/**
* General sorting algorithm of all networks for scanning purposes:
* Place the configurations in ascending order of their AgeIndex. AgeIndex is based on most
* recently connected order. The lower the more recently connected.
* If networks have the same AgeIndex, place the configurations with
* |lastSeenInQualifiedNetworkSelection| set first.
*/
private final WifiConfigurationUtil.WifiConfigurationComparator mScanListComparator =
new WifiConfigurationUtil.WifiConfigurationComparator() {
@Override
public int compareNetworksWithSameStatus(WifiConfiguration a, WifiConfiguration b) {
int indexA = mLruConnectionTracker.getAgeIndexOfNetwork(a);
int indexB = mLruConnectionTracker.getAgeIndexOfNetwork(b);
if (indexA != indexB) {
return Integer.compare(indexA, indexB);
} else {
boolean isConfigALastSeen =
a.getNetworkSelectionStatus()
.getSeenInLastQualifiedNetworkSelection();
boolean isConfigBLastSeen =
b.getNetworkSelectionStatus()
.getSeenInLastQualifiedNetworkSelection();
return Boolean.compare(isConfigBLastSeen, isConfigALastSeen);
}
}
};
/**
* List of external dependencies for WifiConfigManager.
*/
private final Context mContext;
private final WifiInjector mWifiInjector;
private final Clock mClock;
private final UserManager mUserManager;
private final BackupManagerProxy mBackupManagerProxy;
private final WifiKeyStore mWifiKeyStore;
private final WifiConfigStore mWifiConfigStore;
private final WifiPermissionsUtil mWifiPermissionsUtil;
private final MacAddressUtil mMacAddressUtil;
private final WifiMetrics mWifiMetrics;
private final WifiBlocklistMonitor mWifiBlocklistMonitor;
private final WifiLastResortWatchdog mWifiLastResortWatchdog;
private final WifiCarrierInfoManager mWifiCarrierInfoManager;
private final WifiScoreCard mWifiScoreCard;
// Keep order of network connection.
private final LruConnectionTracker mLruConnectionTracker;
private final BuildProperties mBuildProperties;
/**
* Local log used for debugging any WifiConfigManager issues.
*/
private final LocalLog mLocalLog;
/**
* Map of configured networks with network id as the key.
*/
private final ConfigurationMap mConfiguredNetworks;
/**
* Stores a map of NetworkId to ScanDetailCache.
*/
private final Map<Integer, ScanDetailCache> mScanDetailCaches;
/**
* Framework keeps a list of networks that where temporarily disabled by user,
* framework knows not to autoconnect again even if the app/scorer recommends it.
* Network will be based on FQDN for passpoint and SSID for non-passpoint.
* List will be deleted when Wifi turn off, device restart or network settings reset.
* Also when user manfully select to connect network will unblock that network.
*/
private final MissingCounterTimerLockList<String> mUserTemporarilyDisabledList;
private final NonCarrierMergedNetworksStatusTracker mNonCarrierMergedNetworksStatusTracker;
/**
* Framework keeps a mapping from configKey to the randomized MAC address so that
* when a user forgets a network and thne adds it back, the same randomized MAC address
* will get used.
*/
private final Map<String, String> mRandomizedMacAddressMapping;
/**
* Store the network update listeners.
*/
private final Set<OnNetworkUpdateListener> mListeners;
private final FrameworkFacade mFrameworkFacade;
private final DeviceConfigFacade mDeviceConfigFacade;
/**
* Verbose logging flag. Toggled by developer options.
*/
private boolean mVerboseLoggingEnabled = false;
/**
* Current logged in user ID.
*/
private int mCurrentUserId = UserHandle.SYSTEM.getIdentifier();
/**
* Flag to indicate that the new user's store has not yet been read since user switch.
* Initialize this flag to |true| to trigger a read on the first user unlock after
* bootup.
*/
private boolean mPendingUnlockStoreRead = true;
/**
* Flag to indicate if we have performed a read from store at all. This is used to gate
* any user unlock/switch operations until we read the store (Will happen if wifi is disabled
* when user updates from N to O).
*/
private boolean mPendingStoreRead = true;
/**
* Flag to indicate if the user unlock was deferred until the store load occurs.
*/
private boolean mDeferredUserUnlockRead = false;
/**
* This is keeping track of the next network ID to be assigned. Any new networks will be
* assigned |mNextNetworkId| as network ID.
*/
private int mNextNetworkId = 0;
/**
* This is used to remember which network was selected successfully last by an app. This is set
* when an app invokes {@link #enableNetwork(int, boolean, int)} with |disableOthers| flag set.
* This is the only way for an app to request connection to a specific network using the
* {@link WifiManager} API's.
*/
private int mLastSelectedNetworkId = WifiConfiguration.INVALID_NETWORK_ID;
private long mLastSelectedTimeStamp =
WifiConfiguration.NetworkSelectionStatus.INVALID_NETWORK_SELECTION_DISABLE_TIMESTAMP;
// Store data for network list and deleted ephemeral SSID list. Used for serializing
// parsing data to/from the config store.
private final NetworkListSharedStoreData mNetworkListSharedStoreData;
private final NetworkListUserStoreData mNetworkListUserStoreData;
private final RandomizedMacStoreData mRandomizedMacStoreData;
private static class NetworkIdentifier {
private WifiSsid mSsid;
private byte[] mOui;
NetworkIdentifier(WifiSsid ssid, byte[] oui) {
mSsid = ssid;
mOui = oui;
}
@Override
public int hashCode() {
return Objects.hash(mSsid, Arrays.hashCode(mOui));
}
@Override
public boolean equals(Object otherObj) {
if (this == otherObj) {
return true;
} else if (!(otherObj instanceof NetworkIdentifier)) {
return false;
}
NetworkIdentifier other = (NetworkIdentifier) otherObj;
return Objects.equals(mSsid, other.mSsid) && Arrays.equals(mOui, other.mOui);
}
}
private final Map<NetworkIdentifier, List<DhcpOption>> mCustomDhcpOptions = new HashMap<>();
/**
* Create new instance of WifiConfigManager.
*/
WifiConfigManager(
Context context,
WifiKeyStore wifiKeyStore,
WifiConfigStore wifiConfigStore,
NetworkListSharedStoreData networkListSharedStoreData,
NetworkListUserStoreData networkListUserStoreData,
RandomizedMacStoreData randomizedMacStoreData,
LruConnectionTracker lruConnectionTracker,
WifiInjector wifiInjector) {
mContext = context;
mWifiInjector = wifiInjector;
mClock = wifiInjector.getClock();
mUserManager = wifiInjector.getUserManager();
mWifiCarrierInfoManager = wifiInjector.getWifiCarrierInfoManager();
mWifiMetrics = wifiInjector.getWifiMetrics();
mWifiBlocklistMonitor = wifiInjector.getWifiBlocklistMonitor();
mWifiLastResortWatchdog = wifiInjector.getWifiLastResortWatchdog();
mWifiScoreCard = wifiInjector.getWifiScoreCard();
mWifiPermissionsUtil = wifiInjector.getWifiPermissionsUtil();
mFrameworkFacade = wifiInjector.getFrameworkFacade();
mDeviceConfigFacade = wifiInjector.getDeviceConfigFacade();
mMacAddressUtil = wifiInjector.getMacAddressUtil();
mBuildProperties = wifiInjector.getBuildProperties();
mBackupManagerProxy = new BackupManagerProxy();
mWifiKeyStore = wifiKeyStore;
mWifiConfigStore = wifiConfigStore;
mConfiguredNetworks = new ConfigurationMap(mWifiPermissionsUtil);
mScanDetailCaches = new HashMap<>(16, 0.75f);
mUserTemporarilyDisabledList =
new MissingCounterTimerLockList<>(SCAN_RESULT_MISSING_COUNT_THRESHOLD, mClock);
mNonCarrierMergedNetworksStatusTracker = new NonCarrierMergedNetworksStatusTracker(mClock);
mRandomizedMacAddressMapping = new HashMap<>();
mListeners = new ArraySet<>();
// Register store data for network list and deleted ephemeral SSIDs.
mNetworkListSharedStoreData = networkListSharedStoreData;
mNetworkListUserStoreData = networkListUserStoreData;
mRandomizedMacStoreData = randomizedMacStoreData;
mWifiConfigStore.registerStoreData(mNetworkListSharedStoreData);
mWifiConfigStore.registerStoreData(mNetworkListUserStoreData);
mWifiConfigStore.registerStoreData(mRandomizedMacStoreData);
mLocalLog = new LocalLog(
context.getSystemService(ActivityManager.class).isLowRamDevice() ? 128 : 256);
mLruConnectionTracker = lruConnectionTracker;
}
/**
* Update the cellular data availability of the default data SIM.
*/
public void onCellularConnectivityChanged(@WifiDataStall.CellularDataStatusCode int status) {
localLog("onCellularConnectivityChanged:" + status);
if (status == WifiDataStall.CELLULAR_DATA_NOT_AVAILABLE) {
stopRestrictingAutoJoinToSubscriptionId();
}
}
/**
* Determine if the framework should perform non-persistent MAC randomization when connecting
* to the SSID or FQDN in the input WifiConfiguration.
* @param config
* @return
*/
public boolean shouldUseNonPersistentRandomization(WifiConfiguration config) {
// If this is the secondary STA for multi internet for DBS AP, use non persistent mac
// randomization, as the primary and secondary STAs could connect to the same SSID.
if (isMacRandomizationSupported() && config.dbsSecondaryInternet) {
return true;
}
if (!isMacRandomizationSupported()
|| config.macRandomizationSetting == WifiConfiguration.RANDOMIZATION_NONE) {
return false;
}
// Use non-persistent randomization if it's forced on by dev option
if (mFrameworkFacade.getIntegerSetting(mContext,
NON_PERSISTENT_MAC_RANDOMIZATION_FEATURE_FORCE_ENABLE_FLAG, 0) == 1) {
return true;
}
// use non-persistent or persistent randomization if configured to do so.
if (config.macRandomizationSetting == WifiConfiguration.RANDOMIZATION_NON_PERSISTENT) {
return true;
}
if (config.macRandomizationSetting == WifiConfiguration.RANDOMIZATION_PERSISTENT) {
return false;
}
// otherwise the wifi frameworks should decide automatically
if (config.getIpConfiguration().getIpAssignment() == IpConfiguration.IpAssignment.STATIC) {
return false;
}
if (config.isOpenNetwork() && shouldEnableNonPersistentRandomizationOnOpenNetwork(config)) {
return true;
}
if (config.isPasspoint()) {
return isNetworkOptInForNonPersistentRandomization(config.FQDN);
} else {
return isNetworkOptInForNonPersistentRandomization(config.SSID);
}
}
private boolean shouldEnableNonPersistentRandomizationOnOpenNetwork(WifiConfiguration config) {
if (!mDeviceConfigFacade.allowNonPersistentMacRandomizationOnOpenSsids()
&& !mContext.getResources().getBoolean(
R.bool.config_wifiAllowNonPersistentMacRandomizationOnOpenSsids)) {
return false;
}
return config.getNetworkSelectionStatus().hasEverConnected()
&& config.getNetworkSelectionStatus().hasNeverDetectedCaptivePortal();
}
private boolean isNetworkOptInForNonPersistentRandomization(String ssidOrFqdn) {
Set<String> perDeviceSsidBlocklist = new ArraySet<>(mContext.getResources().getStringArray(
R.array.config_wifi_non_persistent_randomization_ssid_blocklist));
if (mDeviceConfigFacade.getNonPersistentMacRandomizationSsidBlocklist().contains(ssidOrFqdn)
|| perDeviceSsidBlocklist.contains(ssidOrFqdn)) {
return false;
}
Set<String> perDeviceSsidAllowlist = new ArraySet<>(mContext.getResources().getStringArray(
R.array.config_wifi_non_persistent_randomization_ssid_allowlist));
return mDeviceConfigFacade.getNonPersistentMacRandomizationSsidAllowlist()
.contains(ssidOrFqdn) || perDeviceSsidAllowlist.contains(ssidOrFqdn);
}
@VisibleForTesting
protected int getRandomizedMacAddressMappingSize() {
return mRandomizedMacAddressMapping.size();
}
/**
* The persistent randomized MAC address is locally generated for each SSID and does not
* change until factory reset of the device. In the initial Q release the per-SSID randomized
* MAC is saved on the device, but in an update the storing of randomized MAC is removed.
* Instead, the randomized MAC is calculated directly from the SSID and a on device secret.
* For backward compatibility, this method first checks the device storage for saved
* randomized MAC. If it is not found or the saved MAC is invalid then it will calculate the
* randomized MAC directly.
*
* In the future as devices launched on Q no longer get supported, this method should get
* simplified to return the calculated MAC address directly.
* @param config the WifiConfiguration to obtain MAC address for.
* @return persistent MAC address for this WifiConfiguration
*/
@VisibleForTesting
public MacAddress getPersistentMacAddress(WifiConfiguration config) {
// mRandomizedMacAddressMapping had been the location to save randomized MAC addresses.
String persistentMacString = mRandomizedMacAddressMapping.get(
config.getNetworkKey());
// Use the MAC address stored in the storage if it exists and is valid. Otherwise
// use the MAC address calculated from a hash function as the persistent MAC.
if (persistentMacString != null) {
try {
return MacAddress.fromString(persistentMacString);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Error creating randomized MAC address from stored value.");
mRandomizedMacAddressMapping.remove(config.getNetworkKey());
}
}
MacAddress result = mMacAddressUtil.calculatePersistentMac(config.getNetworkKey(),
mMacAddressUtil.obtainMacRandHashFunction(Process.WIFI_UID));
if (result == null) {
result = mMacAddressUtil.calculatePersistentMac(config.getNetworkKey(),
mMacAddressUtil.obtainMacRandHashFunction(Process.WIFI_UID));
}
if (result == null) {
Log.wtf(TAG, "Failed to generate MAC address from KeyStore even after retrying. "
+ "Using locally generated MAC address instead.");
result = config.getRandomizedMacAddress();
if (DEFAULT_MAC_ADDRESS.equals(result)) {
result = MacAddressUtils.createRandomUnicastAddress();
}
}
return result;
}
/**
* Sets the randomized MAC expiration time based on the DHCP lease duration.
* This should be called every time DHCP lease information is obtained.
*/
public void updateRandomizedMacExpireTime(WifiConfiguration config, long dhcpLeaseSeconds) {
WifiConfiguration internalConfig = getInternalConfiguredNetwork(config.networkId);
if (internalConfig == null) {
return;
}
long expireDurationMs = (dhcpLeaseSeconds & 0xffffffffL) * 1000;
expireDurationMs = Math.max(NON_PERSISTENT_MAC_REFRESH_MS_MIN, expireDurationMs);
expireDurationMs = Math.min(NON_PERSISTENT_MAC_REFRESH_MS_MAX, expireDurationMs);
internalConfig.randomizedMacExpirationTimeMs = mClock.getWallClockMillis()
+ expireDurationMs;
}
private void setRandomizedMacAddress(WifiConfiguration config, MacAddress mac) {
config.setRandomizedMacAddress(mac);
config.randomizedMacLastModifiedTimeMs = mClock.getWallClockMillis();
}
/**
* Obtain the persistent MAC address by first reading from an internal database. If non exists
* then calculate the persistent MAC using HMAC-SHA256.
* Finally set the randomized MAC of the configuration to the randomized MAC obtained.
* @param config the WifiConfiguration to make the update
* @return the persistent MacAddress or null if the operation is unsuccessful
*/
private MacAddress setRandomizedMacToPersistentMac(WifiConfiguration config) {
MacAddress persistentMac = getPersistentMacAddress(config);
if (persistentMac == null || persistentMac.equals(config.getRandomizedMacAddress())) {
return persistentMac;
}
WifiConfiguration internalConfig = getInternalConfiguredNetwork(config.networkId);
setRandomizedMacAddress(internalConfig, persistentMac);
return persistentMac;
}
/**
* This method is called before connecting to a network that has non-persistent randomization
* enabled, and will re-randomize the MAC address if needed.
* @param config the WifiConfiguration to make the update
* @return the updated MacAddress
*/
private MacAddress updateRandomizedMacIfNeeded(WifiConfiguration config) {
boolean shouldUpdateMac = config.randomizedMacExpirationTimeMs
< mClock.getWallClockMillis() || mClock.getWallClockMillis()
- config.randomizedMacLastModifiedTimeMs >= NON_PERSISTENT_MAC_REFRESH_MS_MAX;
if (!shouldUpdateMac) {
return config.getRandomizedMacAddress();
}
WifiConfiguration internalConfig = getInternalConfiguredNetwork(config.networkId);
setRandomizedMacAddress(internalConfig, MacAddressUtils.createRandomUnicastAddress());
return internalConfig.getRandomizedMacAddress();
}
/**
* Returns the randomized MAC address that should be used for this WifiConfiguration.
* This API may return a randomized MAC different from the persistent randomized MAC if
* the WifiConfiguration is configured for non-persistent MAC randomization.
* @param config
* @return MacAddress
*/
public MacAddress getRandomizedMacAndUpdateIfNeeded(WifiConfiguration config) {
MacAddress mac = shouldUseNonPersistentRandomization(config)
? updateRandomizedMacIfNeeded(config)
: setRandomizedMacToPersistentMac(config);
return mac;
}
/**
* Enable/disable verbose logging in WifiConfigManager & its helper classes.
*/
public void enableVerboseLogging(boolean verbose) {
mVerboseLoggingEnabled = verbose;
mWifiConfigStore.enableVerboseLogging(mVerboseLoggingEnabled);
mWifiKeyStore.enableVerboseLogging(mVerboseLoggingEnabled);
mWifiBlocklistMonitor.enableVerboseLogging(mVerboseLoggingEnabled);
}
/**
* Helper method to mask all passwords/keys from the provided WifiConfiguration object. This
* is needed when the network configurations are being requested via the public WifiManager
* API's.
* This currently masks the following elements: psk, wepKeys & enterprise config password.
*/
private void maskPasswordsInWifiConfiguration(WifiConfiguration configuration) {
if (!TextUtils.isEmpty(configuration.preSharedKey)) {
configuration.preSharedKey = PASSWORD_MASK;
}
if (configuration.wepKeys != null) {
for (int i = 0; i < configuration.wepKeys.length; i++) {
if (!TextUtils.isEmpty(configuration.wepKeys[i])) {
configuration.wepKeys[i] = PASSWORD_MASK;
}
}
}
if (configuration.enterpriseConfig != null && !TextUtils.isEmpty(
configuration.enterpriseConfig.getPassword())) {
configuration.enterpriseConfig.setPassword(PASSWORD_MASK);
}
}
/**
* Helper method to mask randomized MAC address from the provided WifiConfiguration Object.
* This is needed when the network configurations are being requested via the public
* WifiManager API's. This method puts "02:00:00:00:00:00" as the MAC address.
* @param configuration WifiConfiguration to hide the MAC address
*/
private void maskRandomizedMacAddressInWifiConfiguration(WifiConfiguration configuration) {
setRandomizedMacAddress(configuration, DEFAULT_MAC_ADDRESS);
}
/**
* Helper method to create a copy of the provided internal WifiConfiguration object to be
* passed to external modules.
*
* @param configuration provided WifiConfiguration object.
* @param maskPasswords Mask passwords or not.
* @param targetUid Target UID for MAC address reading: -1 = mask all, 0 = mask none, >0 =
* mask all but the targetUid (carrier app).
* @return Copy of the WifiConfiguration object, or a default WifiConfiguration if the input
* is null.
*/
private @NonNull WifiConfiguration createExternalWifiConfiguration(
@NonNull WifiConfiguration configuration, boolean maskPasswords, int targetUid) {
if (configuration == null) {
Log.wtf(TAG, "Unexpected null configuration in createExternalWifiConfiguration");
return new WifiConfiguration();
}
WifiConfiguration network = new WifiConfiguration(configuration);
if (maskPasswords) {
maskPasswordsInWifiConfiguration(network);
}
if (targetUid != Process.WIFI_UID && targetUid != Process.SYSTEM_UID
&& targetUid != configuration.creatorUid) {
maskRandomizedMacAddressInWifiConfiguration(network);
}
if (!isMacRandomizationSupported()) {
network.macRandomizationSetting = WifiConfiguration.RANDOMIZATION_NONE;
}
return network;
}
/**
* Returns whether MAC randomization is supported on this device.
* @param config
* @return
*/
private boolean isMacRandomizationSupported() {
return mContext.getResources().getBoolean(
R.bool.config_wifi_connected_mac_randomization_supported);
}
/**
* Fetch the list of currently configured networks maintained in WifiConfigManager.
*
* This retrieves a copy of the internal configurations maintained by WifiConfigManager and
* should be used for any public interfaces.
*
* @param savedOnly Retrieve only saved networks.
* @param maskPasswords Mask passwords or not.
* @param targetUid Target UID for MAC address reading: -1 (Invalid UID) = mask all,
* WIFI||SYSTEM = mask none, <other> = mask all but the targetUid (carrier
* app).
* @return List of WifiConfiguration objects representing the networks.
*/
private List<WifiConfiguration> getConfiguredNetworks(
boolean savedOnly, boolean maskPasswords, int targetUid) {
List<WifiConfiguration> networks = new ArrayList<>();
for (WifiConfiguration config : getInternalConfiguredNetworks()) {
if (savedOnly && (config.ephemeral || config.isPasspoint())) {
continue;
}
networks.add(createExternalWifiConfiguration(config, maskPasswords, targetUid));
}
return networks;
}
/**
* Retrieves the list of all configured networks with passwords masked.
*
* @return List of WifiConfiguration objects representing the networks.
*/
public List<WifiConfiguration> getConfiguredNetworks() {
return getConfiguredNetworks(false, true, Process.WIFI_UID);
}
/**
* Retrieves the list of all configured networks with the passwords in plaintext.
*
* WARNING: Don't use this to pass network configurations to external apps. Should only be
* sent to system apps/wifi stack, when there is a need for passwords in plaintext.
* TODO: Need to understand the current use case of this API.
*
* @return List of WifiConfiguration objects representing the networks.
*/
public List<WifiConfiguration> getConfiguredNetworksWithPasswords() {
return getConfiguredNetworks(false, false, Process.WIFI_UID);
}
/**
* Retrieves the list of all configured networks with the passwords masked.
*
* @return List of WifiConfiguration objects representing the networks.
*/
public List<WifiConfiguration> getSavedNetworks(int targetUid) {
return getConfiguredNetworks(true, true, targetUid);
}
/**
* Retrieves the configured network corresponding to the provided networkId with password
* masked.
*
* @param networkId networkId of the requested network.
* @return WifiConfiguration object if found, null otherwise.
*/
public @Nullable WifiConfiguration getConfiguredNetwork(int networkId) {
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config == null) {
return null;
}
// Create a new configuration object with the passwords masked to send out to the external
// world.
return createExternalWifiConfiguration(config, true, Process.WIFI_UID);
}
/**
* Retrieves the configured network corresponding to the provided config key with password
* masked.
*
* @param configKey configKey of the requested network.
* @return WifiConfiguration object if found, null otherwise.
*/
public @Nullable WifiConfiguration getConfiguredNetwork(String configKey) {
WifiConfiguration config = getInternalConfiguredNetwork(configKey);
if (config == null) {
return null;
}
// Create a new configuration object with the passwords masked to send out to the external
// world.
return createExternalWifiConfiguration(config, true, Process.WIFI_UID);
}
/**
* Retrieves the configured network corresponding to the provided networkId with password
* in plaintext.
*
* WARNING: Don't use this to pass network configurations to external apps. Should only be
* sent to system apps/wifi stack, when there is a need for passwords in plaintext.
*
* @param networkId networkId of the requested network.
* @return WifiConfiguration object if found, null otherwise.
*/
public @Nullable WifiConfiguration getConfiguredNetworkWithPassword(int networkId) {
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config == null) {
return null;
}
// Create a new configuration object without the passwords masked to send out to the
// external world.
return createExternalWifiConfiguration(config, false, Process.WIFI_UID);
}
/**
* Retrieves the configured network corresponding to the provided networkId
* without any masking.
*
* WARNING: Don't use this to pass network configurations except in the wifi stack, when
* there is a need for passwords and randomized MAC address.
*
* @param networkId networkId of the requested network.
* @return Copy of WifiConfiguration object if found, null otherwise.
*/
public @Nullable WifiConfiguration getConfiguredNetworkWithoutMasking(int networkId) {
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config == null) {
return null;
}
return new WifiConfiguration(config);
}
/**
* Helper method to retrieve all the internal WifiConfiguration objects corresponding to all
* the networks in our database.
*/
private Collection<WifiConfiguration> getInternalConfiguredNetworks() {
return mConfiguredNetworks.valuesForCurrentUser();
}
private @Nullable WifiConfiguration getInternalConfiguredNetworkByUpgradableType(
@NonNull WifiConfiguration config) {
WifiConfiguration internalConfig = null;
int securityType = config.getDefaultSecurityParams().getSecurityType();
WifiConfiguration possibleExistingConfig = new WifiConfiguration(config);
switch (securityType) {
case WifiConfiguration.SECURITY_TYPE_PSK:
possibleExistingConfig.setSecurityParams(WifiConfiguration.SECURITY_TYPE_SAE);
break;
case WifiConfiguration.SECURITY_TYPE_SAE:
possibleExistingConfig.setSecurityParams(WifiConfiguration.SECURITY_TYPE_PSK);
break;
case WifiConfiguration.SECURITY_TYPE_EAP:
possibleExistingConfig.setSecurityParams(
WifiConfiguration.SECURITY_TYPE_EAP_WPA3_ENTERPRISE);
break;
case WifiConfiguration.SECURITY_TYPE_EAP_WPA3_ENTERPRISE:
possibleExistingConfig.setSecurityParams(
WifiConfiguration.SECURITY_TYPE_EAP);
break;
case WifiConfiguration.SECURITY_TYPE_OPEN:
possibleExistingConfig.setSecurityParams(WifiConfiguration.SECURITY_TYPE_OWE);
break;
case WifiConfiguration.SECURITY_TYPE_OWE:
possibleExistingConfig.setSecurityParams(WifiConfiguration.SECURITY_TYPE_OPEN);
break;
default:
return null;
}
internalConfig = mConfiguredNetworks.getByConfigKeyForCurrentUser(
possibleExistingConfig.getProfileKey());
return internalConfig;
}
/**
* Helper method to retrieve the internal WifiConfiguration object corresponding to the
* provided configuration in our database.
* This first attempts to find the network using the provided network ID in configuration,
* else it attempts to find a matching configuration using the configKey.
*/
private @Nullable WifiConfiguration getInternalConfiguredNetwork(
@NonNull WifiConfiguration config) {
WifiConfiguration internalConfig = mConfiguredNetworks.getForCurrentUser(config.networkId);
if (internalConfig != null) {
return internalConfig;
}
internalConfig = mConfiguredNetworks.getByConfigKeyForCurrentUser(
config.getProfileKey());
if (internalConfig != null) {
return internalConfig;
}
internalConfig = getInternalConfiguredNetworkByUpgradableType(config);
if (internalConfig == null) {
Log.e(TAG, "Cannot find network with networkId " + config.networkId
+ " or configKey " + config.getProfileKey()
+ " or upgradable security type check");
}
return internalConfig;
}
/**
* Helper method to retrieve the internal WifiConfiguration object corresponding to the
* provided network ID in our database.
*/
private @Nullable WifiConfiguration getInternalConfiguredNetwork(int networkId) {
if (networkId == WifiConfiguration.INVALID_NETWORK_ID) {
return null;
}
WifiConfiguration internalConfig = mConfiguredNetworks.getForCurrentUser(networkId);
if (internalConfig == null) {
Log.e(TAG, "Cannot find network with networkId " + networkId);
}
return internalConfig;
}
/**
* Helper method to retrieve the internal WifiConfiguration object corresponding to the
* provided configKey in our database.
*/
private @Nullable WifiConfiguration getInternalConfiguredNetwork(String configKey) {
WifiConfiguration internalConfig =
mConfiguredNetworks.getByConfigKeyForCurrentUser(configKey);
if (internalConfig == null) {
Log.e(TAG, "Cannot find network with configKey " + configKey);
}
return internalConfig;
}
/**
* Method to send out the configured networks change broadcast when network configurations
* changed.
*
* In Android R we stopped sending out WifiConfiguration due to user privacy concerns.
* Thus, no matter how many networks changed,
* {@link WifiManager#EXTRA_MULTIPLE_NETWORKS_CHANGED} is always set to true, and
* {@link WifiManager#EXTRA_WIFI_CONFIGURATION} is always null.
*
* @param reason The reason for the change, should be one of WifiManager.CHANGE_REASON_ADDED,
* WifiManager.CHANGE_REASON_REMOVED, or WifiManager.CHANGE_REASON_CHANGE.
*/
private void sendConfiguredNetworkChangedBroadcast(int reason) {
Intent intent = new Intent(WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
intent.putExtra(WifiManager.EXTRA_MULTIPLE_NETWORKS_CHANGED, true);
intent.putExtra(WifiManager.EXTRA_CHANGE_REASON, reason);
mContext.sendBroadcastAsUser(intent, UserHandle.ALL, Manifest.permission.ACCESS_WIFI_STATE);
}
/**
* Checks if |uid| has permission to modify the provided configuration.
*
* @param config WifiConfiguration object corresponding to the network to be modified.
* @param uid UID of the app requesting the modification.
* @param packageName Package name of the app requesting the modification.
*/
private boolean canModifyNetwork(WifiConfiguration config, int uid,
@Nullable String packageName) {
// Passpoint configurations are generated and managed by PasspointManager. They can be
// added by either PasspointNetworkNominator (for auto connection) or Settings app
// (for manual connection), and need to be removed once the connection is completed.
// Since it is "owned" by us, so always allow us to modify them.
if (config.isPasspoint() && uid == Process.WIFI_UID) {
return true;
}
// EAP-SIM/AKA/AKA' network needs framework to update the anonymous identity provided
// by authenticator back to the WifiConfiguration object.
// Since it is "owned" by us, so always allow us to modify them.
if (config.enterpriseConfig != null
&& uid == Process.WIFI_UID
&& config.enterpriseConfig.isAuthenticationSimBased()) {
return true;
}
// TODO: ideally package should not be null here (and hence we wouldn't need the
// isDeviceOwner(uid) method), but it would require changing many methods to pass the
// package name around (for example, all methods called by
// WifiServiceImpl.triggerConnectAndReturnStatus(netId, callingUid)
final boolean isOrganizationOwnedDeviceAdmin =
mWifiPermissionsUtil.isOrganizationOwnedDeviceAdmin(uid, packageName);
// If |uid| corresponds to the device owner or the profile owner of an organization owned
// device, allow all modifications.
if (isOrganizationOwnedDeviceAdmin) {
return true;
}
final boolean isCreator = (config.creatorUid == uid);
// WiFi config lockdown related logic. At this point we know uid is NOT a Device Owner
// or a Profile Owner of an organization owned device.
final boolean isConfigEligibleForLockdown =
mWifiPermissionsUtil.isOrganizationOwnedDeviceAdmin(config.creatorUid,
config.creatorName);
if (!isConfigEligibleForLockdown) {
// App that created the network or settings app (i.e user) has permission to
// modify the network.
return isCreator
|| mWifiPermissionsUtil.checkNetworkSettingsPermission(uid)
|| mWifiPermissionsUtil.checkNetworkSetupWizardPermission(uid);
}
final ContentResolver resolver = mContext.getContentResolver();
final boolean isLockdownFeatureEnabled = Settings.Global.getInt(resolver,
Settings.Global.WIFI_DEVICE_OWNER_CONFIGS_LOCKDOWN, 0) != 0;
return !isLockdownFeatureEnabled
// If not locked down, settings app (i.e user) has permission to modify the network.
&& (mWifiPermissionsUtil.checkNetworkSettingsPermission(uid)
|| mWifiPermissionsUtil.checkNetworkSetupWizardPermission(uid));
}
private void mergeSecurityParamsListWithInternalWifiConfiguration(
WifiConfiguration internalConfig, WifiConfiguration externalConfig) {
// If not set, just copy over all list.
if (internalConfig.getSecurityParamsList().isEmpty()) {
internalConfig.setSecurityParams(externalConfig.getSecurityParamsList());
return;
}
WifiConfigurationUtil.addUpgradableSecurityTypeIfNecessary(externalConfig);
// An external caller is only allowed to set one type manually.
// As a result, only default type matters.
// There might be 3 cases:
// 1. Existing config with new upgradable type config,
// ex. PSK/SAE config with SAE config.
// 2. Existing configuration with downgradable type config,
// ex. SAE config with PSK config.
// 3. The new type is not a compatible type of existing config.
// ex. Open config with PSK config.
// This might happen when updating a config via network ID directly.
int oldType = internalConfig.getDefaultSecurityParams().getSecurityType();
int newType = externalConfig.getDefaultSecurityParams().getSecurityType();
if (oldType != newType) {
if (internalConfig.isSecurityType(newType)) {
internalConfig.setSecurityParamsIsAddedByAutoUpgrade(newType, false);
} else if (externalConfig.isSecurityType(oldType)) {
internalConfig.setSecurityParams(newType);
internalConfig.addSecurityParams(oldType);
} else {
internalConfig.setSecurityParams(externalConfig.getSecurityParamsList());
}
}
}
private void mergeDppSecurityParamsWithInternalWifiConfiguration(
WifiConfiguration internalConfig, WifiConfiguration externalConfig) {
// Do not update for non-DPP network
if (!externalConfig.isSecurityType(WifiConfiguration.SECURITY_TYPE_DPP)) {
return;
}
if (externalConfig.getDppConnector().length != 0
&& externalConfig.getDppCSignKey().length != 0
&& externalConfig.getDppNetAccessKey().length != 0) {
internalConfig.setDppConnectionKeys(externalConfig.getDppConnector(),
externalConfig.getDppCSignKey(), externalConfig.getDppNetAccessKey());
}
if (externalConfig.getDppPrivateEcKey().length != 0) {
internalConfig.setDppConfigurator(externalConfig.getDppPrivateEcKey());
}
}
/**
* Copy over public elements from an external WifiConfiguration object to the internal
* configuration object if element has been set in the provided external WifiConfiguration.
* The only exception is the hidden |IpConfiguration| parameters, these need to be copied over
* for every update.
*
* This method updates all elements that are common to both network addition & update.
* The following fields of {@link WifiConfiguration} are not copied from external configs:
* > networkId - These are allocated by Wi-Fi stack internally for any new configurations.
* > status - The status needs to be explicitly updated using
* {@link WifiManager#enableNetwork(int, boolean)} or
* {@link WifiManager#disableNetwork(int)}.
*
* @param internalConfig WifiConfiguration object in our internal map.
* @param externalConfig WifiConfiguration object provided from the external API.
*/
private void mergeWithInternalWifiConfiguration(
WifiConfiguration internalConfig, WifiConfiguration externalConfig) {
if (externalConfig.SSID != null) {
internalConfig.SSID = externalConfig.SSID;
}
if (externalConfig.BSSID != null) {
internalConfig.BSSID = externalConfig.BSSID.toLowerCase();
}
internalConfig.hiddenSSID = externalConfig.hiddenSSID;
if (externalConfig.preSharedKey != null
&& !externalConfig.preSharedKey.equals(PASSWORD_MASK)) {
internalConfig.preSharedKey = externalConfig.preSharedKey;
}
// Modify only wep keys are present in the provided configuration. This is a little tricky
// because there is no easy way to tell if the app is actually trying to null out the
// existing keys or not.
if (externalConfig.wepKeys != null) {
boolean hasWepKey = false;
for (int i = 0; i < internalConfig.wepKeys.length; i++) {
if (externalConfig.wepKeys[i] != null
&& !externalConfig.wepKeys[i].equals(PASSWORD_MASK)) {
internalConfig.wepKeys[i] = externalConfig.wepKeys[i];
hasWepKey = true;
}
}
if (hasWepKey) {
internalConfig.wepTxKeyIndex = externalConfig.wepTxKeyIndex;
}
}
if (externalConfig.FQDN != null) {
internalConfig.FQDN = externalConfig.FQDN;
}
if (externalConfig.providerFriendlyName != null) {
internalConfig.providerFriendlyName = externalConfig.providerFriendlyName;
}
if (externalConfig.roamingConsortiumIds != null) {
internalConfig.roamingConsortiumIds = externalConfig.roamingConsortiumIds.clone();
}
mergeSecurityParamsListWithInternalWifiConfiguration(internalConfig, externalConfig);
mergeDppSecurityParamsWithInternalWifiConfiguration(internalConfig, externalConfig);
// Copy over the |IpConfiguration| parameters if set.
if (externalConfig.getIpConfiguration() != null) {
IpConfiguration.IpAssignment ipAssignment = externalConfig.getIpAssignment();
if (ipAssignment != IpConfiguration.IpAssignment.UNASSIGNED) {
internalConfig.setIpAssignment(ipAssignment);
if (ipAssignment == IpConfiguration.IpAssignment.STATIC) {
internalConfig.setStaticIpConfiguration(
new StaticIpConfiguration(externalConfig.getStaticIpConfiguration()));
}
}
IpConfiguration.ProxySettings proxySettings = externalConfig.getProxySettings();
if (proxySettings != IpConfiguration.ProxySettings.UNASSIGNED) {
internalConfig.setProxySettings(proxySettings);
if (proxySettings == IpConfiguration.ProxySettings.PAC
|| proxySettings == IpConfiguration.ProxySettings.STATIC) {
internalConfig.setHttpProxy(new ProxyInfo(externalConfig.getHttpProxy()));
}
}
}
internalConfig.allowAutojoin = externalConfig.allowAutojoin;
// Copy over the |WifiEnterpriseConfig| parameters if set.
if (externalConfig.enterpriseConfig != null) {
internalConfig.enterpriseConfig.copyFromExternal(
externalConfig.enterpriseConfig, PASSWORD_MASK);
}
// Copy over any metered information.
internalConfig.meteredHint = externalConfig.meteredHint;
internalConfig.meteredOverride = externalConfig.meteredOverride;
internalConfig.trusted = externalConfig.trusted;
internalConfig.oemPaid = externalConfig.oemPaid;
internalConfig.oemPrivate = externalConfig.oemPrivate;
internalConfig.dbsSecondaryInternet = externalConfig.dbsSecondaryInternet;
internalConfig.carrierMerged = externalConfig.carrierMerged;
internalConfig.restricted = externalConfig.restricted;
// Copy over macRandomizationSetting
internalConfig.macRandomizationSetting = externalConfig.macRandomizationSetting;
internalConfig.carrierId = externalConfig.carrierId;
internalConfig.isHomeProviderNetwork = externalConfig.isHomeProviderNetwork;
internalConfig.subscriptionId = externalConfig.subscriptionId;
internalConfig.setSubscriptionGroup(externalConfig.getSubscriptionGroup());
internalConfig.getNetworkSelectionStatus()
.setConnectChoice(externalConfig.getNetworkSelectionStatus().getConnectChoice());
internalConfig.getNetworkSelectionStatus().setConnectChoiceRssi(
externalConfig.getNetworkSelectionStatus().getConnectChoiceRssi());
internalConfig.setBssidAllowlist(externalConfig.getBssidAllowlistInternal());
internalConfig.setRepeaterEnabled(externalConfig.isRepeaterEnabled());
}
/**
* Set all the exposed defaults in the newly created WifiConfiguration object.
* These fields have a default value advertised in our public documentation. The only exception
* is the hidden |IpConfiguration| parameters, these have a default value even though they're
* hidden.
*
* @param configuration provided WifiConfiguration object.
*/
private void setDefaultsInWifiConfiguration(WifiConfiguration configuration) {
configuration.setIpAssignment(IpConfiguration.IpAssignment.DHCP);
configuration.setProxySettings(IpConfiguration.ProxySettings.NONE);
configuration.status = WifiConfiguration.Status.DISABLED;
configuration.getNetworkSelectionStatus().setNetworkSelectionStatus(
NetworkSelectionStatus.NETWORK_SELECTION_PERMANENTLY_DISABLED);
configuration.getNetworkSelectionStatus().setNetworkSelectionDisableReason(
NetworkSelectionStatus.DISABLED_BY_WIFI_MANAGER);
}
/**
* Create a new internal WifiConfiguration object by copying over parameters from the provided
* external configuration and set defaults for the appropriate parameters.
*
* @param externalConfig WifiConfiguration object provided from the external API.
* @return New WifiConfiguration object with parameters merged from the provided external
* configuration.
*/
private WifiConfiguration createNewInternalWifiConfigurationFromExternal(
WifiConfiguration externalConfig, int uid, @Nullable String packageName) {
WifiConfiguration newInternalConfig = new WifiConfiguration();
// First allocate a new network ID for the configuration.
newInternalConfig.networkId = mNextNetworkId++;
// First set defaults in the new configuration created.
setDefaultsInWifiConfiguration(newInternalConfig);
// Convert legacy fields to new security params
externalConfig.convertLegacyFieldsToSecurityParamsIfNeeded();
// Copy over all the public elements from the provided configuration.
mergeWithInternalWifiConfiguration(newInternalConfig, externalConfig);
// Copy over the hidden configuration parameters. These are the only parameters used by
// system apps to indicate some property about the network being added.
// These are only copied over for network additions and ignored for network updates.
newInternalConfig.noInternetAccessExpected = externalConfig.noInternetAccessExpected;
newInternalConfig.ephemeral = externalConfig.ephemeral;
newInternalConfig.osu = externalConfig.osu;
newInternalConfig.fromWifiNetworkSuggestion = externalConfig.fromWifiNetworkSuggestion;
newInternalConfig.fromWifiNetworkSpecifier = externalConfig.fromWifiNetworkSpecifier;
newInternalConfig.useExternalScores = externalConfig.useExternalScores;
newInternalConfig.shared = externalConfig.shared;
newInternalConfig.updateIdentifier = externalConfig.updateIdentifier;
newInternalConfig.setPasspointUniqueId(externalConfig.getPasspointUniqueId());
// Add debug information for network addition.
newInternalConfig.creatorUid = newInternalConfig.lastUpdateUid = uid;
newInternalConfig.creatorName = newInternalConfig.lastUpdateName =
packageName != null ? packageName : mContext.getPackageManager().getNameForUid(uid);
newInternalConfig.lastUpdated = mClock.getWallClockMillis();
newInternalConfig.numRebootsSinceLastUse = 0;
initRandomizedMacForInternalConfig(newInternalConfig);
return newInternalConfig;
}
/**
* Create a new internal WifiConfiguration object by copying over parameters from the provided
* external configuration to a copy of the existing internal WifiConfiguration object.
*
* @param internalConfig WifiConfiguration object in our internal map.
* @param externalConfig WifiConfiguration object provided from the external API.
* @param overrideCreator when this set to true, will overrider the creator to the current
* modifier.
* @return Copy of existing WifiConfiguration object with parameters merged from the provided
* configuration.
*/
private @NonNull WifiConfiguration updateExistingInternalWifiConfigurationFromExternal(
@NonNull WifiConfiguration internalConfig, @NonNull WifiConfiguration externalConfig,
int uid, @Nullable String packageName, boolean overrideCreator) {
WifiConfiguration newInternalConfig = new WifiConfiguration(internalConfig);
// Copy over all the public elements from the provided configuration.
mergeWithInternalWifiConfiguration(newInternalConfig, externalConfig);
// Add debug information for network update.
newInternalConfig.lastUpdateUid = uid;
newInternalConfig.lastUpdateName =
packageName != null ? packageName : mContext.getPackageManager().getNameForUid(uid);
newInternalConfig.lastUpdated = mClock.getWallClockMillis();
newInternalConfig.numRebootsSinceLastUse = 0;
if (overrideCreator) {
newInternalConfig.creatorName = newInternalConfig.lastUpdateName;
newInternalConfig.creatorUid = uid;
}
return newInternalConfig;
}
private void logUserActionEvents(WifiConfiguration before, WifiConfiguration after) {
// Logs changes in meteredOverride.
if (before.meteredOverride != after.meteredOverride) {
mWifiMetrics.logUserActionEvent(
WifiMetrics.convertMeteredOverrideEnumToUserActionEventType(
after.meteredOverride),
after.networkId);
}
// Logs changes in macRandomizationSetting.
if (before.macRandomizationSetting != after.macRandomizationSetting) {
mWifiMetrics.logUserActionEvent(
after.macRandomizationSetting == WifiConfiguration.RANDOMIZATION_NONE
? UserActionEvent.EVENT_CONFIGURE_MAC_RANDOMIZATION_OFF
: UserActionEvent.EVENT_CONFIGURE_MAC_RANDOMIZATION_ON,
after.networkId);
}
}
/**
* Add a network or update a network configuration to our database.
* If the supplied networkId is INVALID_NETWORK_ID, we create a new empty
* network configuration. Otherwise, the networkId should refer to an existing configuration.
*
* @param config provided WifiConfiguration object.
* @param uid UID of the app requesting the network addition/modification.
* @param packageName Package name of the app requesting the network addition/modification.
* @param overrideCreator when this set to true, will overrider the creator to the current
* modifier.
* @return NetworkUpdateResult object representing status of the update.
* WifiConfiguration object representing the existing configuration matching
* the new config, or null if none matches.
*/
private @NonNull Pair<NetworkUpdateResult, WifiConfiguration> addOrUpdateNetworkInternal(
@NonNull WifiConfiguration config, int uid, @Nullable String packageName,
boolean overrideCreator) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Adding/Updating network " + config.getPrintableSsid());
}
WifiConfiguration newInternalConfig = null;
long supportedFeatures = mWifiInjector.getActiveModeWarden()
.getPrimaryClientModeManager().getSupportedFeatures();
// First check if we already have a network with the provided network id or configKey.
WifiConfiguration existingInternalConfig = getInternalConfiguredNetwork(config);
// No existing network found. So, potentially a network add.
if (existingInternalConfig == null) {
if (!WifiConfigurationUtil.validate(config, supportedFeatures,
WifiConfigurationUtil.VALIDATE_FOR_ADD)) {
Log.e(TAG, "Cannot add network with invalid config");
return new Pair<>(
new NetworkUpdateResult(WifiConfiguration.INVALID_NETWORK_ID),
existingInternalConfig);
}
newInternalConfig =
createNewInternalWifiConfigurationFromExternal(config, uid, packageName);
// Since the original config provided may have had an empty
// {@link WifiConfiguration#allowedKeyMgmt} field, check again if we already have a
// network with the the same configkey.
existingInternalConfig =
getInternalConfiguredNetwork(newInternalConfig.getProfileKey());
}
// Existing network found. So, a network update.
if (existingInternalConfig != null) {
if (!WifiConfigurationUtil.validate(
config, supportedFeatures, WifiConfigurationUtil.VALIDATE_FOR_UPDATE)) {
Log.e(TAG, "Cannot update network with invalid config");
return new Pair<>(
new NetworkUpdateResult(WifiConfiguration.INVALID_NETWORK_ID),
existingInternalConfig);
}
// Check for the app's permission before we let it update this network.
if (!canModifyNetwork(existingInternalConfig, uid, packageName)) {
Log.e(TAG, "UID " + uid + " does not have permission to update configuration "
+ config.getProfileKey());
return new Pair<>(
new NetworkUpdateResult(WifiConfiguration.INVALID_NETWORK_ID),
existingInternalConfig);
}
if (mWifiPermissionsUtil.checkNetworkSettingsPermission(uid)
&& !config.isPasspoint()) {
logUserActionEvents(existingInternalConfig, config);
}
newInternalConfig =
updateExistingInternalWifiConfigurationFromExternal(
existingInternalConfig, config, uid, packageName, overrideCreator);
}
if (!WifiConfigurationUtil.addUpgradableSecurityTypeIfNecessary(newInternalConfig)) {
return new Pair<>(
new NetworkUpdateResult(WifiConfiguration.INVALID_NETWORK_ID),
existingInternalConfig);
}
// Only add networks with proxy settings if the user has permission to
if (WifiConfigurationUtil.hasProxyChanged(existingInternalConfig, newInternalConfig)
&& !canModifyProxySettings(uid, packageName)) {
Log.e(TAG, "UID " + uid + " does not have permission to modify proxy Settings "
+ config.getProfileKey() + ". Must have NETWORK_SETTINGS,"
+ " or be device or profile owner.");
return new Pair<>(
new NetworkUpdateResult(WifiConfiguration.INVALID_NETWORK_ID),
existingInternalConfig);
}
// Only allow changes in Repeater Enabled flag if the user has permission to
if (WifiConfigurationUtil.hasRepeaterEnabledChanged(
existingInternalConfig, newInternalConfig)
&& !mWifiPermissionsUtil.checkNetworkSettingsPermission(uid)) {
Log.e(TAG, "UID " + uid
+ " does not have permission to modify Repeater Enabled Settings "
+ " , or add a network with Repeater Enabled set to true "
+ config.getProfileKey() + ". Must have NETWORK_SETTINGS.");
return new Pair<>(
new NetworkUpdateResult(WifiConfiguration.INVALID_NETWORK_ID),
existingInternalConfig);
}
if (WifiConfigurationUtil.hasMacRandomizationSettingsChanged(existingInternalConfig,
newInternalConfig) && !mWifiPermissionsUtil.checkNetworkSettingsPermission(uid)
&& !mWifiPermissionsUtil.checkNetworkSetupWizardPermission(uid)
&& !(newInternalConfig.isPasspoint() && uid == newInternalConfig.creatorUid)
&& !config.fromWifiNetworkSuggestion
&& !mWifiPermissionsUtil.isDeviceInDemoMode(mContext)
&& !(mWifiPermissionsUtil.isAdmin(uid, packageName)
&& uid == newInternalConfig.creatorUid)) {
Log.e(TAG, "UID " + uid + " does not have permission to modify MAC randomization "
+ "Settings " + config.getProfileKey() + ". Must have "
+ "NETWORK_SETTINGS or NETWORK_SETUP_WIZARD or be in Demo Mode "
+ "or be the creator adding or updating a passpoint network "
+ "or be an admin updating their own network.");
return new Pair<>(
new NetworkUpdateResult(WifiConfiguration.INVALID_NETWORK_ID),
existingInternalConfig);
}
// Update the keys for saved enterprise networks. For Passpoint, the certificates
// and keys are installed at the time the provider is installed. For suggestion enterprise
// network the certificates and keys are installed at the time the suggestion is added
if (!config.isPasspoint() && !config.fromWifiNetworkSuggestion && config.isEnterprise()) {
if (!(mWifiKeyStore.updateNetworkKeys(newInternalConfig, existingInternalConfig))) {
return new Pair<>(
new NetworkUpdateResult(WifiConfiguration.INVALID_NETWORK_ID),
existingInternalConfig);
}
}
// Validate an Enterprise network with Trust On First Use.
if (config.isEnterprise() && config.enterpriseConfig.isTrustOnFirstUseEnabled()) {
if ((supportedFeatures & WIFI_FEATURE_TRUST_ON_FIRST_USE) == 0) {
Log.e(TAG, "Trust On First Use could not be set "
+ "when Trust On First Use is not supported.");
return new Pair<>(
new NetworkUpdateResult(WifiConfiguration.INVALID_NETWORK_ID),
existingInternalConfig);
}
if (!config.enterpriseConfig.isEapMethodServerCertUsed()) {
Log.e(TAG, "Trust On First Use could not be set "
+ "when the server certificate is not used.");
return new Pair<>(
new NetworkUpdateResult(WifiConfiguration.INVALID_NETWORK_ID),
existingInternalConfig);
} else if (config.enterpriseConfig.hasCaCertificate()) {
Log.e(TAG, "Trust On First Use could not be set "
+ "when Root CA certificate is set.");
return new Pair<>(
new NetworkUpdateResult(WifiConfiguration.INVALID_NETWORK_ID),
existingInternalConfig);
}
}
boolean newNetwork = (existingInternalConfig == null);
// This is needed to inform IpClient about any IP configuration changes.
boolean hasIpChanged =
newNetwork || WifiConfigurationUtil.hasIpChanged(
existingInternalConfig, newInternalConfig);
boolean hasProxyChanged =
newNetwork || WifiConfigurationUtil.hasProxyChanged(
existingInternalConfig, newInternalConfig);
// Reset the |hasEverConnected| flag if the credential parameters changed in this update.
boolean hasCredentialChanged =
newNetwork || WifiConfigurationUtil.hasCredentialChanged(
existingInternalConfig, newInternalConfig);
if (hasCredentialChanged) {
newInternalConfig.getNetworkSelectionStatus().setHasEverConnected(false);
}
// Ensure that the user approve flag is set to false for a new network.
if (newNetwork && config.isEnterprise()) {
config.enterpriseConfig.setUserApproveNoCaCert(false);
}
// Add it to our internal map. This will replace any existing network configuration for
// updates.
try {
if (null != existingInternalConfig) {
mConfiguredNetworks.remove(existingInternalConfig.networkId);
}
mConfiguredNetworks.put(newInternalConfig);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Failed to add network to config map", e);
return new Pair<>(
new NetworkUpdateResult(WifiConfiguration.INVALID_NETWORK_ID),
existingInternalConfig);
}
if (removeExcessNetworks(uid, packageName)) {
if (mConfiguredNetworks.getForAllUsers(newInternalConfig.networkId) == null) {
Log.e(TAG, "Cannot add network because number of configured networks is maxed.");
return new Pair<>(
new NetworkUpdateResult(WifiConfiguration.INVALID_NETWORK_ID),
existingInternalConfig);
}
}
// Only re-enable network: 1. add or update user saved network; 2. add or update a user
// saved passpoint network framework consider it is a new network.
if (!newInternalConfig.fromWifiNetworkSuggestion
&& (!newInternalConfig.isPasspoint() || newNetwork)) {
userEnabledNetwork(newInternalConfig.networkId);
}
// Stage the backup of the SettingsProvider package which backs this up.
mBackupManagerProxy.notifyDataChanged();
NetworkUpdateResult result = new NetworkUpdateResult(
newInternalConfig.networkId,
hasIpChanged,
hasProxyChanged,
hasCredentialChanged,
newNetwork);
localLog("addOrUpdateNetworkInternal: added/updated config."
+ " netId=" + newInternalConfig.networkId
+ " configKey=" + newInternalConfig.getProfileKey()
+ " uid=" + Integer.toString(newInternalConfig.creatorUid)
+ " name=" + newInternalConfig.creatorName);
return new Pair<>(result, existingInternalConfig);
}
/**
* Add a network or update a network configuration to our database.
* If the supplied networkId is INVALID_NETWORK_ID, we create a new empty
* network configuration. Otherwise, the networkId should refer to an existing configuration.
*
* @param config provided WifiConfiguration object.
* @param uid UID of the app requesting the network addition/modification.
* @param packageName Package name of the app requesting the network addition/modification.
* @param overrideCreator when this set to true, will overrider the creator to the current
* modifier.
* @return NetworkUpdateResult object representing status of the update.
*/
public NetworkUpdateResult addOrUpdateNetwork(WifiConfiguration config, int uid,
@Nullable String packageName, boolean overrideCreator) {
if (!mWifiPermissionsUtil.doesUidBelongToCurrentUserOrDeviceOwner(uid)) {
Log.e(TAG, "UID " + uid + " not visible to the current user");
return new NetworkUpdateResult(WifiConfiguration.INVALID_NETWORK_ID);
}
if (config == null) {
Log.e(TAG, "Cannot add/update network with null config");
return new NetworkUpdateResult(WifiConfiguration.INVALID_NETWORK_ID);
}
if (mPendingStoreRead) {
Log.e(TAG, "Cannot add/update network before store is read!");
return new NetworkUpdateResult(WifiConfiguration.INVALID_NETWORK_ID);
}
config.convertLegacyFieldsToSecurityParamsIfNeeded();
WifiConfiguration existingConfig = getInternalConfiguredNetwork(config);
if (!config.isEphemeral()) {
// Removes the existing ephemeral network if it exists to add this configuration.
if (existingConfig != null && existingConfig.isEphemeral()) {
// In this case, new connection for this config won't happen because same
// network is already registered as an ephemeral network.
// Clear the Ephemeral Network to address the situation.
removeNetwork(
existingConfig.networkId, existingConfig.creatorUid, config.creatorName);
}
}
Pair<NetworkUpdateResult, WifiConfiguration> resultPair = addOrUpdateNetworkInternal(
config, uid, packageName, overrideCreator);
NetworkUpdateResult result = resultPair.first;
existingConfig = resultPair.second;
if (!result.isSuccess()) {
Log.e(TAG, "Failed to add/update network " + config.getPrintableSsid());
return result;
}
WifiConfiguration newConfig = getInternalConfiguredNetwork(result.getNetworkId());
sendConfiguredNetworkChangedBroadcast(
result.isNewNetwork()
? WifiManager.CHANGE_REASON_ADDED
: WifiManager.CHANGE_REASON_CONFIG_CHANGE);
// Unless the added network is ephemeral or Passpoint, persist the network update/addition.
if (!config.ephemeral && !config.isPasspoint()) {
saveToStore(true);
}
for (OnNetworkUpdateListener listener : mListeners) {
if (result.isNewNetwork()) {
listener.onNetworkAdded(
createExternalWifiConfiguration(newConfig, true, Process.WIFI_UID));
} else {
listener.onNetworkUpdated(
createExternalWifiConfiguration(newConfig, true, Process.WIFI_UID),
createExternalWifiConfiguration(existingConfig, true, Process.WIFI_UID));
}
}
return result;
}
/**
* Add a network or update a network configuration to our database.
* If the supplied networkId is INVALID_NETWORK_ID, we create a new empty
* network configuration. Otherwise, the networkId should refer to an existing configuration.
*
* @param config provided WifiConfiguration object.
* @param uid UID of the app requesting the network addition/modification.
* @return NetworkUpdateResult object representing status of the update.
*/
public NetworkUpdateResult addOrUpdateNetwork(WifiConfiguration config, int uid) {
return addOrUpdateNetwork(config, uid, null, false);
}
/**
* Increments the number of reboots since last use for each configuration.
*
* @see {@link WifiConfiguration#numRebootsSinceLastUse}
*/
public void incrementNumRebootsSinceLastUse() {
getInternalConfiguredNetworks().forEach(config -> config.numRebootsSinceLastUse++);
saveToStore(false);
}
private boolean isDeviceOwnerProfileOwnerOrSystem(int uid, String packageName) {
return mWifiPermissionsUtil.isDeviceOwner(uid, packageName)
|| mWifiPermissionsUtil.isProfileOwner(uid, packageName)
|| mWifiPermissionsUtil.isSystem(packageName, uid);
}
/**
* Removes excess networks in case the number of saved networks exceeds the max limit
* specified in config_wifiMaxNumWifiConfigurations.
*
* If called by a non DO/PO/system app, and a limit on app-added networks is specified in
* config_wifiMaxNumWifiConfigurationsForAppAddedNetworks, only removes excess
* app-added networks.
*
* Configs are removed in ascending order of
* 1. Non-carrier networks before carrier networks
* 2. Non-connected networks before connected networks.
* 3. Deletion priority {@see WifiConfiguration#getDeletionPriority()}
* 4. Last use/creation/update time (lastUpdated/lastConnected or numRebootsSinceLastUse)
* 5. Open and OWE networks before networks with other security types.
* 6. Number of associations
*
* @param uid UID of the app requesting the network addition/modification.
* @param packageName Package name of the app requesting the network addition/modification.
* @return {@code true} if networks were removed, {@code false} otherwise.
*/
private boolean removeExcessNetworks(int uid, String packageName) {
final int maxNumTotalConfigs = mContext.getResources().getInteger(
R.integer.config_wifiMaxNumWifiConfigurations);
final int maxNumAppAddedConfigs = mContext.getResources().getInteger(
R.integer.config_wifiMaxNumWifiConfigurationsAddedByAllApps);
boolean callerIsApp = !isDeviceOwnerProfileOwnerOrSystem(uid, packageName);
if (maxNumTotalConfigs < 0 && (!callerIsApp || maxNumAppAddedConfigs < 0)) {
// Max number of saved networks not specified or does not need to be checked.
return false;
}
int numExcessNetworks = -1;
List<WifiConfiguration> networkList = getSavedNetworks(Process.WIFI_UID);
if (maxNumTotalConfigs >= 0) {
numExcessNetworks = networkList.size() - maxNumTotalConfigs;
}
if (callerIsApp && maxNumAppAddedConfigs >= 0) {
List<WifiConfiguration> appAddedNetworks = networkList
.stream()
.filter(n -> !isDeviceOwnerProfileOwnerOrSystem(n.creatorUid, n.creatorName))
.collect(Collectors.toList());
int numExcessAppAddedNetworks = appAddedNetworks.size() - maxNumAppAddedConfigs;
if (numExcessAppAddedNetworks > 0) {
// Only enforce the limit on app-added networks if it has been exceeded.
// Otherwise, default to checking the limit on the total number of networks.
numExcessNetworks = numExcessAppAddedNetworks;
networkList = appAddedNetworks;
}
}
if (numExcessNetworks <= 0) {
return false;
}
List<WifiConfiguration> configsToDelete = networkList
.stream()
.sorted(Comparator.comparing((WifiConfiguration config) -> config.carrierId
!= TelephonyManager.UNKNOWN_CARRIER_ID)
.thenComparing((WifiConfiguration config) -> config.status
== WifiConfiguration.Status.CURRENT)
.thenComparing((WifiConfiguration config) -> config.getDeletionPriority())
.thenComparing((WifiConfiguration config) -> -config.numRebootsSinceLastUse)
.thenComparing((WifiConfiguration config) ->
Math.max(config.lastConnected, config.lastUpdated))
.thenComparing((WifiConfiguration config) -> {
try {
int authType = config.getAuthType();
return !(authType == WifiConfiguration.KeyMgmt.NONE
|| authType == WifiConfiguration.KeyMgmt.OWE);
} catch (IllegalStateException e) {
// An invalid keymgmt configuration should be pruned first.
return false;
}
})
.thenComparing((WifiConfiguration config) -> config.numAssociation))
.limit(numExcessNetworks)
.collect(Collectors.toList());
for (WifiConfiguration config : configsToDelete) {
mConfiguredNetworks.remove(config.networkId);
localLog("removeExcessNetworks: removed config."
+ " netId=" + config.networkId
+ " configKey=" + config.getProfileKey());
}
return true;
}
/**
* Removes the specified network configuration from our database.
*
* @param config provided WifiConfiguration object.
* @param uid UID of the app requesting the network deletion.
* @return true if successful, false otherwise.
*/
private boolean removeNetworkInternal(WifiConfiguration config, int uid) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Removing network " + config.getPrintableSsid());
}
// Remove any associated enterprise keys for saved enterprise networks. Passpoint network
// will remove the enterprise keys when provider is uninstalled. Suggestion enterprise
// networks will remove the enterprise keys when suggestion is removed.
if (!config.fromWifiNetworkSuggestion && !config.isPasspoint() && config.isEnterprise()) {
mWifiKeyStore.removeKeys(config.enterpriseConfig, false);
}
// Do not remove the user choice when passpoint or suggestion networks are removed from
// WifiConfigManager. Will remove that when profile is deleted from PassointManager or
// WifiNetworkSuggestionsManager.
if (!config.isPasspoint() && !config.fromWifiNetworkSuggestion) {
removeConnectChoiceFromAllNetworks(config.getProfileKey());
}
mConfiguredNetworks.remove(config.networkId);
mScanDetailCaches.remove(config.networkId);
// Stage the backup of the SettingsProvider package which backs this up.
mBackupManagerProxy.notifyDataChanged();
mWifiBlocklistMonitor.handleNetworkRemoved(config.SSID);
localLog("removeNetworkInternal: removed config."
+ " netId=" + config.networkId
+ " configKey=" + config.getProfileKey()
+ " uid=" + Integer.toString(uid)
+ " name=" + mContext.getPackageManager().getNameForUid(uid));
return true;
}
/**
* Removes the specified network configuration from our database.
*
* @param networkId network ID of the provided network.
* @param uid UID of the app requesting the network deletion.
* @return true if successful, false otherwise.
*/
public boolean removeNetwork(int networkId, int uid, String packageName) {
if (!mWifiPermissionsUtil.doesUidBelongToCurrentUserOrDeviceOwner(uid)) {
Log.e(TAG, "UID " + uid + " not visible to the current user");
return false;
}
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config == null) {
return false;
}
if (!canModifyNetwork(config, uid, packageName)) {
Log.e(TAG, "UID " + uid + " does not have permission to delete configuration "
+ config.getProfileKey());
return false;
}
if (!removeNetworkInternal(config, uid)) {
Log.e(TAG, "Failed to remove network " + config.getPrintableSsid());
return false;
}
if (networkId == mLastSelectedNetworkId) {
clearLastSelectedNetwork();
}
if (!config.ephemeral && !config.isPasspoint()) {
mLruConnectionTracker.removeNetwork(config);
}
sendConfiguredNetworkChangedBroadcast(WifiManager.CHANGE_REASON_REMOVED);
// Unless the removed network is ephemeral or Passpoint, persist the network removal.
if (!config.ephemeral && !config.isPasspoint()) {
saveToStore(true);
}
for (OnNetworkUpdateListener listener : mListeners) {
listener.onNetworkRemoved(
createExternalWifiConfiguration(config, true, Process.WIFI_UID));
}
return true;
}
private String getCreatorPackageName(WifiConfiguration config) {
String creatorName = config.creatorName;
// getNameForUid (Stored in WifiConfiguration.creatorName) returns a concatenation of name
// and uid for shared UIDs ("name:uid").
if (!creatorName.contains(":")) {
return creatorName; // regular app not using shared UID.
}
// Separate the package name from the string for app using shared UID.
return creatorName.substring(0, creatorName.indexOf(":"));
}
/**
* Remove all networks associated with an application.
*
* @param app Application info of the package of networks to remove.
* @return the {@link Set} of networks that were removed by this call. Networks which matched
* but failed to remove are omitted from this set.
*/
public Set<Integer> removeNetworksForApp(ApplicationInfo app) {
if (app == null || app.packageName == null) {
return Collections.<Integer>emptySet();
}
Log.d(TAG, "Remove all networks for app " + app);
Set<Integer> removedNetworks = new ArraySet<>();
WifiConfiguration[] copiedConfigs =
mConfiguredNetworks.valuesForAllUsers().toArray(new WifiConfiguration[0]);
for (WifiConfiguration config : copiedConfigs) {
if (app.uid != config.creatorUid
|| !app.packageName.equals(getCreatorPackageName(config))) {
continue;
}
localLog("Removing network " + config.SSID
+ ", application \"" + app.packageName + "\" uninstalled"
+ " from user " + UserHandle.getUserHandleForUid(app.uid));
if (removeNetwork(config.networkId, config.creatorUid, config.creatorName)) {
removedNetworks.add(config.networkId);
}
}
return removedNetworks;
}
/**
* Remove all networks associated with a user.
*
* @param userId The identifier of the user which is being removed.
* @return the {@link Set} of networks that were removed by this call. Networks which matched
* but failed to remove are omitted from this set.
*/
Set<Integer> removeNetworksForUser(int userId) {
Log.d(TAG, "Remove all networks for user " + userId);
Set<Integer> removedNetworks = new ArraySet<>();
WifiConfiguration[] copiedConfigs =
mConfiguredNetworks.valuesForAllUsers().toArray(new WifiConfiguration[0]);
for (WifiConfiguration config : copiedConfigs) {
if (userId != UserHandle.getUserHandleForUid(config.creatorUid).getIdentifier()) {
continue;
}
localLog("Removing network " + config.SSID + ", user " + userId + " removed");
if (removeNetwork(config.networkId, config.creatorUid, config.creatorName)) {
removedNetworks.add(config.networkId);
}
}
return removedNetworks;
}
/**
* Iterates through the internal list of configured networks and removes any ephemeral or
* passpoint network configurations which are transient in nature.
*
* @return true if a network was removed, false otherwise.
*/
public boolean removeAllEphemeralOrPasspointConfiguredNetworks() {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Removing all passpoint or ephemeral configured networks");
}
boolean didRemove = false;
WifiConfiguration[] copiedConfigs =
mConfiguredNetworks.valuesForAllUsers().toArray(new WifiConfiguration[0]);
for (WifiConfiguration config : copiedConfigs) {
if (config.isPasspoint()) {
Log.d(TAG, "Removing passpoint network config " + config.getProfileKey());
removeNetwork(config.networkId, config.creatorUid, config.creatorName);
didRemove = true;
} else if (config.ephemeral) {
Log.d(TAG, "Removing ephemeral network config " + config.getProfileKey());
removeNetwork(config.networkId, config.creatorUid, config.creatorName);
didRemove = true;
}
}
return didRemove;
}
/**
* Removes the suggestion network configuration matched with WifiConfiguration provided.
* @param suggestion WifiConfiguration for suggestion which needs to remove
* @return true if a network was removed, false otherwise.
*/
public boolean removeSuggestionConfiguredNetwork(@NonNull WifiConfiguration suggestion) {
WifiConfiguration config = getInternalConfiguredNetwork(
suggestion.getProfileKey());
if (config != null && config.ephemeral && config.fromWifiNetworkSuggestion) {
Log.d(TAG, "Removing suggestion network config " + config.getProfileKey());
return removeNetwork(config.networkId, suggestion.creatorUid, suggestion.creatorName);
}
return false;
}
/**
* Removes the passpoint network configuration matched with {@code configKey} provided.
*
* @param configKey Config Key for the corresponding passpoint.
* @return true if a network was removed, false otherwise.
*/
public boolean removePasspointConfiguredNetwork(@NonNull String configKey) {
WifiConfiguration config = getInternalConfiguredNetwork(configKey);
if (config != null && config.isPasspoint()) {
Log.d(TAG, "Removing passpoint network config " + config.getProfileKey());
return removeNetwork(config.networkId, config.creatorUid, config.creatorName);
}
return false;
}
/**
* Removes all save networks configurations not created by the caller.
*
* @param callerUid the uid of the caller
* @return {@code true} if at least one network is removed.
*/
public boolean removeNonCallerConfiguredNetwork(int callerUid) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "removeNonCallerConfiguredNetwork caller = " + callerUid);
}
boolean didRemove = false;
WifiConfiguration[] copiedConfigs =
mConfiguredNetworks.valuesForAllUsers().toArray(new WifiConfiguration[0]);
for (WifiConfiguration config : copiedConfigs) {
if (config.creatorUid != callerUid) {
Log.d(TAG, "Removing non-caller network config " + config.getProfileKey());
removeNetwork(config.networkId, config.creatorUid, config.creatorName);
didRemove = true;
}
}
return didRemove;
}
/**
* Check whether a network belong to a known list of networks that may not support randomized
* MAC.
* @param networkId
* @return true if the network is in the hotlist and MAC randomization is enabled.
*/
public boolean isInFlakyRandomizationSsidHotlist(int networkId) {
WifiConfiguration config = getConfiguredNetwork(networkId);
return config != null
&& config.macRandomizationSetting != WifiConfiguration.RANDOMIZATION_NONE
&& mDeviceConfigFacade.getRandomizationFlakySsidHotlist().contains(config.SSID);
}
/**
* Helper method to set the publicly exposed status for the network and send out the network
* status change broadcast.
*/
private void setNetworkStatus(WifiConfiguration config, int status) {
config.status = status;
sendConfiguredNetworkChangedBroadcast(WifiManager.CHANGE_REASON_CONFIG_CHANGE);
}
/**
* Update a network's status (both internal and public) according to the update reason and
* its current state.
*
* Each network has 2 status:
* 1. NetworkSelectionStatus: This is internal selection status of the network. This is used
* for temporarily disabling a network for Network Selector.
* 2. Status: This is the exposed status for a network. This is mostly set by
* the public API's {@link WifiManager#enableNetwork(int, boolean)} &
* {@link WifiManager#disableNetwork(int)}.
*
* @param networkId network ID of the network that needs the update.
* @param reason reason to update the network.
* @return true if the input configuration has been updated, false otherwise.
*/
public boolean updateNetworkSelectionStatus(int networkId, int reason) {
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config == null) {
return false;
}
return updateNetworkSelectionStatus(config, reason);
}
private boolean updateNetworkSelectionStatus(@NonNull WifiConfiguration config, int reason) {
int prevNetworkSelectionStatus = config.getNetworkSelectionStatus()
.getNetworkSelectionStatus();
if (!mWifiBlocklistMonitor.updateNetworkSelectionStatus(config, reason)) {
return false;
}
int newNetworkSelectionStatus = config.getNetworkSelectionStatus()
.getNetworkSelectionStatus();
if (prevNetworkSelectionStatus != newNetworkSelectionStatus) {
sendNetworkSelectionStatusChangedUpdate(config, newNetworkSelectionStatus, reason);
sendConfiguredNetworkChangedBroadcast(WifiManager.CHANGE_REASON_CONFIG_CHANGE);
}
saveToStore(false);
return true;
}
private void sendNetworkSelectionStatusChangedUpdate(@NonNull WifiConfiguration config,
int newNetworkSelectionStatus, int disableReason) {
switch (newNetworkSelectionStatus) {
case NetworkSelectionStatus.NETWORK_SELECTION_ENABLED:
for (OnNetworkUpdateListener listener : mListeners) {
listener.onNetworkEnabled(
createExternalWifiConfiguration(config, true, Process.WIFI_UID));
}
break;
case NetworkSelectionStatus.NETWORK_SELECTION_TEMPORARY_DISABLED:
for (OnNetworkUpdateListener listener : mListeners) {
listener.onNetworkTemporarilyDisabled(
createExternalWifiConfiguration(config, true, Process.WIFI_UID),
disableReason);
}
break;
case NetworkSelectionStatus.NETWORK_SELECTION_PERMANENTLY_DISABLED:
for (OnNetworkUpdateListener listener : mListeners) {
WifiConfiguration configForListener = new WifiConfiguration(config);
listener.onNetworkPermanentlyDisabled(
createExternalWifiConfiguration(config, true, Process.WIFI_UID),
disableReason);
}
break;
default:
// all cases covered
}
}
/**
* Re-enable all temporary disabled configured networks.
*/
public void enableTemporaryDisabledNetworks() {
mWifiBlocklistMonitor.clearBssidBlocklist();
for (WifiConfiguration config : getInternalConfiguredNetworks()) {
if (config.getNetworkSelectionStatus().isNetworkTemporaryDisabled()) {
updateNetworkSelectionStatus(config,
NetworkSelectionStatus.DISABLED_NONE);
}
}
}
/**
* Attempt to re-enable a network for network selection, if this network was either:
* a) Previously temporarily disabled, but its disable timeout has expired, or
* b) Previously disabled because of a user switch, but is now visible to the current
* user.
*
* @param networkId the id of the network to be checked for possible unblock (due to timeout)
* @return true if the network identified by {@param networkId} was re-enabled for qualified
* network selection, false otherwise.
*/
public boolean tryEnableNetwork(int networkId) {
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config == null) {
return false;
}
if (mWifiBlocklistMonitor.shouldEnableNetwork(config)) {
return updateNetworkSelectionStatus(config, NetworkSelectionStatus.DISABLED_NONE);
}
return false;
}
/**
* Enable a network using the public {@link WifiManager#enableNetwork(int, boolean)} API.
*
* @param networkId network ID of the network that needs the update.
* @param disableOthers Whether to disable all other networks or not. This is used to indicate
* that the app requested connection to a specific network.
* @param uid uid of the app requesting the update.
* @param packageName Package name of calling apps
* @return true if it succeeds, false otherwise
*/
public boolean enableNetwork(int networkId, boolean disableOthers, int uid,
@NonNull String packageName) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Enabling network " + networkId + " (disableOthers " + disableOthers + ")");
}
if (!mWifiPermissionsUtil.doesUidBelongToCurrentUserOrDeviceOwner(uid)) {
Log.e(TAG, "UID " + uid + " not visible to the current user");
return false;
}
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config == null) {
return false;
}
// Set the "last selected" flag even if the app does not have permissions to modify this
// network config. Apps are allowed to connect to networks even if they don't have
// permission to modify it.
if (disableOthers) {
setLastSelectedNetwork(networkId);
}
if (!canModifyNetwork(config, uid, packageName)) {
Log.e(TAG, "UID " + uid + " package " + packageName
+ " does not have permission to update configuration "
+ config.getProfileKey());
return false;
}
if (!updateNetworkSelectionStatus(
networkId, WifiConfiguration.NetworkSelectionStatus.DISABLED_NONE)) {
return false;
}
saveToStore(true);
return true;
}
/**
* Disable a network using the public {@link WifiManager#disableNetwork(int)} API.
*
* @param networkId network ID of the network that needs the update.
* @param uid uid of the app requesting the update.
* @return true if it succeeds, false otherwise
*/
public boolean disableNetwork(int networkId, int uid, @NonNull String packageName) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Disabling network " + networkId);
}
if (!mWifiPermissionsUtil.doesUidBelongToCurrentUserOrDeviceOwner(uid)) {
Log.e(TAG, "UID " + uid + " package " + packageName
+ " not visible to the current user");
return false;
}
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config == null) {
return false;
}
// Reset the "last selected" flag even if the app does not have permissions to modify this
// network config.
if (networkId == mLastSelectedNetworkId) {
clearLastSelectedNetwork();
}
if (!canModifyNetwork(config, uid, packageName)) {
Log.e(TAG, "UID " + uid + " package " + packageName
+ " does not have permission to update configuration "
+ config.getProfileKey());
return false;
}
if (!updateNetworkSelectionStatus(
networkId, NetworkSelectionStatus.DISABLED_BY_WIFI_MANAGER)) {
return false;
}
saveToStore(true);
return true;
}
/**
* Changes the user's choice to allow auto-join using the
* {@link WifiManager#allowAutojoin(int, boolean)} API.
*
* @param networkId network ID of the network that needs the update.
* @param choice the choice to allow auto-join or not
* @return true if it succeeds, false otherwise
*/
public boolean allowAutojoin(int networkId, boolean choice) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Setting allowAutojoin to " + choice + " for netId " + networkId);
}
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config == null) {
Log.e(TAG, "allowAutojoin: Supplied networkId " + networkId
+ " has no matching config");
return false;
}
config.allowAutojoin = choice;
if (!choice) {
removeConnectChoiceFromAllNetworks(config.getProfileKey());
clearConnectChoiceInternal(config);
}
sendConfiguredNetworkChangedBroadcast(WifiManager.CHANGE_REASON_CONFIG_CHANGE);
if (!config.ephemeral) {
saveToStore(true);
}
return true;
}
/**
* Updates the last connected UID for the provided configuration.
*
* @param networkId network ID corresponding to the network.
* @param uid uid of the app requesting the connection.
* @return true if the network was found, false otherwise.
*/
private boolean updateLastConnectUid(int networkId, int uid) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Update network last connect UID for " + networkId);
}
if (!mWifiPermissionsUtil.doesUidBelongToCurrentUserOrDeviceOwner(uid)) {
Log.e(TAG, "UID " + uid + " not visible to the current user");
return false;
}
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config == null) {
return false;
}
config.lastConnectUid = uid;
return true;
}
/**
* Updates a network configuration after a successful connection to it.
*
* This method updates the following WifiConfiguration elements:
* 1. Set the |lastConnected| timestamp.
* 2. Increment |numAssociation| counter.
* 3. Clear the disable reason counters in the associated |NetworkSelectionStatus|.
* 4. Set the hasEverConnected| flag in the associated |NetworkSelectionStatus|.
* 5. Sets the status of network as |CURRENT|.
*
* @param networkId network ID corresponding to the network.
* @param shouldSetUserConnectChoice setup user connect choice on this network.
* @param rssi signal strength of the connected network.
* @return true if the network was found, false otherwise.
*/
public boolean updateNetworkAfterConnect(int networkId, boolean shouldSetUserConnectChoice,
int rssi) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Update network after connect for " + networkId);
}
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config == null) {
return false;
}
// Only record connection order for non-passpoint from user saved or suggestion.
if (!config.isPasspoint() && (config.fromWifiNetworkSuggestion || !config.ephemeral)) {
mLruConnectionTracker.addNetwork(config);
}
if (shouldSetUserConnectChoice) {
setUserConnectChoice(config.networkId, rssi);
}
config.lastConnected = mClock.getWallClockMillis();
config.numRebootsSinceLastUse = 0;
config.numAssociation++;
config.getNetworkSelectionStatus().clearDisableReasonCounter();
config.getNetworkSelectionStatus().setHasEverConnected(true);
setNetworkStatus(config, WifiConfiguration.Status.CURRENT);
saveToStore(false);
return true;
}
/**
* Set captive portal to be detected for this network.
* @param networkId
*/
public void noteCaptivePortalDetected(int networkId) {
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config != null) {
config.getNetworkSelectionStatus().setHasNeverDetectedCaptivePortal(false);
}
}
/**
* Updates a network configuration after disconnection from it.
*
* This method updates the following WifiConfiguration elements:
* 1. Set the |lastDisConnected| timestamp.
* 2. Sets the status of network back to |ENABLED|.
*
* @param networkId network ID corresponding to the network.
* @return true if the network was found, false otherwise.
*/
public boolean updateNetworkAfterDisconnect(int networkId) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Update network after disconnect for " + networkId);
}
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config == null) {
return false;
}
config.lastDisconnected = mClock.getWallClockMillis();
config.randomizedMacExpirationTimeMs = Math.max(config.randomizedMacExpirationTimeMs,
config.lastDisconnected + NON_PERSISTENT_MAC_WAIT_AFTER_DISCONNECT_MS);
// If the network hasn't been disabled, mark it back as
// enabled after disconnection.
if (config.status == WifiConfiguration.Status.CURRENT) {
setNetworkStatus(config, WifiConfiguration.Status.ENABLED);
}
saveToStore(false);
return true;
}
/**
* Set default GW MAC address for the provided network.
*
* @param networkId network ID corresponding to the network.
* @param macAddress MAC address of the gateway to be set.
* @return true if the network was found, false otherwise.
*/
public boolean setNetworkDefaultGwMacAddress(int networkId, String macAddress) {
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config == null) {
return false;
}
config.defaultGwMacAddress = macAddress;
return true;
}
/**
* Clear the {@link NetworkSelectionStatus#mCandidate},
* {@link NetworkSelectionStatus#mCandidateScore} &
* {@link NetworkSelectionStatus#mSeenInLastQualifiedNetworkSelection} for the provided network.
*
* This is invoked by Network Selector at the start of every selection procedure to clear all
* configured networks' scan-result-candidates.
*
* @param networkId network ID corresponding to the network.
* @return true if the network was found, false otherwise.
*/
public boolean clearNetworkCandidateScanResult(int networkId) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Clear network candidate scan result for " + networkId);
}
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config == null) {
return false;
}
config.getNetworkSelectionStatus().setCandidate(null);
config.getNetworkSelectionStatus().setCandidateScore(Integer.MIN_VALUE);
config.getNetworkSelectionStatus().setSeenInLastQualifiedNetworkSelection(false);
config.getNetworkSelectionStatus().setCandidateSecurityParams(null);
return true;
}
/**
* Set the {@link NetworkSelectionStatus#mCandidate},
* {@link NetworkSelectionStatus#mCandidateScore} &
* {@link NetworkSelectionStatus#mSeenInLastQualifiedNetworkSelection} for the provided network.
*
* This is invoked by Network Selector when it sees a network during network selection procedure
* to set the scan result candidate.
*
* @param networkId network ID corresponding to the network.
* @param scanResult Candidate ScanResult associated with this network.
* @param score Score assigned to the candidate.
* @param params Security params for this candidate.
* @return true if the network was found, false otherwise.
*/
public boolean setNetworkCandidateScanResult(int networkId, ScanResult scanResult, int score,
SecurityParams params) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Set network candidate scan result " + scanResult + " for " + networkId
+ " with security params " + params);
}
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config == null) {
Log.e(TAG, "Cannot find network for " + networkId);
return false;
}
config.getNetworkSelectionStatus().setCandidate(scanResult);
config.getNetworkSelectionStatus().setCandidateScore(score);
config.getNetworkSelectionStatus().setSeenInLastQualifiedNetworkSelection(true);
config.getNetworkSelectionStatus().setCandidateSecurityParams(params);
return true;
}
/**
* Set the {@link NetworkSelectionStatus#mLastUsedSecurityParams}.
*
* @param networkId network ID corresponding to the network.
* @param params Security params for this candidate.
* @return true if the network was found, false otherwise.
*/
public boolean setNetworkLastUsedSecurityParams(int networkId, SecurityParams params) {
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config == null) {
Log.e(TAG, "Cannot find network for " + networkId);
return false;
}
config.getNetworkSelectionStatus().setLastUsedSecurityParams(params);
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Update last used security param for " + config.getProfileKey()
+ " with security type " + params.getSecurityType());
}
return true;
}
/**
* Iterate through all the saved networks and remove the provided configuration from the
* {@link NetworkSelectionStatus#mConnectChoice} from them.
*
* This is invoked when a network is removed from our records.
*
* @param connectChoiceConfigKey ConfigKey corresponding to the network that is being removed.
*/
public void removeConnectChoiceFromAllNetworks(String connectChoiceConfigKey) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Removing connect choice from all networks " + connectChoiceConfigKey);
}
if (connectChoiceConfigKey == null) {
return;
}
for (WifiConfiguration config : getInternalConfiguredNetworks()) {
WifiConfiguration.NetworkSelectionStatus status = config.getNetworkSelectionStatus();
String connectChoice = status.getConnectChoice();
if (TextUtils.equals(connectChoice, connectChoiceConfigKey)) {
Log.d(TAG, "remove connect choice:" + connectChoice + " from " + config.SSID
+ " : " + config.networkId);
clearConnectChoiceInternal(config);
}
}
for (OnNetworkUpdateListener listener : mListeners) {
listener.onConnectChoiceRemoved(connectChoiceConfigKey);
}
}
/**
* Increments the number of no internet access reports in the provided network.
*
* @param networkId network ID corresponding to the network.
* @return true if the network was found, false otherwise.
*/
public boolean incrementNetworkNoInternetAccessReports(int networkId) {
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config == null) {
return false;
}
config.numNoInternetAccessReports++;
config.validatedInternetAccess = false;
return true;
}
/**
* Sets the internet access is validated or not in the provided network.
*
* @param networkId network ID corresponding to the network.
* @param validated Whether access is validated or not.
* @return true if the network was found, false otherwise.
*/
public boolean setNetworkValidatedInternetAccess(int networkId, boolean validated) {
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config == null) {
return false;
}
config.validatedInternetAccess = validated;
config.numNoInternetAccessReports = 0;
saveToStore(false);
return true;
}
/**
* Sets whether the internet access is expected or not in the provided network.
*
* @param networkId network ID corresponding to the network.
* @param expected Whether access is expected or not.
* @return true if the network was found, false otherwise.
*/
public boolean setNetworkNoInternetAccessExpected(int networkId, boolean expected) {
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config == null) {
return false;
}
config.noInternetAccessExpected = expected;
return true;
}
/**
* Helper method to clear out the {@link #mNextNetworkId} user/app network selection. This
* is done when either the corresponding network is either removed or disabled.
*/
public void clearLastSelectedNetwork() {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Clearing last selected network");
}
mLastSelectedNetworkId = WifiConfiguration.INVALID_NETWORK_ID;
mLastSelectedTimeStamp = NetworkSelectionStatus.INVALID_NETWORK_SELECTION_DISABLE_TIMESTAMP;
}
/**
* Helper method to mark a network as the last selected one by an app/user. This is set
* when an app invokes {@link #enableNetwork(int, boolean, int)} with |disableOthers| flag set.
* This is used by network selector to assign a special bonus during network selection.
*/
private void setLastSelectedNetwork(int networkId) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Setting last selected network to " + networkId);
}
mLastSelectedNetworkId = networkId;
mLastSelectedTimeStamp = mClock.getElapsedSinceBootMillis();
}
/**
* Retrieve the network Id corresponding to the last network that was explicitly selected by
* an app/user.
*
* @return network Id corresponding to the last selected network.
*/
public int getLastSelectedNetwork() {
return mLastSelectedNetworkId;
}
/**
* Retrieve the configKey corresponding to the last network that was explicitly selected by
* an app/user.
*
* @return network Id corresponding to the last selected network.
*/
public String getLastSelectedNetworkConfigKey() {
if (mLastSelectedNetworkId == WifiConfiguration.INVALID_NETWORK_ID) {
return "";
}
WifiConfiguration config = getInternalConfiguredNetwork(mLastSelectedNetworkId);
if (config == null) {
return "";
}
return config.getProfileKey();
}
/**
* Retrieve the time stamp at which a network was explicitly selected by an app/user.
*
* @return timestamp in milliseconds from boot when this was set.
*/
public long getLastSelectedTimeStamp() {
return mLastSelectedTimeStamp;
}
/**
* Helper method to get the scan detail cache entry {@link #mScanDetailCaches} for the provided
* network.
*
* @param networkId network ID corresponding to the network.
* @return existing {@link ScanDetailCache} entry if one exists or null.
*/
public ScanDetailCache getScanDetailCacheForNetwork(int networkId) {
return mScanDetailCaches.get(networkId);
}
/**
* Helper method to get or create a scan detail cache entry {@link #mScanDetailCaches} for
* the provided network.
*
* @param config configuration corresponding to the the network.
* @return existing {@link ScanDetailCache} entry if one exists or a new instance created for
* this network.
*/
private ScanDetailCache getOrCreateScanDetailCacheForNetwork(WifiConfiguration config) {
if (config == null) return null;
ScanDetailCache cache = getScanDetailCacheForNetwork(config.networkId);
if (cache == null && config.networkId != WifiConfiguration.INVALID_NETWORK_ID) {
cache = new ScanDetailCache(
config, SCAN_CACHE_ENTRIES_MAX_SIZE, SCAN_CACHE_ENTRIES_TRIM_SIZE);
mScanDetailCaches.put(config.networkId, cache);
}
return cache;
}
/**
* Saves the provided ScanDetail into the corresponding scan detail cache entry
* {@link #mScanDetailCaches} for the provided network.
*
* @param config configuration corresponding to the the network.
* @param scanDetail new scan detail instance to be saved into the cache.
*/
private void saveToScanDetailCacheForNetwork(
WifiConfiguration config, ScanDetail scanDetail) {
ScanResult scanResult = scanDetail.getScanResult();
WifiScoreCard.PerNetwork network = mWifiScoreCard.lookupNetwork(config.SSID);
network.addFrequency(scanResult.frequency);
ScanDetailCache scanDetailCache = getOrCreateScanDetailCacheForNetwork(config);
if (scanDetailCache == null) {
Log.e(TAG, "Could not allocate scan cache for " + config.getPrintableSsid());
return;
}
// Adding a new BSSID
if (config.ephemeral) {
// For an ephemeral Wi-Fi config, the ScanResult should be considered
// untrusted.
scanResult.untrusted = true;
}
// Add the scan detail to this network's scan detail cache.
scanDetailCache.put(scanDetail);
}
/**
* Retrieves a configured network corresponding to the provided scan detail if one exists.
*
* @param scanDetail ScanDetail instance to use for looking up the network.
* @return WifiConfiguration object representing the network corresponding to the scanDetail,
* null if none exists.
*/
public WifiConfiguration getSavedNetworkForScanDetail(ScanDetail scanDetail) {
ScanResult scanResult = scanDetail.getScanResult();
if (scanResult == null) {
Log.e(TAG, "No scan result found in scan detail");
return null;
}
return getSavedNetworkForScanResult(scanResult);
}
/**
* Retrieves a configured network corresponding to the provided scan result if one exists.
*
* @param scanResult ScanResult instance to use for looking up the network.
* @return WifiConfiguration object representing the network corresponding to the scanResult,
* null if none exists.
*/
public WifiConfiguration getSavedNetworkForScanResult(@NonNull ScanResult scanResult) {
WifiConfiguration config = null;
try {
config = mConfiguredNetworks.getByScanResultForCurrentUser(scanResult);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Failed to lookup network from config map", e);
}
if (config != null) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "getSavedNetworkFromScanResult Found " + config.getProfileKey()
+ " for " + scanResult.SSID + "[" + scanResult.capabilities + "]");
}
}
return config;
}
/**
* Caches the provided |scanDetail| into the corresponding scan detail cache entry
* {@link #mScanDetailCaches} for the retrieved network.
*
* @param scanDetail input a scanDetail from the scan result
*/
public void updateScanDetailCacheFromScanDetailForSavedNetwork(ScanDetail scanDetail) {
WifiConfiguration network = getSavedNetworkForScanDetail(scanDetail);
if (network == null) {
return;
}
saveToScanDetailCacheForNetwork(network, scanDetail);
}
/**
* Retrieves a configured network corresponding to the provided scan detail if one exists and
* caches the provided |scanDetail| into the corresponding scan detail cache entry
* {@link #mScanDetailCaches} for the retrieved network.
*
* @param scanDetail input a scanDetail from the scan result
* @return WifiConfiguration object representing the network corresponding to the scanDetail,
* null if none exists.
*/
public WifiConfiguration getSavedNetworkForScanDetailAndCache(ScanDetail scanDetail) {
WifiConfiguration network = getSavedNetworkForScanDetail(scanDetail);
if (network == null) {
return null;
}
saveToScanDetailCacheForNetwork(network, scanDetail);
// Cache DTIM values parsed from the beacon frame Traffic Indication Map (TIM)
// Information Element (IE), into the associated WifiConfigurations. Most of the
// time there is no TIM IE in the scan result (Probe Response instead of Beacon
// Frame), these scanResult DTIM's are negative and ignored.
// Used for metrics collection.
if (scanDetail.getNetworkDetail() != null
&& scanDetail.getNetworkDetail().getDtimInterval() > 0) {
network.dtimInterval = scanDetail.getNetworkDetail().getDtimInterval();
}
return createExternalWifiConfiguration(network, true, Process.WIFI_UID);
}
/**
* Update the scan detail cache associated with current connected network with latest
* RSSI value in the provided WifiInfo.
* This is invoked when we get an RSSI poll update after connection.
*
* @param info WifiInfo instance pointing to the current connected network.
*/
public void updateScanDetailCacheFromWifiInfo(WifiInfo info) {
WifiConfiguration config = getInternalConfiguredNetwork(info.getNetworkId());
ScanDetailCache scanDetailCache = getScanDetailCacheForNetwork(info.getNetworkId());
if (config != null && scanDetailCache != null) {
ScanDetail scanDetail = scanDetailCache.getScanDetail(info.getBSSID());
if (scanDetail != null) {
ScanResult result = scanDetail.getScanResult();
long previousSeen = result.seen;
int previousRssi = result.level;
// Update the scan result
scanDetail.setSeen();
result.level = info.getRssi();
// Average the RSSI value
long maxAge = SCAN_RESULT_MAXIMUM_AGE_MS;
long age = result.seen - previousSeen;
if (previousSeen > 0 && age > 0 && age < maxAge / 2) {
// Average the RSSI with previously seen instances of this scan result
double alpha = 0.5 - (double) age / (double) maxAge;
result.level = (int) ((double) result.level * (1 - alpha)
+ (double) previousRssi * alpha);
}
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Updating scan detail cache freq=" + result.frequency
+ " BSSID=" + result.BSSID
+ " RSSI=" + result.level
+ " for " + config.getProfileKey());
}
}
}
}
/**
* Save the ScanDetail to the ScanDetailCache of the given network. This is used
* by {@link PasspointNetworkNominator} for caching
* ScanDetail for newly created {@link WifiConfiguration} for Passpoint network.
*
* @param networkId The ID of the network to save ScanDetail to
* @param scanDetail The ScanDetail to cache
*/
public void updateScanDetailForNetwork(int networkId, ScanDetail scanDetail) {
WifiConfiguration network = getInternalConfiguredNetwork(networkId);
if (network == null) {
return;
}
saveToScanDetailCacheForNetwork(network, scanDetail);
}
/**
* Helper method to check if the 2 provided networks can be linked or not.
* Networks are considered for linking if:
* 1. Share the same GW MAC address.
* 2. Scan results for the networks have AP's with MAC address which differ only in the last
* nibble.
*
* @param network1 WifiConfiguration corresponding to network 1.
* @param network2 WifiConfiguration corresponding to network 2.
* @param scanDetailCache1 ScanDetailCache entry for network 1.
* @param scanDetailCache1 ScanDetailCache entry for network 2.
* @return true if the networks should be linked, false if the networks should be unlinked.
*/
private boolean shouldNetworksBeLinked(
WifiConfiguration network1, WifiConfiguration network2,
ScanDetailCache scanDetailCache1, ScanDetailCache scanDetailCache2) {
// Check if networks should not be linked due to credential mismatch
if (mContext.getResources().getBoolean(
R.bool.config_wifi_only_link_same_credential_configurations)) {
if (!TextUtils.equals(network1.preSharedKey, network2.preSharedKey)) {
return false;
}
}
// Skip VRRP MAC addresses since they are likely to correspond to different networks even if
// they match.
if ((network1.defaultGwMacAddress != null && network1.defaultGwMacAddress
.regionMatches(true, 0, VRRP_MAC_ADDRESS_PREFIX, 0,
VRRP_MAC_ADDRESS_PREFIX.length()))
|| (network2.defaultGwMacAddress != null && network2.defaultGwMacAddress
.regionMatches(true, 0, VRRP_MAC_ADDRESS_PREFIX, 0,
VRRP_MAC_ADDRESS_PREFIX.length()))) {
return false;
}
// Check if networks should be linked due to default gateway match
if (network1.defaultGwMacAddress != null && network2.defaultGwMacAddress != null) {
// If both default GW are known, link only if they are equal
if (network1.defaultGwMacAddress.equalsIgnoreCase(network2.defaultGwMacAddress)) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "shouldNetworksBeLinked link due to same gw " + network2.SSID
+ " and " + network1.SSID + " GW " + network1.defaultGwMacAddress);
}
return true;
}
return false;
}
// We do not know BOTH default gateways yet, but if the first 16 ASCII characters of BSSID
// match then we can assume this is a DBDC with the same gateway. Once both gateways become
// known, we will unlink the networks if it turns out the gateways are actually different.
if (!mContext.getResources().getBoolean(
R.bool.config_wifiAllowLinkingUnknownDefaultGatewayConfigurations)) {
return false;
}
if (scanDetailCache1 != null && scanDetailCache2 != null) {
for (String abssid : scanDetailCache1.keySet()) {
for (String bbssid : scanDetailCache2.keySet()) {
if (abssid.regionMatches(
true, 0, bbssid, 0, LINK_CONFIGURATION_BSSID_MATCH_LENGTH)) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "shouldNetworksBeLinked link due to DBDC BSSID match "
+ network2.SSID + " and " + network1.SSID
+ " bssida " + abssid + " bssidb " + bbssid);
}
return true;
}
}
}
}
return false;
}
/**
* Helper methods to link 2 networks together.
*
* @param network1 WifiConfiguration corresponding to network 1.
* @param network2 WifiConfiguration corresponding to network 2.
*/
private void linkNetworks(WifiConfiguration network1, WifiConfiguration network2) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "linkNetworks will link " + network2.getProfileKey()
+ " and " + network1.getProfileKey());
}
if (network2.linkedConfigurations == null) {
network2.linkedConfigurations = new HashMap<>();
}
if (network1.linkedConfigurations == null) {
network1.linkedConfigurations = new HashMap<>();
}
// TODO (b/30638473): This needs to become a set instead of map, but it will need
// public interface changes and need some migration of existing store data.
network2.linkedConfigurations.put(network1.getProfileKey(), 1);
network1.linkedConfigurations.put(network2.getProfileKey(), 1);
}
/**
* Helper methods to unlink 2 networks from each other.
*
* @param network1 WifiConfiguration corresponding to network 1.
* @param network2 WifiConfiguration corresponding to network 2.
*/
private void unlinkNetworks(WifiConfiguration network1, WifiConfiguration network2) {
if (network2.linkedConfigurations != null
&& (network2.linkedConfigurations.get(network1.getProfileKey()) != null)) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "unlinkNetworks un-link " + network1.getProfileKey()
+ " from " + network2.getProfileKey());
}
network2.linkedConfigurations.remove(network1.getProfileKey());
}
if (network1.linkedConfigurations != null
&& (network1.linkedConfigurations.get(network2.getProfileKey()) != null)) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "unlinkNetworks un-link " + network2.getProfileKey()
+ " from " + network1.getProfileKey());
}
network1.linkedConfigurations.remove(network2.getProfileKey());
}
}
/**
* This method runs through all the saved networks and checks if the provided network can be
* linked with any of them.
*
* @param config WifiConfiguration object corresponding to the network that needs to be
* checked for potential links.
*/
private void attemptNetworkLinking(WifiConfiguration config) {
// Only link WPA_PSK config.
if (!config.isSecurityType(WifiConfiguration.SECURITY_TYPE_PSK)) {
return;
}
ScanDetailCache scanDetailCache = getScanDetailCacheForNetwork(config.networkId);
// Ignore configurations with large number of BSSIDs.
if (scanDetailCache != null
&& scanDetailCache.size() > LINK_CONFIGURATION_MAX_SCAN_CACHE_ENTRIES) {
return;
}
for (WifiConfiguration linkConfig : getInternalConfiguredNetworks()) {
if (linkConfig.getProfileKey().equals(config.getProfileKey())) {
continue;
}
if (linkConfig.ephemeral) {
continue;
}
if (!linkConfig.getNetworkSelectionStatus().isNetworkEnabled()) {
continue;
}
// Network Selector will be allowed to dynamically jump from a linked configuration
// to another, hence only link configurations that have WPA_PSK security type.
if (!linkConfig.isSecurityType(WifiConfiguration.SECURITY_TYPE_PSK)) {
continue;
}
ScanDetailCache linkScanDetailCache =
getScanDetailCacheForNetwork(linkConfig.networkId);
// Ignore configurations with large number of BSSIDs.
if (linkScanDetailCache != null
&& linkScanDetailCache.size() > LINK_CONFIGURATION_MAX_SCAN_CACHE_ENTRIES) {
continue;
}
// Check if the networks should be linked/unlinked.
if (shouldNetworksBeLinked(
config, linkConfig, scanDetailCache, linkScanDetailCache)) {
linkNetworks(config, linkConfig);
} else {
unlinkNetworks(config, linkConfig);
}
}
}
/**
* Retrieves a list of all the saved hidden networks for scans
*
* Hidden network list sent to the firmware has limited size. If there are a lot of saved
* networks, this list will be truncated and we might end up not sending the networks
* with the highest chance of connecting to the firmware.
* So, re-sort the network list based on the frequency of connection to those networks
* and whether it was last seen in the scan results.
*
* @param autoJoinOnly retrieve hidden network autojoin enabled only.
* @return list of hidden networks in the order of priority.
*/
public List<WifiScanner.ScanSettings.HiddenNetwork> retrieveHiddenNetworkList(
boolean autoJoinOnly) {
List<WifiScanner.ScanSettings.HiddenNetwork> hiddenList = new ArrayList<>();
List<WifiConfiguration> networks = getConfiguredNetworks();
// Remove any non hidden networks.
networks.removeIf(config -> !config.hiddenSSID);
networks.sort(mScanListComparator);
// The most frequently connected network has the highest priority now.
for (WifiConfiguration config : networks) {
if (!autoJoinOnly || config.allowAutojoin) {
hiddenList.add(new WifiScanner.ScanSettings.HiddenNetwork(config.SSID));
}
}
return hiddenList;
}
/**
* Check if the provided network was temporarily disabled by the user and still blocked.
*
* @param network Input can be SSID or FQDN. And caller must ensure that the SSID passed thru
* this API matched the WifiConfiguration.SSID rules, and thus be surrounded by
* quotes.
* @return true if network is blocking, otherwise false.
*/
public boolean isNetworkTemporarilyDisabledByUser(String network) {
if (mUserTemporarilyDisabledList.isLocked(network)) {
return true;
}
mUserTemporarilyDisabledList.remove(network);
return false;
}
/**
* Check if the provided network should be disabled because it's a non-carrier-merged network.
* @param config WifiConfiguration
* @return true if the network is a non-carrier-merged network and it should be disabled,
* otherwise false.
*/
public boolean isNonCarrierMergedNetworkTemporarilyDisabled(
@NonNull WifiConfiguration config) {
return mNonCarrierMergedNetworksStatusTracker.isNetworkDisabled(config);
}
/**
* User temporarily disable a network and will be block to auto-join when network is still
* nearby.
*
* The network will be re-enabled when:
* a) User select to connect the network.
* b) The network is not in range for {@link #USER_DISCONNECT_NETWORK_BLOCK_EXPIRY_MS}
* c) The maximum disable duration configured by
* config_wifiAllNonCarrierMergedWifiMaxDisableDurationMinutes has passed.
* d) Toggle wifi off, reset network settings or device reboot.
*
* @param network Input can be SSID or FQDN. And caller must ensure that the SSID passed thru
* this API matched the WifiConfiguration.SSID rules, and thus be surrounded by
* quotes.
* uid UID of the calling process.
*/
public void userTemporarilyDisabledNetwork(String network, int uid) {
int maxDisableDurationMinutes = mContext.getResources().getInteger(R.integer
.config_wifiAllNonCarrierMergedWifiMaxDisableDurationMinutes);
mUserTemporarilyDisabledList.add(network, USER_DISCONNECT_NETWORK_BLOCK_EXPIRY_MS,
maxDisableDurationMinutes * 60 * 1000);
Log.d(TAG, "Temporarily disable network: " + network + " uid=" + uid + " num="
+ mUserTemporarilyDisabledList.size() + ", maxDisableDurationMinutes:"
+ maxDisableDurationMinutes);
removeUserChoiceFromDisabledNetwork(network, uid);
saveToStore(false);
}
/**
* Temporarily disable visible and configured networks except for carrier merged networks for
* the given subscriptionId.
* @param subscriptionId
*/
public void startRestrictingAutoJoinToSubscriptionId(int subscriptionId) {
int minDisableDurationMinutes = mContext.getResources().getInteger(R.integer
.config_wifiAllNonCarrierMergedWifiMinDisableDurationMinutes);
int maxDisableDurationMinutes = mContext.getResources().getInteger(R.integer
.config_wifiAllNonCarrierMergedWifiMaxDisableDurationMinutes);
localLog("startRestrictingAutoJoinToSubscriptionId: " + subscriptionId
+ " minDisableDurationMinutes:" + minDisableDurationMinutes
+ " maxDisableDurationMinutes:" + maxDisableDurationMinutes);
long maxDisableDurationMs = maxDisableDurationMinutes * 60 * 1000;
// do a clear to make sure we start at a clean state.
mNonCarrierMergedNetworksStatusTracker.clear();
mNonCarrierMergedNetworksStatusTracker.disableAllNonCarrierMergedNetworks(subscriptionId,
minDisableDurationMinutes * 60 * 1000,
maxDisableDurationMs);
for (WifiConfiguration config : getInternalConfiguredNetworks()) {
ScanDetailCache scanDetailCache = getScanDetailCacheForNetwork(config.networkId);
if (scanDetailCache == null) {
continue;
}
ScanResult scanResult = scanDetailCache.getMostRecentScanResult();
if (scanResult == null) {
continue;
}
if (mClock.getWallClockMillis() - scanResult.seen
< NON_CARRIER_MERGED_NETWORKS_SCAN_CACHE_QUERY_DURATION_MS) {
// do not disable if this is a carrier-merged-network with the given subscriptionId
if (config.carrierMerged && config.subscriptionId == subscriptionId) {
continue;
}
mNonCarrierMergedNetworksStatusTracker.temporarilyDisableNetwork(config,
USER_DISCONNECT_NETWORK_BLOCK_EXPIRY_MS, maxDisableDurationMs);
}
}
}
/**
* Resets the effects of startTemporarilyDisablngAllNonCarrierMergedWifi.
*/
public void stopRestrictingAutoJoinToSubscriptionId() {
mNonCarrierMergedNetworksStatusTracker.clear();
}
/**
* Update the user temporarily disabled network list with networks in range.
* @param networks networks in range in String format, FQDN or SSID. And caller must ensure
* that the SSID passed thru this API matched the WifiConfiguration.SSID rules,
* and thus be surrounded by quotes.
*/
public void updateUserDisabledList(List<String> networks) {
mUserTemporarilyDisabledList.update(new HashSet<>(networks));
mNonCarrierMergedNetworksStatusTracker.update(new HashSet<>(networks));
}
private void removeUserChoiceFromDisabledNetwork(
@NonNull String network, int uid) {
for (WifiConfiguration config : getInternalConfiguredNetworks()) {
if (TextUtils.equals(config.SSID, network) || TextUtils.equals(config.FQDN, network)) {
if (mWifiPermissionsUtil.checkNetworkSettingsPermission(uid)) {
mWifiMetrics.logUserActionEvent(
UserActionEvent.EVENT_DISCONNECT_WIFI, config.networkId);
}
removeConnectChoiceFromAllNetworks(config.getProfileKey());
}
}
}
/**
* User enabled network manually, maybe trigger by user select to connect network.
* @param networkId enabled network id.
* @return true if the operation succeeded, false otherwise.
*/
public boolean userEnabledNetwork(int networkId) {
WifiConfiguration configuration = getInternalConfiguredNetwork(networkId);
if (configuration == null) {
return false;
}
final String network;
if (configuration.isPasspoint()) {
network = configuration.FQDN;
} else {
network = configuration.SSID;
}
mUserTemporarilyDisabledList.remove(network);
mWifiBlocklistMonitor.clearBssidBlocklistForSsid(configuration.SSID);
Log.d(TAG, "Enable disabled network: " + network + " num="
+ mUserTemporarilyDisabledList.size());
return true;
}
/**
* Clear all user temporarily disabled networks.
*/
public void clearUserTemporarilyDisabledList() {
mUserTemporarilyDisabledList.clear();
}
/**
* Resets all sim networks state.
*/
public void resetSimNetworks() {
if (mVerboseLoggingEnabled) localLog("resetSimNetworks");
for (WifiConfiguration config : getInternalConfiguredNetworks()) {
if (config.enterpriseConfig == null
|| !config.enterpriseConfig.isAuthenticationSimBased()) {
continue;
}
if (config.enterpriseConfig.getEapMethod() == WifiEnterpriseConfig.Eap.PEAP) {
Pair<String, String> currentIdentity =
mWifiCarrierInfoManager.getSimIdentity(config);
if (mVerboseLoggingEnabled) {
Log.d(TAG, "New identity for config " + config + ": " + currentIdentity);
}
// Update the loaded config
if (currentIdentity == null) {
Log.d(TAG, "Identity is null");
} else {
config.enterpriseConfig.setIdentity(currentIdentity.first);
}
// do not reset anonymous identity since it may be dependent on user-entry
// (i.e. cannot re-request on every reboot/SIM re-entry)
} else {
// reset identity as well: supplicant will ask us for it
config.enterpriseConfig.setIdentity("");
if (!WifiCarrierInfoManager.isAnonymousAtRealmIdentity(
config.enterpriseConfig.getAnonymousIdentity())) {
config.enterpriseConfig.setAnonymousIdentity("");
}
}
}
}
/**
* Clear all ephemeral carrier networks from the app without carrier privilege, which leads to
* a disconnection.
* Disconnection and removing networks installed by privileged apps is handled by will be
* cleaned when privilege revokes.
*/
public void removeEphemeralCarrierNetworks(Set<String> carrierPrivilegedPackages) {
if (mVerboseLoggingEnabled) localLog("removeEphemeralCarrierNetwork");
WifiConfiguration[] copiedConfigs =
mConfiguredNetworks.valuesForAllUsers().toArray(new WifiConfiguration[0]);
for (WifiConfiguration config : copiedConfigs) {
if (!config.ephemeral
|| config.subscriptionId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
continue;
}
if (carrierPrivilegedPackages.contains(config.creatorName)) {
continue;
}
removeNetwork(config.networkId, config.creatorUid, config.creatorName);
}
}
/**
* Helper method to perform the following operations during user switch/unlock:
* - Remove private networks of the old user.
* - Load from the new user store file.
* - Save the store files again to migrate any user specific networks from the shared store
* to user store.
* This method assumes the user store is visible (i.e CE storage is unlocked). So, the caller
* should ensure that the stores are accessible before invocation.
*
* @param userId The identifier of the new foreground user, after the unlock or switch.
*/
private void handleUserUnlockOrSwitch(int userId) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Loading from store after user switch/unlock for " + userId);
}
// Switch out the user store file.
if (loadFromUserStoreAfterUnlockOrSwitch(userId)) {
saveToStore(true);
mPendingUnlockStoreRead = false;
}
}
/**
* Handles the switch to a different foreground user:
* - Flush the current state to the old user's store file.
* - Switch the user specific store file.
* - Reload the networks from the store files (shared & user).
* - Write the store files to move any user specific private networks from shared store to user
* store.
*
* Need to be called when {@link com.android.server.SystemService#onUserSwitching} is invoked.
*
* @param userId The identifier of the new foreground user, after the switch.
* @return List of network ID's of all the private networks of the old user which will be
* removed from memory.
*/
public Set<Integer> handleUserSwitch(int userId) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Handling user switch for " + userId);
}
if (userId == mCurrentUserId) {
Log.w(TAG, "User already in foreground " + userId);
return new HashSet<>();
}
if (mPendingStoreRead) {
Log.w(TAG, "User switch before store is read!");
mConfiguredNetworks.setNewUser(userId);
mCurrentUserId = userId;
// Reset any state from previous user unlock.
mDeferredUserUnlockRead = false;
// Cannot read data from new user's CE store file before they log-in.
mPendingUnlockStoreRead = true;
return new HashSet<>();
}
if (mUserManager.isUserUnlockingOrUnlocked(UserHandle.of(mCurrentUserId))) {
saveToStore(true);
}
// Remove any private networks of the old user before switching the userId.
Set<Integer> removedNetworkIds = clearInternalDataForUser(mCurrentUserId);
mConfiguredNetworks.setNewUser(userId);
mCurrentUserId = userId;
if (mUserManager.isUserUnlockingOrUnlocked(UserHandle.of(mCurrentUserId))) {
handleUserUnlockOrSwitch(mCurrentUserId);
// only handle the switching of unlocked users in {@link WifiCarrierInfoManager}.
mWifiCarrierInfoManager.onUnlockedUserSwitching(mCurrentUserId);
} else {
// Cannot read data from new user's CE store file before they log-in.
mPendingUnlockStoreRead = true;
Log.i(TAG, "Waiting for user unlock to load from store");
}
return removedNetworkIds;
}
/**
* Handles the unlock of foreground user. This maybe needed to read the store file if the user's
* CE storage is not visible when {@link #handleUserSwitch(int)} is invoked.
*
* Need to be called when {@link com.android.server.SystemService#onUserUnlocking} is invoked.
*
* @param userId The identifier of the user that unlocked.
*/
public void handleUserUnlock(int userId) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Handling user unlock for " + userId);
}
if (userId != mCurrentUserId) {
Log.e(TAG, "Ignore user unlock for non current user " + userId);
return;
}
if (mPendingStoreRead) {
Log.w(TAG, "Ignore user unlock until store is read!");
mDeferredUserUnlockRead = true;
return;
}
if (mPendingUnlockStoreRead) {
handleUserUnlockOrSwitch(mCurrentUserId);
}
}
/**
* Handles the stop of foreground user. This is needed to write the store file to flush
* out any pending data before the user's CE store storage is unavailable.
*
* Need to be called when {@link com.android.server.SystemService#onUserStopping} is invoked.
*
* @param userId The identifier of the user that stopped.
*/
public void handleUserStop(int userId) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Handling user stop for " + userId);
}
if (userId == mCurrentUserId
&& mUserManager.isUserUnlockingOrUnlocked(UserHandle.of(mCurrentUserId))) {
saveToStore(true);
clearInternalDataForUser(mCurrentUserId);
}
}
/**
* Helper method to clear internal databases.
* This method clears the:
* - List of configured networks.
* - Map of scan detail caches.
* - List of deleted ephemeral networks.
*/
private void clearInternalData() {
localLog("clearInternalData: Clearing all internal data");
mConfiguredNetworks.clear();
mUserTemporarilyDisabledList.clear();
mNonCarrierMergedNetworksStatusTracker.clear();
mRandomizedMacAddressMapping.clear();
mScanDetailCaches.clear();
clearLastSelectedNetwork();
}
/**
* Helper method to clear internal databases of the specified user.
* This method clears the:
* - Private configured configured networks of the specified user.
* - Map of scan detail caches.
* - List of deleted ephemeral networks.
*
* @return List of network ID's of all the private networks of the old user which will be
* removed from memory.
*/
private Set<Integer> clearInternalDataForUser(int user) {
localLog("clearInternalUserData: Clearing user internal data for " + user);
Set<Integer> removedNetworkIds = new HashSet<>();
// Remove any private networks of the old user before switching the userId.
for (WifiConfiguration config : getConfiguredNetworks()) {
if ((!config.shared
&& mWifiPermissionsUtil.doesUidBelongToUser(config.creatorUid, user))
|| config.ephemeral) {
removedNetworkIds.add(config.networkId);
localLog("clearInternalUserData: removed config."
+ " netId=" + config.networkId
+ " configKey=" + config.getProfileKey());
mConfiguredNetworks.remove(config.networkId);
for (OnNetworkUpdateListener listener : mListeners) {
listener.onNetworkRemoved(
createExternalWifiConfiguration(config, true, Process.WIFI_UID));
}
}
}
if (!removedNetworkIds.isEmpty()) {
sendConfiguredNetworkChangedBroadcast(WifiManager.CHANGE_REASON_REMOVED);
}
mUserTemporarilyDisabledList.clear();
mNonCarrierMergedNetworksStatusTracker.clear();
mScanDetailCaches.clear();
clearLastSelectedNetwork();
return removedNetworkIds;
}
/**
* Helper function to populate the internal (in-memory) data from the retrieved shared store
* (file) data.
*
* @param configurations list of configurations retrieved from store.
*/
private void loadInternalDataFromSharedStore(
List<WifiConfiguration> configurations,
Map<String, String> macAddressMapping) {
long supportedFeatures = mWifiInjector.getActiveModeWarden()
.getPrimaryClientModeManager().getSupportedFeatures();
for (WifiConfiguration configuration : configurations) {
if (!WifiConfigurationUtil.validate(
configuration, supportedFeatures, WifiConfigurationUtil.VALIDATE_FOR_ADD)) {
Log.e(TAG, "Skipping malformed network from shared store: " + configuration);
continue;
}
WifiConfiguration existingConfiguration = getInternalConfiguredNetwork(configuration);
if (null != existingConfiguration) {
Log.d(TAG, "Merging network from shared store "
+ configuration.getProfileKey());
mergeWithInternalWifiConfiguration(existingConfiguration, configuration);
continue;
}
configuration.networkId = mNextNetworkId++;
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Adding network from shared store "
+ configuration.getProfileKey());
}
try {
mConfiguredNetworks.put(configuration);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Failed to add network to config map", e);
}
}
mRandomizedMacAddressMapping.putAll(macAddressMapping);
}
/**
* Helper function to populate the internal (in-memory) data from the retrieved user store
* (file) data.
*
* @param configurations list of configurations retrieved from store.
*/
private void loadInternalDataFromUserStore(List<WifiConfiguration> configurations) {
long supportedFeatures = mWifiInjector.getActiveModeWarden()
.getPrimaryClientModeManager().getSupportedFeatures();
for (WifiConfiguration configuration : configurations) {
if (!WifiConfigurationUtil.validate(
configuration, supportedFeatures, WifiConfigurationUtil.VALIDATE_FOR_ADD)) {
Log.e(TAG, "Skipping malformed network from user store: " + configuration);
continue;
}
WifiConfiguration existingConfiguration = getInternalConfiguredNetwork(configuration);
if (null != existingConfiguration) {
Log.d(TAG, "Merging network from user store "
+ configuration.getProfileKey());
mergeWithInternalWifiConfiguration(existingConfiguration, configuration);
continue;
}
configuration.networkId = mNextNetworkId++;
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Adding network from user store "
+ configuration.getProfileKey());
}
try {
mConfiguredNetworks.put(configuration);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Failed to add network to config map", e);
}
if (configuration.isMostRecentlyConnected) {
mLruConnectionTracker.addNetwork(configuration);
}
}
}
/**
* Initializes the randomized MAC address for an internal WifiConfiguration depending on
* whether it should use non-persistent randomization.
* @param config
*/
private void initRandomizedMacForInternalConfig(WifiConfiguration internalConfig) {
MacAddress randomizedMac = shouldUseNonPersistentRandomization(internalConfig)
? MacAddressUtils.createRandomUnicastAddress()
: getPersistentMacAddress(internalConfig);
if (randomizedMac != null) {
setRandomizedMacAddress(internalConfig, randomizedMac);
}
}
/**
* Assign randomized MAC addresses for configured networks.
* This is needed to generate persistent randomized MAC address for existing networks when
* a device updates to Q+ for the first time since we are not calling addOrUpdateNetwork when
* we load configuration at boot.
*/
private void generateRandomizedMacAddresses() {
for (WifiConfiguration config : getInternalConfiguredNetworks()) {
if (DEFAULT_MAC_ADDRESS.equals(config.getRandomizedMacAddress())) {
initRandomizedMacForInternalConfig(config);
}
}
}
/**
* Helper function to populate the internal (in-memory) data from the retrieved stores (file)
* data.
* This method:
* 1. Clears all existing internal data.
* 2. Sends out the networks changed broadcast after loading all the data.
*
* @param sharedConfigurations list of network configurations retrieved from shared store.
* @param userConfigurations list of network configurations retrieved from user store.
* @param macAddressMapping
*/
private void loadInternalData(
List<WifiConfiguration> sharedConfigurations,
List<WifiConfiguration> userConfigurations,
Map<String, String> macAddressMapping) {
// Clear out all the existing in-memory lists and load the lists from what was retrieved
// from the config store.
clearInternalData();
loadInternalDataFromSharedStore(sharedConfigurations, macAddressMapping);
loadInternalDataFromUserStore(userConfigurations);
generateRandomizedMacAddresses();
if (mConfiguredNetworks.sizeForAllUsers() == 0) {
Log.w(TAG, "No stored networks found.");
}
// reset identity & anonymous identity for networks using SIM-based authentication
// on load (i.e. boot) so that if the user changed SIMs while the device was powered off,
// we do not reuse stale credentials that would lead to authentication failure.
resetSimNetworks();
sendConfiguredNetworkChangedBroadcast(WifiManager.CHANGE_REASON_ADDED);
mPendingStoreRead = false;
}
/**
* Helper method to handle any config store errors on user builds vs other debuggable builds.
*/
private boolean handleConfigStoreFailure(boolean onlyUserStore) {
// On eng/userdebug builds, return failure to leave the device in a debuggable state.
if (!mBuildProperties.isUserBuild()) return false;
// On user builds, ignore the failure and let the user create new networks.
Log.w(TAG, "Ignoring config store errors on user build");
if (!onlyUserStore) {
loadInternalData(Collections.emptyList(), Collections.emptyList(),
Collections.emptyMap());
} else {
loadInternalDataFromUserStore(Collections.emptyList());
}
return true;
}
/**
* Read the config store and load the in-memory lists from the store data retrieved and sends
* out the networks changed broadcast.
*
* This reads all the network configurations from:
* 1. Shared WifiConfigStore.xml
* 2. User WifiConfigStore.xml
*
* @return true on success or not needed (fresh install), false otherwise.
*/
public boolean loadFromStore() {
// If the user unlock comes in before we load from store, which means the user store have
// not been setup yet for the current user. Setup the user store before the read so that
// configurations for the current user will also being loaded.
if (mDeferredUserUnlockRead) {
Log.i(TAG, "Handling user unlock before loading from store.");
List<WifiConfigStore.StoreFile> userStoreFiles =
WifiConfigStore.createUserFiles(
mCurrentUserId, mFrameworkFacade.isNiapModeOn(mContext));
if (userStoreFiles == null) {
Log.wtf(TAG, "Failed to create user store files");
return false;
}
mWifiConfigStore.setUserStores(userStoreFiles);
mDeferredUserUnlockRead = false;
}
try {
mWifiConfigStore.read();
} catch (IOException | IllegalStateException e) {
Log.wtf(TAG, "Reading from new store failed. All saved networks are lost!", e);
return handleConfigStoreFailure(false);
} catch (XmlPullParserException e) {
Log.wtf(TAG, "XML deserialization of store failed. All saved networks are lost!", e);
return handleConfigStoreFailure(false);
}
loadInternalData(mNetworkListSharedStoreData.getConfigurations(),
mNetworkListUserStoreData.getConfigurations(),
mRandomizedMacStoreData.getMacMapping());
return true;
}
/**
* Read the user config store and load the in-memory lists from the store data retrieved and
* sends out the networks changed broadcast.
* This should be used for all user switches/unlocks to only load networks from the user
* specific store and avoid reloading the shared networks.
*
* This reads all the network configurations from:
* 1. User WifiConfigStore.xml
*
* @param userId The identifier of the foreground user.
* @return true on success, false otherwise.
*/
private boolean loadFromUserStoreAfterUnlockOrSwitch(int userId) {
try {
List<WifiConfigStore.StoreFile> userStoreFiles =
WifiConfigStore.createUserFiles(
userId, mFrameworkFacade.isNiapModeOn(mContext));
if (userStoreFiles == null) {
Log.e(TAG, "Failed to create user store files");
return false;
}
mWifiConfigStore.switchUserStoresAndRead(userStoreFiles);
} catch (IOException | IllegalStateException e) {
Log.wtf(TAG, "Reading from new store failed. All saved private networks are lost!", e);
return handleConfigStoreFailure(true);
} catch (XmlPullParserException e) {
Log.wtf(TAG, "XML deserialization of store failed. All saved private networks are "
+ "lost!", e);
return handleConfigStoreFailure(true);
}
loadInternalDataFromUserStore(mNetworkListUserStoreData.getConfigurations());
return true;
}
/**
* Save the current snapshot of the in-memory lists to the config store.
*
* @param forceWrite Whether the write needs to be forced or not.
* @return Whether the write was successful or not, this is applicable only for force writes.
*/
public boolean saveToStore(boolean forceWrite) {
if (mPendingStoreRead) {
Log.e(TAG, "Cannot save to store before store is read!");
return false;
}
ArrayList<WifiConfiguration> sharedConfigurations = new ArrayList<>();
ArrayList<WifiConfiguration> userConfigurations = new ArrayList<>();
// List of network IDs for legacy Passpoint configuration to be removed.
List<Integer> legacyPasspointNetId = new ArrayList<>();
for (WifiConfiguration config : mConfiguredNetworks.valuesForAllUsers()) {
// Ignore ephemeral networks and non-legacy Passpoint configurations.
if (config.ephemeral || (config.isPasspoint() && !config.isLegacyPasspointConfig)) {
continue;
}
// Migrate the legacy Passpoint configurations owned by the current user to
// {@link PasspointManager}.
if (config.isLegacyPasspointConfig && mWifiPermissionsUtil
.doesUidBelongToUser(config.creatorUid, mCurrentUserId)) {
legacyPasspointNetId.add(config.networkId);
// Migrate the legacy Passpoint configuration and add it to PasspointManager.
if (!PasspointManager.addLegacyPasspointConfig(config)) {
Log.e(TAG, "Failed to migrate legacy Passpoint config: " + config.FQDN);
}
// This will prevent adding |config| to the |sharedConfigurations|.
continue;
}
config.isMostRecentlyConnected =
mLruConnectionTracker.isMostRecentlyConnected(config);
// We push all shared networks & private networks not belonging to the current
// user to the shared store. Ideally, private networks for other users should
// not even be in memory,
// But, this logic is in place to deal with store migration from N to O
// because all networks were previously stored in a central file. We cannot
// write these private networks to the user specific store until the corresponding
// user logs in.
if (config.shared || !mWifiPermissionsUtil
.doesUidBelongToUser(config.creatorUid, mCurrentUserId)) {
sharedConfigurations.add(config);
} else {
userConfigurations.add(config);
}
}
// Remove the configurations for migrated Passpoint configurations.
for (int networkId : legacyPasspointNetId) {
mConfiguredNetworks.remove(networkId);
}
// Setup store data for write.
mNetworkListSharedStoreData.setConfigurations(sharedConfigurations);
mNetworkListUserStoreData.setConfigurations(userConfigurations);
mRandomizedMacStoreData.setMacMapping(mRandomizedMacAddressMapping);
try {
mWifiConfigStore.write(forceWrite);
} catch (IOException | IllegalStateException e) {
Log.wtf(TAG, "Writing to store failed. Saved networks maybe lost!", e);
return false;
} catch (XmlPullParserException e) {
Log.wtf(TAG, "XML serialization for store failed. Saved networks maybe lost!", e);
return false;
}
return true;
}
/**
* Helper method for logging into local log buffer.
*/
private void localLog(String s) {
if (mLocalLog != null) {
mLocalLog.log(s);
}
}
/**
* Dump the local log buffer and other internal state of WifiConfigManager.
*/
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("Dump of WifiConfigManager");
pw.println("WifiConfigManager - Log Begin ----");
mLocalLog.dump(fd, pw, args);
pw.println("WifiConfigManager - Log End ----");
pw.println("WifiConfigManager - Configured networks Begin ----");
for (WifiConfiguration network : getInternalConfiguredNetworks()) {
pw.println(network);
}
pw.println("WifiConfigManager - Configured networks End ----");
pw.println("WifiConfigManager - ConfigurationMap Begin ----");
mConfiguredNetworks.dump(fd, pw, args);
pw.println("WifiConfigManager - ConfigurationMap End ----");
pw.println("WifiConfigManager - Next network ID to be allocated " + mNextNetworkId);
pw.println("WifiConfigManager - Last selected network ID " + mLastSelectedNetworkId);
pw.println("WifiConfigManager - PNO scan frequency culling enabled = "
+ mContext.getResources().getBoolean(R.bool.config_wifiPnoFrequencyCullingEnabled));
pw.println("WifiConfigManager - PNO scan recency sorting enabled = "
+ mContext.getResources().getBoolean(R.bool.config_wifiPnoRecencySortingEnabled));
mWifiConfigStore.dump(fd, pw, args);
mWifiCarrierInfoManager.dump(fd, pw, args);
mNonCarrierMergedNetworksStatusTracker.dump(fd, pw, args);
}
/**
* Returns true if the given uid has permission to add, update or remove proxy settings
*/
private boolean canModifyProxySettings(int uid, String packageName) {
final boolean isAdmin = mWifiPermissionsUtil.isAdmin(uid, packageName);
final boolean hasNetworkSettingsPermission =
mWifiPermissionsUtil.checkNetworkSettingsPermission(uid);
final boolean hasNetworkSetupWizardPermission =
mWifiPermissionsUtil.checkNetworkSetupWizardPermission(uid);
final boolean hasNetworkManagedProvisioningPermission =
mWifiPermissionsUtil.checkNetworkManagedProvisioningPermission(uid);
// If |uid| corresponds to the admin, allow all modifications.
if (isAdmin || hasNetworkSettingsPermission
|| hasNetworkSetupWizardPermission || hasNetworkManagedProvisioningPermission) {
return true;
}
if (mVerboseLoggingEnabled) {
Log.v(TAG, "UID: " + uid + " cannot modify WifiConfiguration proxy settings."
+ " hasNetworkSettings=" + hasNetworkSettingsPermission
+ " hasNetworkSetupWizard=" + hasNetworkSetupWizardPermission
+ " Admin=" + isAdmin);
}
return false;
}
/**
* Add the network update event listener
*/
public void addOnNetworkUpdateListener(@NonNull OnNetworkUpdateListener listener) {
if (listener == null) {
Log.wtf(TAG, "addOnNetworkUpdateListener: listener must not be null");
return;
}
mListeners.add(listener);
}
/**
* Remove the network update event listener
*/
public void removeOnNetworkUpdateListener(@NonNull OnNetworkUpdateListener listener) {
if (listener == null) {
Log.wtf(TAG, "removeOnNetworkUpdateListener: listener must not be null");
return;
}
mListeners.remove(listener);
}
/**
* Set extra failure reason for given config. Used to surface extra failure details to the UI
* @param netId The network ID of the config to set the extra failure reason for
* @param reason the WifiConfiguration.ExtraFailureReason failure code representing the most
* recent failure reason
*/
public void setRecentFailureAssociationStatus(int netId, int reason) {
WifiConfiguration config = getInternalConfiguredNetwork(netId);
if (config == null) {
return;
}
mWifiMetrics.incrementRecentFailureAssociationStatusCount(reason);
int previousReason = config.recentFailure.getAssociationStatus();
config.recentFailure.setAssociationStatus(reason, mClock.getElapsedSinceBootMillis());
if (previousReason != reason) {
sendConfiguredNetworkChangedBroadcast(WifiManager.CHANGE_REASON_CONFIG_CHANGE);
}
}
/**
* @param netId The network ID of the config to clear the extra failure reason from
*/
public void clearRecentFailureReason(int netId) {
WifiConfiguration config = getInternalConfiguredNetwork(netId);
if (config == null) {
return;
}
config.recentFailure.clear();
}
/**
* Clear all recent failure reasons that have timed out.
*/
public void cleanupExpiredRecentFailureReasons() {
long timeoutDuration = mContext.getResources().getInteger(
R.integer.config_wifiRecentFailureReasonExpirationMinutes) * 60 * 1000;
for (WifiConfiguration config : getInternalConfiguredNetworks()) {
if (config.recentFailure.getAssociationStatus()
!= WifiConfiguration.RECENT_FAILURE_NONE
&& mClock.getElapsedSinceBootMillis()
>= config.recentFailure.getLastUpdateTimeSinceBootMillis() + timeoutDuration) {
config.recentFailure.clear();
sendConfiguredNetworkChangedBroadcast(WifiManager.CHANGE_REASON_CONFIG_CHANGE);
}
}
}
/**
* Find the highest RSSI among all valid scanDetails in current network's scanDetail cache.
* If scanDetail is too old, it is not considered to be valid.
* @param netId The network ID of the config to find scan RSSI
* @params scanRssiValidTimeMs The valid time for scan RSSI
* @return The highest RSSI in dBm found with current network's scanDetail cache.
*/
public int findScanRssi(int netId, int scanRssiValidTimeMs) {
int scanMaxRssi = WifiInfo.INVALID_RSSI;
ScanDetailCache scanDetailCache = getScanDetailCacheForNetwork(netId);
if (scanDetailCache == null || scanDetailCache.size() == 0) return scanMaxRssi;
long nowInMillis = mClock.getWallClockMillis();
for (ScanDetail scanDetail : scanDetailCache.values()) {
ScanResult result = scanDetail.getScanResult();
if (result == null) continue;
boolean valid = (nowInMillis - result.seen) < scanRssiValidTimeMs;
if (valid) {
scanMaxRssi = Math.max(scanMaxRssi, result.level);
}
}
return scanMaxRssi;
}
public Comparator<WifiConfiguration> getScanListComparator() {
return mScanListComparator;
}
/**
* This API is called when a connection successfully completes on an existing network
* selected by the user. It is not called after the first connection of a newly added network.
* Following actions will be triggered:
* 1. If this network is disabled, we need re-enable it again.
* 2. This network is favored over all the other networks visible in latest network
* selection procedure.
*
* @param netId ID for the network chosen by the user
* @param rssi the signal strength of the user selected network
* @return true -- There is change made to connection choice of any saved network.
* false -- There is no change made to connection choice of any saved network.
*/
private boolean setUserConnectChoice(int netId, int rssi) {
localLog("userSelectNetwork: network ID=" + netId);
WifiConfiguration selected = getInternalConfiguredNetwork(netId);
if (selected == null || selected.getProfileKey() == null) {
localLog("userSelectNetwork: Invalid configuration with nid=" + netId);
return false;
}
// Enable the network if it is disabled.
if (!selected.getNetworkSelectionStatus().isNetworkEnabled()) {
updateNetworkSelectionStatus(selected,
WifiConfiguration.NetworkSelectionStatus.DISABLED_NONE);
}
boolean changed = setLegacyUserConnectChoice(selected, rssi);
return changed;
}
/**
* This maintains the legacy user connect choice state in the config store
*/
public boolean setLegacyUserConnectChoice(@NonNull final WifiConfiguration selected,
int rssi) {
boolean change = false;
Collection<WifiConfiguration> configuredNetworks = getInternalConfiguredNetworks();
ArrayList<WifiConfiguration> networksInRange = new ArrayList<>();
String key = selected.getProfileKey();
for (WifiConfiguration network : configuredNetworks) {
WifiConfiguration.NetworkSelectionStatus status = network.getNetworkSelectionStatus();
if (network.networkId == selected.networkId) {
if (status.getConnectChoice() != null) {
localLog("Remove user selection preference of " + status.getConnectChoice()
+ " from " + network.SSID + " : " + network.networkId);
clearConnectChoiceInternal(network);
change = true;
}
continue;
}
if (status.getSeenInLastQualifiedNetworkSelection()) {
setConnectChoiceInternal(network, key, rssi);
change = true;
networksInRange.add(network);
}
}
for (OnNetworkUpdateListener listener : mListeners) {
listener.onConnectChoiceSet(networksInRange, key, rssi);
}
return change;
}
private void clearConnectChoiceInternal(WifiConfiguration config) {
config.getNetworkSelectionStatus().setConnectChoice(null);
config.getNetworkSelectionStatus().setConnectChoiceRssi(0);
}
private void setConnectChoiceInternal(WifiConfiguration config, String key, int rssi) {
config.getNetworkSelectionStatus().setConnectChoice(key);
config.getNetworkSelectionStatus().setConnectChoiceRssi(rssi);
localLog("Add connect choice key: " + key + " rssi: " + rssi + " to "
+ WifiNetworkSelector.toNetworkString(config));
}
/** Update WifiConfigManager before connecting to a network. */
public void updateBeforeConnect(int networkId, int callingUid, @NonNull String packageName) {
userEnabledNetwork(networkId);
if (!enableNetwork(networkId, true, callingUid, null)
|| !updateLastConnectUid(networkId, callingUid)) {
Log.i(TAG, "connect Allowing uid " + callingUid + " packageName " + packageName
+ " with insufficient permissions to connect=" + networkId);
}
}
/** See {@link WifiManager#save(WifiConfiguration, WifiManager.ActionListener)} */
public NetworkUpdateResult updateBeforeSaveNetwork(WifiConfiguration config, int callingUid,
@NonNull String packageName) {
NetworkUpdateResult result = addOrUpdateNetwork(config, callingUid);
if (!result.isSuccess()) {
Log.e(TAG, "saveNetwork adding/updating config=" + config + " failed");
return result;
}
if (!enableNetwork(result.getNetworkId(), false, callingUid, null)) {
Log.e(TAG, "saveNetwork enabling config=" + config + " failed");
return NetworkUpdateResult.makeFailed();
}
return result;
}
/**
* Gets the most recent scan result that is newer than maxAgeMillis for each configured network.
* @param maxAgeMillis scan results older than this parameter will get filtered out.
*/
public @NonNull List<ScanResult> getMostRecentScanResultsForConfiguredNetworks(
int maxAgeMillis) {
List<ScanResult> results = new ArrayList<>();
long timeNowMs = mClock.getWallClockMillis();
for (WifiConfiguration config : getInternalConfiguredNetworks()) {
ScanDetailCache scanDetailCache = getScanDetailCacheForNetwork(config.networkId);
if (scanDetailCache == null) {
continue;
}
ScanResult scanResult = scanDetailCache.getMostRecentScanResult();
if (scanResult == null) {
continue;
}
if (timeNowMs - scanResult.seen < maxAgeMillis) {
results.add(scanResult);
}
}
return results;
}
/**
* Update the configuration according to transition disable indications.
*
* @param networkId network ID corresponding to the network.
* @param indicationBit transition disable indication bits.
* @return true if the network was found, false otherwise.
*/
public boolean updateNetworkTransitionDisable(int networkId,
@WifiMonitor.TransitionDisableIndication int indicationBit) {
localLog("updateNetworkTransitionDisable: network ID=" + networkId
+ " indication: " + indicationBit);
WifiConfiguration config = getInternalConfiguredNetwork(networkId);
if (config == null) {
Log.e(TAG, "Cannot find network for " + networkId);
return false;
}
WifiConfiguration copy = new WifiConfiguration(config);
boolean changed = false;
if (0 != (indicationBit & WifiMonitor.TDI_USE_WPA3_PERSONAL)
&& config.isSecurityType(WifiConfiguration.SECURITY_TYPE_SAE)) {
config.setSecurityParamsEnabled(WifiConfiguration.SECURITY_TYPE_PSK, false);
changed = true;
}
if (0 != (indicationBit & WifiMonitor.TDI_USE_SAE_PK)) {
config.enableSaePkOnlyMode(true);
changed = true;
}
if (0 != (indicationBit & WifiMonitor.TDI_USE_WPA3_ENTERPRISE)
&& config.isSecurityType(WifiConfiguration.SECURITY_TYPE_EAP_WPA3_ENTERPRISE)) {
config.setSecurityParamsEnabled(WifiConfiguration.SECURITY_TYPE_EAP, false);
changed = true;
}
if (0 != (indicationBit & WifiMonitor.TDI_USE_ENHANCED_OPEN)
&& config.isSecurityType(WifiConfiguration.SECURITY_TYPE_OWE)) {
config.setSecurityParamsEnabled(WifiConfiguration.SECURITY_TYPE_OPEN, false);
changed = true;
}
if (changed) {
for (OnNetworkUpdateListener listener : mListeners) {
listener.onSecurityParamsUpdate(copy, config.getSecurityParamsList());
}
}
return true;
}
/**
* Retrieves the configured network corresponding to the provided configKey
* without any masking.
*
* WARNING: Don't use this to pass network configurations except in the wifi stack, when
* there is a need for passwords and randomized MAC address.
*
* @param configKey configKey of the requested network.
* @return Copy of WifiConfiguration object if found, null otherwise.
*/
private WifiConfiguration getConfiguredNetworkWithoutMasking(String configKey) {
WifiConfiguration config = getInternalConfiguredNetwork(configKey);
if (config == null) {
return null;
}
return new WifiConfiguration(config);
}
/**
* This method links the config of the provided network id to every linkable saved network.
*
* @param networkId networkId corresponding to the network to be potentially linked.
*/
public void updateLinkedNetworks(int networkId) {
WifiConfiguration internalConfig = getInternalConfiguredNetwork(networkId);
if (internalConfig == null) {
return;
}
internalConfig.linkedConfigurations = new HashMap<>();
attemptNetworkLinking(internalConfig);
}
/**
* This method returns a map containing each config key and unmasked WifiConfiguration of every
* network linked to the provided network id.
* @param networkId networkId to get the linked configs of.
* @return HashMap of config key to unmasked WifiConfiguration
*/
public Map<String, WifiConfiguration> getLinkedNetworksWithoutMasking(int networkId) {
WifiConfiguration internalConfig = getInternalConfiguredNetwork(networkId);
if (internalConfig == null) {
return null;
}
Map<String, WifiConfiguration> linkedNetworks = new HashMap<>();
Map<String, Integer> linkedConfigurations = internalConfig.linkedConfigurations;
if (linkedConfigurations == null) {
return null;
}
for (String configKey : linkedConfigurations.keySet()) {
WifiConfiguration linkConfig = getConfiguredNetworkWithoutMasking(configKey);
if (linkConfig == null
|| !linkConfig.isSecurityType(WifiConfiguration.SECURITY_TYPE_PSK)) {
continue;
}
linkConfig.getNetworkSelectionStatus().setCandidateSecurityParams(
SecurityParams.createSecurityParamsBySecurityType(
WifiConfiguration.SECURITY_TYPE_PSK));
linkedNetworks.put(configKey, linkConfig);
}
return linkedNetworks;
}
/**
* This method updates FILS AKMs to the internal network.
*
* @param networkId networkId corresponding to the network to be updated.
*/
public void updateFilsAkms(int networkId,
boolean isFilsSha256Supported, boolean isFilsSha384Supported) {
WifiConfiguration internalConfig = getInternalConfiguredNetwork(networkId);
if (internalConfig == null) {
return;
}
internalConfig.enableFils(isFilsSha256Supported, isFilsSha384Supported);
}
/**
* This method updates auto-upgrade flag to the internal network.
*
* @param networkId networkId corresponding to the network to be updated.
* @param securityType the target security type
* @param isAddedByAutoUpgrade indicate whether the target security type is added
* by auto-upgrade or not.
*/
public void updateIsAddedByAutoUpgradeFlag(int networkId,
int securityType, boolean isAddedByAutoUpgrade) {
WifiConfiguration internalConfig = getInternalConfiguredNetwork(networkId);
if (internalConfig == null) {
return;
}
internalConfig.setSecurityParamsIsAddedByAutoUpgrade(securityType, isAddedByAutoUpgrade);
saveToStore(true);
}
private static final int SUBJECT_ALTERNATIVE_NAMES_EMAIL = 1;
private static final int SUBJECT_ALTERNATIVE_NAMES_DNS = 2;
private static final int SUBJECT_ALTERNATIVE_NAMES_URI = 6;
/** altSubjectMatch only matches EMAIL, DNS, and URI. */
private static String getAltSubjectMatchFromAltSubjectName(X509Certificate cert) {
Collection<List<?>> col = null;
try {
col = cert.getSubjectAlternativeNames();
} catch (CertificateParsingException ex) {
col = null;
}
if (null == col) return null;
if (0 == col.size()) return null;
List<String> altSubjectNameList = new ArrayList<>();
for (List<?> item: col) {
if (2 != item.size()) continue;
if (!(item.get(0) instanceof Integer)) continue;
if (!(item.get(1) instanceof String)) continue;
StringBuilder sb = new StringBuilder();
int type = (Integer) item.get(0);
if (SUBJECT_ALTERNATIVE_NAMES_EMAIL == type) {
sb.append("EMAIL:");
} else if (SUBJECT_ALTERNATIVE_NAMES_DNS == type) {
sb.append("DNS:");
} else if (SUBJECT_ALTERNATIVE_NAMES_URI == type) {
sb.append("URI:");
} else {
Log.d(TAG, "Ignore type " + type + " for altSubjectMatch");
continue;
}
sb.append((String) item.get(1));
altSubjectNameList.add(sb.toString());
}
if (altSubjectNameList.size() > 0) {
// wpa_supplicant uses ';' as the separator.
return String.join(";", altSubjectNameList);
}
return null;
}
/**
* This method updates the Root CA certifiate and the domain name of the
* server in the internal network.
*
* @param networkId networkId corresponding to the network to be updated.
* @param caCert Root CA certificate to be updated.
* @param serverCert Server certificate to be updated.
* @return true if updating Root CA certificate successfully; otherwise, false.
*/
public boolean updateCaCertificate(int networkId, @NonNull X509Certificate caCert,
@NonNull X509Certificate serverCert) {
WifiConfiguration internalConfig = getInternalConfiguredNetwork(networkId);
if (internalConfig == null) {
Log.e(TAG, "No network for network ID " + networkId);
return false;
}
if (!internalConfig.isEnterprise()) {
Log.e(TAG, "Network " + networkId + " is not an Enterprise network");
return false;
}
if (!internalConfig.enterpriseConfig.isEapMethodServerCertUsed()) {
Log.e(TAG, "Network " + networkId + " does not need verifying server cert");
return false;
}
if (null == caCert) {
Log.e(TAG, "Root CA cert is null");
return false;
}
if (null == serverCert) {
Log.e(TAG, "Server cert is null");
return false;
}
CertificateSubjectInfo serverCertInfo = CertificateSubjectInfo.parse(
serverCert.getSubjectDN().getName());
if (null == serverCertInfo) {
Log.e(TAG, "Invalid Server CA cert subject");
return false;
}
WifiConfiguration newConfig = new WifiConfiguration(internalConfig);
try {
if (newConfig.enterpriseConfig.isTrustOnFirstUseEnabled()) {
newConfig.enterpriseConfig.setCaCertificateForTrustOnFirstUse(caCert);
// setCaCertificate will mark that this CA certifiate should be removed on
// removing this configuration.
newConfig.enterpriseConfig.enableTrustOnFirstUse(false);
} else {
newConfig.enterpriseConfig.setCaCertificate(caCert);
}
} catch (IllegalArgumentException ex) {
Log.e(TAG, "Failed to set CA cert: " + caCert);
return false;
}
// If there is a subject alternative name, it should be matched first.
String altSubjectNames = getAltSubjectMatchFromAltSubjectName(serverCert);
if (!TextUtils.isEmpty(altSubjectNames)) {
if (mVerboseLoggingEnabled) {
Log.d(TAG, "Set altSubjectMatch to " + altSubjectNames);
}
newConfig.enterpriseConfig.setAltSubjectMatch(altSubjectNames);
} else {
if (mVerboseLoggingEnabled) {
Log.d(TAG, "Set domainSuffixMatch to " + serverCertInfo.commonName);
}
newConfig.enterpriseConfig.setDomainSuffixMatch(serverCertInfo.commonName);
}
newConfig.enterpriseConfig.setUserApproveNoCaCert(false);
// Trigger an update to install CA certifiate and the corresponding configuration.
NetworkUpdateResult result = addOrUpdateNetwork(newConfig, internalConfig.creatorUid);
if (!result.isSuccess()) {
Log.e(TAG, "Failed to install CA cert for network " + internalConfig.SSID);
mFrameworkFacade.showToast(mContext, mContext.getResources().getString(
R.string.wifi_ca_cert_failed_to_install_ca_cert));
return false;
}
return true;
}
/**
* This method updates Trust On First Use flag according to
* Trust On First Use support and No-Ca-Cert Approval.
*/
public void updateTrustOnFirstUseFlag(boolean enableTrustOnFirstUse) {
getInternalConfiguredNetworks().stream()
.filter(config -> config.isEnterprise())
.filter(config -> config.enterpriseConfig.isEapMethodServerCertUsed())
.filter(config -> !config.enterpriseConfig.hasCaCertificate())
.forEach(config ->
config.enterpriseConfig.enableTrustOnFirstUse(enableTrustOnFirstUse));
}
/**
* This method updates that a network could has no CA cert as a user approves it.
*
* @param networkId networkId corresponding to the network to be updated.
* @param approved true for the approval; otherwise, false.
*/
public void setUserApproveNoCaCert(int networkId, boolean approved) {
WifiConfiguration internalConfig = getInternalConfiguredNetwork(networkId);
if (internalConfig == null) return;
if (!internalConfig.isEnterprise()) return;
if (!internalConfig.enterpriseConfig.isEapMethodServerCertUsed()) return;
internalConfig.enterpriseConfig.setUserApproveNoCaCert(approved);
}
/**
* This method updates that a network uses Trust On First Use.
*
* @param networkId networkId corresponding to the network to be updated.
* @param enable true to enable Trust On First Use; otherwise, disable Trust On First Use.
*/
public void enableTrustOnFirstUse(int networkId, boolean enable) {
WifiConfiguration internalConfig = getInternalConfiguredNetwork(networkId);
if (internalConfig == null) return;
if (!internalConfig.isEnterprise()) return;
if (!internalConfig.enterpriseConfig.isEapMethodServerCertUsed()) return;
internalConfig.enterpriseConfig.enableTrustOnFirstUse(enable);
}
/**
* Add custom DHCP options.
*
* @param ssid the network SSID.
* @param oui the 3-byte OUI.
* @param options the list of DHCP options.
*/
public void addCustomDhcpOptions(@NonNull WifiSsid ssid, @NonNull byte[] oui,
@NonNull List<DhcpOption> options) {
mCustomDhcpOptions.put(new NetworkIdentifier(ssid, oui), options);
}
/**
* Remove custom DHCP options.
*
* @param ssid the network SSID.
* @param oui the 3-byte OUI.
*/
public void removeCustomDhcpOptions(@NonNull WifiSsid ssid, @NonNull byte[] oui) {
mCustomDhcpOptions.remove(new NetworkIdentifier(ssid, oui));
}
/**
* Get custom DHCP options.
*
* @param ssid the network SSID.
* @param ouiList the list of OUIs.
*
* @return null if no entry in the map is keyed by the SSID and any OUI in the list.
* all the DHCP options keyed by the SSID and the OUIs in the list.
*/
public List<DhcpOption> getCustomDhcpOptions(@NonNull WifiSsid ssid,
@NonNull List<byte[]> ouiList) {
Set<DhcpOption> results = new HashSet<>();
for (byte[] oui : ouiList) {
List<DhcpOption> options = mCustomDhcpOptions.get(new NetworkIdentifier(ssid, oui));
if (options != null) {
results.addAll(options);
}
}
return new ArrayList<>(results);
}
}