blob: 1f0a34be758d61af958f73714683b67ee38006e2 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.wifitrackerlib;
import static android.net.wifi.WifiInfo.DEFAULT_MAC_ADDRESS;
import static android.net.wifi.WifiInfo.sanitizeSsid;
import static androidx.core.util.Preconditions.checkNotNull;
import static com.android.wifitrackerlib.Utils.getAppLabel;
import static com.android.wifitrackerlib.Utils.getAutoConnectDescription;
import static com.android.wifitrackerlib.Utils.getAverageSpeedFromScanResults;
import static com.android.wifitrackerlib.Utils.getBestScanResultByLevel;
import static com.android.wifitrackerlib.Utils.getCarrierNameForSubId;
import static com.android.wifitrackerlib.Utils.getCurrentNetworkCapabilitiesInformation;
import static com.android.wifitrackerlib.Utils.getDisconnectedStateDescription;
import static com.android.wifitrackerlib.Utils.getImsiProtectionDescription;
import static com.android.wifitrackerlib.Utils.getMeteredDescription;
import static com.android.wifitrackerlib.Utils.getNetworkDetailedState;
import static com.android.wifitrackerlib.Utils.getSecurityTypeFromWifiConfiguration;
import static com.android.wifitrackerlib.Utils.getSpeedDescription;
import static com.android.wifitrackerlib.Utils.getSpeedFromWifiInfo;
import static com.android.wifitrackerlib.Utils.getSubIdForConfig;
import static com.android.wifitrackerlib.Utils.getVerboseLoggingDescription;
import android.content.Context;
import android.net.NetworkInfo;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiNetworkScoreCache;
import android.net.wifi.hotspot2.PasspointConfiguration;
import android.os.Handler;
import android.text.TextUtils;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;
/**
* WifiEntry representation of a subscribed Passpoint network, uniquely identified by FQDN.
*/
@VisibleForTesting
public class PasspointWifiEntry extends WifiEntry implements WifiEntry.WifiEntryCallback {
static final String KEY_PREFIX = "PasspointWifiEntry:";
private final Object mLock = new Object();
// Scan result list must be thread safe for generating the verbose scan summary
@GuardedBy("mLock")
private final List<ScanResult> mCurrentHomeScanResults = new ArrayList<>();
@GuardedBy("mLock")
private final List<ScanResult> mCurrentRoamingScanResults = new ArrayList<>();
@NonNull private final String mKey;
@NonNull private String mFqdn;
@NonNull private String mFriendlyName;
@NonNull private final Context mContext;
@Nullable
private PasspointConfiguration mPasspointConfig;
@Nullable private WifiConfiguration mWifiConfig;
private @Security int mSecurity = SECURITY_EAP;
private boolean mIsRoaming = false;
private OsuWifiEntry mOsuWifiEntry;
protected long mSubscriptionExpirationTimeInMillis;
// PasspointConfiguration#setMeteredOverride(int meteredOverride) is a hide API and we can't
// set it in PasspointWifiEntry#setMeteredChoice(int meteredChoice).
// For PasspointWifiEntry#getMeteredChoice() to return correct value right after
// PasspointWifiEntry#setMeteredChoice(int meteredChoice), cache
// PasspointConfiguration#getMeteredOverride() in this variable.
private int mMeteredOverride = METERED_CHOICE_AUTO;
/**
* Create a PasspointWifiEntry with the associated PasspointConfiguration
*/
PasspointWifiEntry(@NonNull Context context, @NonNull Handler callbackHandler,
@NonNull PasspointConfiguration passpointConfig,
@NonNull WifiManager wifiManager,
@NonNull WifiNetworkScoreCache scoreCache,
boolean forSavedNetworksPage) throws IllegalArgumentException {
super(callbackHandler, wifiManager, scoreCache, forSavedNetworksPage);
checkNotNull(passpointConfig, "Cannot construct with null PasspointConfiguration!");
mContext = context;
mPasspointConfig = passpointConfig;
mKey = uniqueIdToPasspointWifiEntryKey(passpointConfig.getUniqueId());
mFqdn = passpointConfig.getHomeSp().getFqdn();
mFriendlyName = passpointConfig.getHomeSp().getFriendlyName();
mSubscriptionExpirationTimeInMillis =
passpointConfig.getSubscriptionExpirationTimeMillis();
mMeteredOverride = mPasspointConfig.getMeteredOverride();
}
/**
* Create a PasspointWifiEntry with the associated WifiConfiguration for use with network
* suggestions, since WifiManager#getAllMatchingWifiConfigs() does not provide a corresponding
* PasspointConfiguration.
*/
PasspointWifiEntry(@NonNull Context context, @NonNull Handler callbackHandler,
@NonNull WifiConfiguration wifiConfig,
@NonNull WifiManager wifiManager,
@NonNull WifiNetworkScoreCache scoreCache,
boolean forSavedNetworksPage) throws IllegalArgumentException {
super(callbackHandler, wifiManager, scoreCache, forSavedNetworksPage);
checkNotNull(wifiConfig, "Cannot construct with null PasspointConfiguration!");
if (!wifiConfig.isPasspoint()) {
throw new IllegalArgumentException("Given WifiConfiguration is not for Passpoint!");
}
mContext = context;
mWifiConfig = wifiConfig;
mKey = uniqueIdToPasspointWifiEntryKey(wifiConfig.getKey());
mFqdn = wifiConfig.FQDN;
mFriendlyName = mWifiConfig.providerFriendlyName;
}
@Override
public String getKey() {
return mKey;
}
@Override
@ConnectedState
public int getConnectedState() {
if (isExpired()) {
if (super.getConnectedState() == CONNECTED_STATE_DISCONNECTED
&& mOsuWifiEntry != null) {
return mOsuWifiEntry.getConnectedState();
}
}
return super.getConnectedState();
}
@Override
public String getTitle() {
return mFriendlyName;
}
@Override
public String getSummary(boolean concise) {
StringJoiner sj = new StringJoiner(mContext.getString(R.string.summary_separator));
if (isExpired()) {
if (mOsuWifiEntry != null) {
sj.add(mOsuWifiEntry.getSummary(concise));
} else {
sj.add(mContext.getString(R.string.wifi_passpoint_expired));
}
} else if (getConnectedState() == CONNECTED_STATE_DISCONNECTED) {
String disconnectDescription = getDisconnectedStateDescription(mContext, this);
if (TextUtils.isEmpty(disconnectDescription)) {
if (concise) {
sj.add(mContext.getString(R.string.wifi_disconnected));
} else if (!mForSavedNetworksPage) {
if (mWifiConfig != null && mWifiConfig.fromWifiNetworkSuggestion) {
String carrierName = getCarrierNameForSubId(mContext,
getSubIdForConfig(mContext, mWifiConfig));
String suggestorLabel = getAppLabel(mContext, mWifiConfig.creatorName);
if (TextUtils.isEmpty(suggestorLabel)) {
// Fall-back to the package name in case the app label is missing
suggestorLabel = mWifiConfig.creatorName;
}
sj.add(mContext.getString(R.string.available_via_app, carrierName != null
? carrierName
: suggestorLabel));
} else {
sj.add(mContext.getString(R.string.wifi_remembered));
}
}
} else {
sj.add(disconnectDescription);
}
} else {
String connectDescription = getConnectStateDescription();
if (!TextUtils.isEmpty(connectDescription)) {
sj.add(connectDescription);
}
}
String speedDescription = getSpeedDescription(mContext, this);
if (!TextUtils.isEmpty(speedDescription)) {
sj.add(speedDescription);
}
String autoConnectDescription = getAutoConnectDescription(mContext, this);
if (!TextUtils.isEmpty(autoConnectDescription)) {
sj.add(autoConnectDescription);
}
String meteredDescription = getMeteredDescription(mContext, this);
if (!TextUtils.isEmpty(meteredDescription)) {
sj.add(meteredDescription);
}
if (!concise) {
String verboseLoggingDescription = getVerboseLoggingDescription(this);
if (!TextUtils.isEmpty(verboseLoggingDescription)) {
sj.add(verboseLoggingDescription);
}
}
return sj.toString();
}
private String getConnectStateDescription() {
if (getConnectedState() == CONNECTED_STATE_CONNECTED) {
// For network suggestions
final String suggestionOrSpecifierPackageName = mWifiInfo != null
? mWifiInfo.getRequestingPackageName() : null;
if (!TextUtils.isEmpty(suggestionOrSpecifierPackageName)) {
String carrierName = mWifiConfig != null
? getCarrierNameForSubId(mContext, getSubIdForConfig(mContext, mWifiConfig))
: null;
String suggestorLabel = getAppLabel(mContext, suggestionOrSpecifierPackageName);
if (TextUtils.isEmpty(suggestorLabel)) {
// Fall-back to the package name in case the app label is missing
suggestorLabel = suggestionOrSpecifierPackageName;
}
return mContext.getString(R.string.connected_via_app, carrierName != null
? carrierName
: suggestorLabel);
}
if (mIsLowQuality) {
return mContext.getString(R.string.wifi_connected_low_quality);
}
String networkCapabilitiesinformation =
getCurrentNetworkCapabilitiesInformation(mContext, mNetworkCapabilities);
if (!TextUtils.isEmpty(networkCapabilitiesinformation)) {
return networkCapabilitiesinformation;
}
}
return getNetworkDetailedState(mContext, mNetworkInfo);
}
@Override
public CharSequence getSecondSummary() {
return getConnectedState() == CONNECTED_STATE_CONNECTED
? getImsiProtectionDescription(mContext, mWifiConfig) : "";
}
@Override
public String getSsid() {
if (mWifiInfo != null) {
return sanitizeSsid(mWifiInfo.getSSID());
}
return mWifiConfig != null ? sanitizeSsid(mWifiConfig.SSID) : null;
}
@Override
@Security
public int getSecurity() {
return mSecurity;
}
@Override
public String getMacAddress() {
if (mWifiInfo != null) {
final String wifiInfoMac = mWifiInfo.getMacAddress();
if (!TextUtils.isEmpty(wifiInfoMac)
&& !TextUtils.equals(wifiInfoMac, DEFAULT_MAC_ADDRESS)) {
return wifiInfoMac;
}
}
if (mWifiConfig == null || getPrivacy() != PRIVACY_RANDOMIZED_MAC) {
final String[] factoryMacs = mWifiManager.getFactoryMacAddresses();
if (factoryMacs.length > 0) {
return factoryMacs[0];
}
return null;
}
return mWifiConfig.getRandomizedMacAddress().toString();
}
@Override
public boolean isMetered() {
return getMeteredChoice() == METERED_CHOICE_METERED
|| (mWifiConfig != null && mWifiConfig.meteredHint);
}
@Override
public boolean isSaved() {
return false;
}
@Override
public boolean isSuggestion() {
return mWifiConfig != null && mWifiConfig.fromWifiNetworkSuggestion;
}
@Override
public boolean isSubscription() {
return mPasspointConfig != null;
}
@Override
public WifiConfiguration getWifiConfiguration() {
return null;
}
@Override
public boolean canConnect() {
if (isExpired()) {
return mOsuWifiEntry != null && mOsuWifiEntry.canConnect();
}
return mLevel != WIFI_LEVEL_UNREACHABLE
&& getConnectedState() == CONNECTED_STATE_DISCONNECTED && mWifiConfig != null;
}
@Override
public void connect(@Nullable ConnectCallback callback) {
if (isExpired()) {
if (mOsuWifiEntry != null) {
mOsuWifiEntry.connect(callback);
return;
}
}
mConnectCallback = callback;
if (mWifiConfig == null) {
// We should not be able to call connect() if mWifiConfig is null
new ConnectActionListener().onFailure(0);
}
mWifiManager.connect(mWifiConfig, new ConnectActionListener());
}
@Override
public boolean canDisconnect() {
return getConnectedState() == CONNECTED_STATE_CONNECTED;
}
@Override
public void disconnect(@Nullable DisconnectCallback callback) {
if (canDisconnect()) {
mCalledDisconnect = true;
mDisconnectCallback = callback;
mCallbackHandler.postDelayed(() -> {
if (callback != null && mCalledDisconnect) {
callback.onDisconnectResult(
DisconnectCallback.DISCONNECT_STATUS_FAILURE_UNKNOWN);
}
}, 10_000 /* delayMillis */);
mWifiManager.disableEphemeralNetwork(mWifiConfig.FQDN);
mWifiManager.disconnect();
}
}
@Override
public boolean canForget() {
return !isSuggestion() && mPasspointConfig != null;
}
@Override
public void forget(@Nullable ForgetCallback callback) {
if (!canForget()) {
return;
}
mForgetCallback = callback;
mWifiManager.removePasspointConfiguration(mPasspointConfig.getHomeSp().getFqdn());
new ForgetActionListener().onSuccess();
}
@Override
public boolean canSignIn() {
return false;
}
@Override
public void signIn(@Nullable SignInCallback callback) {
return;
}
@Override
public boolean canShare() {
return false;
}
@Override
public boolean canEasyConnect() {
return false;
}
@Override
@MeteredChoice
public int getMeteredChoice() {
if (mMeteredOverride == WifiConfiguration.METERED_OVERRIDE_METERED) {
return METERED_CHOICE_METERED;
} else if (mMeteredOverride == WifiConfiguration.METERED_OVERRIDE_NOT_METERED) {
return METERED_CHOICE_UNMETERED;
}
return METERED_CHOICE_AUTO;
}
@Override
public boolean canSetMeteredChoice() {
return !isSuggestion() && mPasspointConfig != null;
}
@Override
public void setMeteredChoice(int meteredChoice) {
if (!canSetMeteredChoice()) {
return;
}
switch (meteredChoice) {
case METERED_CHOICE_AUTO:
mMeteredOverride = WifiConfiguration.METERED_OVERRIDE_NONE;
break;
case METERED_CHOICE_METERED:
mMeteredOverride = WifiConfiguration.METERED_OVERRIDE_METERED;
break;
case METERED_CHOICE_UNMETERED:
mMeteredOverride = WifiConfiguration.METERED_OVERRIDE_NOT_METERED;
break;
default:
// Do nothing.
return;
}
mWifiManager.setPasspointMeteredOverride(mPasspointConfig.getHomeSp().getFqdn(),
mMeteredOverride);
}
@Override
public boolean canSetPrivacy() {
return !isSuggestion() && mPasspointConfig != null;
}
@Override
@Privacy
public int getPrivacy() {
if (mPasspointConfig == null) {
return PRIVACY_RANDOMIZED_MAC;
}
return mPasspointConfig.isMacRandomizationEnabled()
? PRIVACY_RANDOMIZED_MAC : PRIVACY_DEVICE_MAC;
}
@Override
public void setPrivacy(int privacy) {
if (!canSetPrivacy()) {
return;
}
mWifiManager.setMacRandomizationSettingPasspointEnabled(
mPasspointConfig.getHomeSp().getFqdn(),
privacy == PRIVACY_DEVICE_MAC ? false : true);
}
@Override
public boolean isAutoJoinEnabled() {
// Suggestion network; use WifiConfig instead
if (mPasspointConfig != null) {
return mPasspointConfig.isAutojoinEnabled();
}
if (mWifiConfig != null) {
return mWifiConfig.allowAutojoin;
}
return false;
}
@Override
public boolean canSetAutoJoinEnabled() {
return mPasspointConfig != null || mWifiConfig != null;
}
@Override
public void setAutoJoinEnabled(boolean enabled) {
if (mPasspointConfig != null) {
mWifiManager.allowAutojoinPasspoint(mPasspointConfig.getHomeSp().getFqdn(), enabled);
} else if (mWifiConfig != null) {
mWifiManager.allowAutojoin(mWifiConfig.networkId, enabled);
}
}
@Override
public String getSecurityString(boolean concise) {
return concise ? mContext.getString(R.string.wifi_security_short_eap) :
mContext.getString(R.string.wifi_security_eap);
}
@Override
public boolean isExpired() {
if (mSubscriptionExpirationTimeInMillis <= 0) {
// Expiration time not specified.
return false;
} else {
return System.currentTimeMillis() >= mSubscriptionExpirationTimeInMillis;
}
}
@WorkerThread
void updatePasspointConfig(@Nullable PasspointConfiguration passpointConfig) {
mPasspointConfig = passpointConfig;
if (mPasspointConfig != null) {
mFriendlyName = passpointConfig.getHomeSp().getFriendlyName();
mSubscriptionExpirationTimeInMillis =
passpointConfig.getSubscriptionExpirationTimeMillis();
mMeteredOverride = passpointConfig.getMeteredOverride();
}
notifyOnUpdated();
}
@WorkerThread
void updateScanResultInfo(@Nullable WifiConfiguration wifiConfig,
@Nullable List<ScanResult> homeScanResults,
@Nullable List<ScanResult> roamingScanResults)
throws IllegalArgumentException {
mIsRoaming = false;
mWifiConfig = wifiConfig;
synchronized (mLock) {
mCurrentHomeScanResults.clear();
mCurrentRoamingScanResults.clear();
if (homeScanResults != null) {
mCurrentHomeScanResults.addAll(homeScanResults);
}
if (roamingScanResults != null) {
mCurrentRoamingScanResults.addAll(roamingScanResults);
}
}
if (mWifiConfig != null) {
mSecurity = getSecurityTypeFromWifiConfiguration(wifiConfig);
List<ScanResult> currentScanResults = new ArrayList<>();
ScanResult bestScanResult = null;
if (homeScanResults != null && !homeScanResults.isEmpty()) {
currentScanResults.addAll(homeScanResults);
} else if (roamingScanResults != null && !roamingScanResults.isEmpty()) {
currentScanResults.addAll(roamingScanResults);
mIsRoaming = true;
}
bestScanResult = getBestScanResultByLevel(currentScanResults);
if (bestScanResult != null) {
mWifiConfig.SSID = "\"" + bestScanResult.SSID + "\"";
}
if (getConnectedState() == CONNECTED_STATE_DISCONNECTED) {
mLevel = bestScanResult != null
? mWifiManager.calculateSignalLevel(bestScanResult.level)
: WIFI_LEVEL_UNREACHABLE;
// Average speed is used to prevent speed label flickering from multiple APs.
mSpeed = getAverageSpeedFromScanResults(mScoreCache, currentScanResults);
}
} else {
mLevel = WIFI_LEVEL_UNREACHABLE;
}
notifyOnUpdated();
}
@WorkerThread
void onScoreCacheUpdated() {
if (mWifiInfo != null) {
mSpeed = getSpeedFromWifiInfo(mScoreCache, mWifiInfo);
} else {
synchronized (mLock) {
// Average speed is used to prevent speed label flickering from multiple APs.
if (!mCurrentHomeScanResults.isEmpty()) {
mSpeed = getAverageSpeedFromScanResults(mScoreCache, mCurrentHomeScanResults);
} else {
mSpeed = getAverageSpeedFromScanResults(mScoreCache,
mCurrentRoamingScanResults);
}
}
}
notifyOnUpdated();
}
@WorkerThread
@Override
protected boolean connectionInfoMatches(@NonNull WifiInfo wifiInfo,
@NonNull NetworkInfo networkInfo) {
if (!wifiInfo.isPasspointAp()) {
return false;
}
// Match with FQDN until WifiInfo supports returning the passpoint uniqueID
return TextUtils.equals(wifiInfo.getPasspointFqdn(), mFqdn);
}
@NonNull
static String uniqueIdToPasspointWifiEntryKey(@NonNull String uniqueId) {
checkNotNull(uniqueId, "Cannot create key with null unique id!");
return KEY_PREFIX + uniqueId;
}
@Override
String getScanResultDescription() {
// TODO(b/70983952): Fill this method in.
return "";
}
@Override
String getNetworkSelectionDescription() {
return Utils.getNetworkSelectionDescription(mWifiConfig);
}
/** Pass a reference to a matching OsuWifiEntry for expiration handling */
void setOsuWifiEntry(OsuWifiEntry osuWifiEntry) {
mOsuWifiEntry = osuWifiEntry;
if (mOsuWifiEntry != null) {
mOsuWifiEntry.setListener(this);
}
}
/** Callback for updates to the linked OsuWifiEntry */
@Override
public void onUpdated() {
notifyOnUpdated();
}
}