/* * Copyright (C) 2015 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.settingslib.wifi; import android.annotation.IntDef; import android.annotation.MainThread; import android.annotation.Nullable; import android.app.AppGlobals; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.net.ConnectivityManager; import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.net.NetworkInfo.DetailedState; import android.net.NetworkInfo.State; import android.net.NetworkKey; import android.net.NetworkScoreManager; import android.net.NetworkScorerAppData; import android.net.ScoredNetwork; import android.net.wifi.IWifiManager; import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiConfiguration.KeyMgmt; import android.net.wifi.WifiEnterpriseConfig; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.net.wifi.WifiNetworkScoreCache; import android.net.wifi.hotspot2.PasspointConfiguration; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Parcelable; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.os.UserHandle; import android.support.annotation.NonNull; import android.text.Spannable; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.TtsSpan; import android.util.ArraySet; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.R; import com.android.settingslib.utils.ThreadUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; /** * Represents a selectable Wifi Network for use in various wifi selection menus backed by * {@link WifiTracker}. * *
An AccessPoint, which would be more fittingly named "WifiNetwork", is an aggregation of
* {@link ScanResult ScanResults} along with pertinent metadata (e.g. current connection info,
* network scores) required to successfully render the network to the user.
*/
public class AccessPoint implements Comparable This cache should not be evicted with scan results, as the values here are used to
* generate a fallback in the absence of scores for the visible APs.
*/
private final Map Must keep in sync with {@link R.array.wifi_signal} and {@link WifiManager#RSSI_LEVELS}.
*/
public static final int SIGNAL_LEVELS = 5;
public static final int UNREACHABLE_RSSI = Integer.MIN_VALUE;
private final Context mContext;
private String ssid;
private String bssid;
private int security;
private int networkId = WifiConfiguration.INVALID_NETWORK_ID;
private int pskType = PSK_UNKNOWN;
private WifiConfiguration mConfig;
private int mRssi = UNREACHABLE_RSSI;
private WifiInfo mInfo;
private NetworkInfo mNetworkInfo;
AccessPointListener mAccessPointListener;
private Object mTag;
@Speed private int mSpeed = Speed.NONE;
private boolean mIsScoredNetworkMetered = false;
// used to co-relate internal vs returned accesspoint.
int mId;
/**
* Information associated with the {@link PasspointConfiguration}. Only maintaining
* the relevant info to preserve spaces.
*/
private String mFqdn;
private String mProviderFriendlyName;
private boolean mIsCarrierAp = false;
/**
* The EAP type {@link WifiEnterpriseConfig.Eap} associated with this AP if it is a carrier AP.
*/
private int mCarrierApEapType = WifiEnterpriseConfig.Eap.NONE;
private String mCarrierName = null;
public AccessPoint(Context context, Bundle savedState) {
mContext = context;
if (savedState.containsKey(KEY_CONFIG)) {
mConfig = savedState.getParcelable(KEY_CONFIG);
}
if (mConfig != null) {
loadConfig(mConfig);
}
if (savedState.containsKey(KEY_SSID)) {
ssid = savedState.getString(KEY_SSID);
}
if (savedState.containsKey(KEY_SECURITY)) {
security = savedState.getInt(KEY_SECURITY);
}
if (savedState.containsKey(KEY_SPEED)) {
mSpeed = savedState.getInt(KEY_SPEED);
}
if (savedState.containsKey(KEY_PSKTYPE)) {
pskType = savedState.getInt(KEY_PSKTYPE);
}
mInfo = savedState.getParcelable(KEY_WIFIINFO);
if (savedState.containsKey(KEY_NETWORKINFO)) {
mNetworkInfo = savedState.getParcelable(KEY_NETWORKINFO);
}
if (savedState.containsKey(KEY_SCANRESULTS)) {
Parcelable[] scanResults = savedState.getParcelableArray(KEY_SCANRESULTS);
mScanResults.clear();
for (Parcelable result : scanResults) {
mScanResults.add((ScanResult) result);
}
}
if (savedState.containsKey(KEY_SCOREDNETWORKCACHE)) {
ArrayList Any cached {@link TimestampedScoredNetwork} objects older than the given max age in millis
* will be removed when this method is invoked.
*
* Precondition: {@link #mRssi} is up to date before invoking this method.
*
* @param scoreCache The score cache to use to retrieve scores
* @param maxScoreCacheAgeMillis the maximum age in milliseconds of scores to consider when
* generating speed labels
*
* @return true if the set speed has changed
*/
private boolean updateScores(WifiNetworkScoreCache scoreCache, long maxScoreCacheAgeMillis) {
long nowMillis = SystemClock.elapsedRealtime();
for (ScanResult result : mScanResults) {
ScoredNetwork score = scoreCache.getScoredNetwork(result);
if (score == null) {
continue;
}
TimestampedScoredNetwork timedScore = mScoredNetworkCache.get(result.BSSID);
if (timedScore == null) {
mScoredNetworkCache.put(
result.BSSID, new TimestampedScoredNetwork(score, nowMillis));
} else {
// Update data since the has been seen in the score cache
timedScore.update(score, nowMillis);
}
}
// Remove old cached networks
long evictionCutoff = nowMillis - maxScoreCacheAgeMillis;
Iterator Use {@#isReachable()} to determine if an AccessPoint is in range, as this method will
* always return at least 0.
*/
public int getLevel() {
return WifiManager.calculateSignalLevel(mRssi, SIGNAL_LEVELS);
}
public int getRssi() {
return mRssi;
}
/**
* Returns the underlying scan result set.
*
* Callers should not modify this set.
*/
public Set If the given connection is active, the existing value of {@link #mRssi} will be returned.
* If the given AccessPoint is not active, a value will be calculated from previous scan
* results, returning the best RSSI for all matching AccessPoints averaged with the previous
* value. If the access point is not connected and there are no scan results, the rssi will be
* set to {@link #UNREACHABLE_RSSI}.
*/
private void updateRssi() {
if (this.isActive()) {
return;
}
int rssi = UNREACHABLE_RSSI;
for (ScanResult result : mScanResults) {
if (result.level > rssi) {
rssi = result.level;
}
}
if (rssi != UNREACHABLE_RSSI && mRssi != UNREACHABLE_RSSI) {
mRssi = (mRssi + rssi) / 2; // half-life previous value
} else {
mRssi = rssi;
}
}
/**
* Returns if the network should be considered metered.
*/
public boolean isMetered() {
return mIsScoredNetworkMetered
|| WifiConfiguration.isMetered(mConfig, mInfo);
}
public NetworkInfo getNetworkInfo() {
return mNetworkInfo;
}
public int getSecurity() {
return security;
}
public String getSecurityString(boolean concise) {
Context context = mContext;
if (isPasspoint() || isPasspointConfig()) {
return concise ? context.getString(R.string.wifi_security_short_eap) :
context.getString(R.string.wifi_security_eap);
}
switch(security) {
case SECURITY_EAP:
return concise ? context.getString(R.string.wifi_security_short_eap) :
context.getString(R.string.wifi_security_eap);
case SECURITY_PSK:
switch (pskType) {
case PSK_WPA:
return concise ? context.getString(R.string.wifi_security_short_wpa) :
context.getString(R.string.wifi_security_wpa);
case PSK_WPA2:
return concise ? context.getString(R.string.wifi_security_short_wpa2) :
context.getString(R.string.wifi_security_wpa2);
case PSK_WPA_WPA2:
return concise ? context.getString(R.string.wifi_security_short_wpa_wpa2) :
context.getString(R.string.wifi_security_wpa_wpa2);
case PSK_UNKNOWN:
default:
return concise ? context.getString(R.string.wifi_security_short_psk_generic)
: context.getString(R.string.wifi_security_psk_generic);
}
case SECURITY_WEP:
return concise ? context.getString(R.string.wifi_security_short_wep) :
context.getString(R.string.wifi_security_wep);
case SECURITY_NONE:
default:
return concise ? "" : context.getString(R.string.wifi_security_none);
}
}
public String getSsidStr() {
return ssid;
}
public String getBssid() {
return bssid;
}
public CharSequence getSsid() {
final SpannableString str = new SpannableString(ssid);
str.setSpan(new TtsSpan.TelephoneBuilder(ssid).build(), 0, ssid.length(),
Spannable.SPAN_INCLUSIVE_INCLUSIVE);
return str;
}
public String getConfigName() {
if (mConfig != null && mConfig.isPasspoint()) {
return mConfig.providerFriendlyName;
} else if (mFqdn != null) {
return mProviderFriendlyName;
} else {
return ssid;
}
}
public DetailedState getDetailedState() {
if (mNetworkInfo != null) {
return mNetworkInfo.getDetailedState();
}
Log.w(TAG, "NetworkInfo is null, cannot return detailed state");
return null;
}
public boolean isCarrierAp() {
return mIsCarrierAp;
}
public int getCarrierApEapType() {
return mCarrierApEapType;
}
public String getCarrierName() {
return mCarrierName;
}
public String getSavedNetworkSummary() {
WifiConfiguration config = mConfig;
if (config != null) {
PackageManager pm = mContext.getPackageManager();
String systemName = pm.getNameForUid(android.os.Process.SYSTEM_UID);
int userId = UserHandle.getUserId(config.creatorUid);
ApplicationInfo appInfo = null;
if (config.creatorName != null && config.creatorName.equals(systemName)) {
appInfo = mContext.getApplicationInfo();
} else {
try {
IPackageManager ipm = AppGlobals.getPackageManager();
appInfo = ipm.getApplicationInfo(config.creatorName, 0 /* flags */, userId);
} catch (RemoteException rex) {
}
}
if (appInfo != null &&
!appInfo.packageName.equals(mContext.getString(R.string.settings_package)) &&
!appInfo.packageName.equals(
mContext.getString(R.string.certinstaller_package))) {
return mContext.getString(R.string.saved_network, appInfo.loadLabel(pm));
}
}
return "";
}
public String getSummary() {
return getSettingsSummary(mConfig);
}
public String getSettingsSummary() {
return getSettingsSummary(mConfig);
}
private String getSettingsSummary(WifiConfiguration config) {
// Update to new summary
StringBuilder summary = new StringBuilder();
if (isActive() && config != null && config.isPasspoint()) {
// This is the active connection on passpoint
summary.append(getSummary(mContext, getDetailedState(),
false, config.providerFriendlyName));
} else if (isActive() && config != null && getDetailedState() == DetailedState.CONNECTED
&& mIsCarrierAp) {
summary.append(String.format(mContext.getString(R.string.connected_via_carrier), mCarrierName));
} else if (isActive()) {
// This is the active connection on non-passpoint network
summary.append(getSummary(mContext, getDetailedState(),
mInfo != null && mInfo.isEphemeral()));
} else if (config != null && config.isPasspoint()
&& config.getNetworkSelectionStatus().isNetworkEnabled()) {
String format = mContext.getString(R.string.available_via_passpoint);
summary.append(String.format(format, config.providerFriendlyName));
} else if (config != null && config.hasNoInternetAccess()) {
int messageID = config.getNetworkSelectionStatus().isNetworkPermanentlyDisabled()
? R.string.wifi_no_internet_no_reconnect
: R.string.wifi_no_internet;
summary.append(mContext.getString(messageID));
} else if (config != null && !config.getNetworkSelectionStatus().isNetworkEnabled()) {
WifiConfiguration.NetworkSelectionStatus networkStatus =
config.getNetworkSelectionStatus();
switch (networkStatus.getNetworkSelectionDisableReason()) {
case WifiConfiguration.NetworkSelectionStatus.DISABLED_AUTHENTICATION_FAILURE:
summary.append(mContext.getString(R.string.wifi_disabled_password_failure));
break;
case WifiConfiguration.NetworkSelectionStatus.DISABLED_BY_WRONG_PASSWORD:
summary.append(mContext.getString(R.string.wifi_check_password_try_again));
break;
case WifiConfiguration.NetworkSelectionStatus.DISABLED_DHCP_FAILURE:
case WifiConfiguration.NetworkSelectionStatus.DISABLED_DNS_FAILURE:
summary.append(mContext.getString(R.string.wifi_disabled_network_failure));
break;
case WifiConfiguration.NetworkSelectionStatus.DISABLED_ASSOCIATION_REJECTION:
summary.append(mContext.getString(R.string.wifi_disabled_generic));
break;
}
} else if (config != null && config.getNetworkSelectionStatus().isNotRecommended()) {
summary.append(mContext.getString(R.string.wifi_disabled_by_recommendation_provider));
} else if (mIsCarrierAp) {
summary.append(String.format(mContext.getString(R.string.available_via_carrier), mCarrierName));
} else if (!isReachable()) { // Wifi out of range
summary.append(mContext.getString(R.string.wifi_not_in_range));
} else { // In range, not disabled.
if (config != null) { // Is saved network
// Last attempt to connect to this failed. Show reason why
switch (config.recentFailure.getAssociationStatus()) {
case WifiConfiguration.RecentFailure.STATUS_AP_UNABLE_TO_HANDLE_NEW_STA:
summary.append(mContext.getString(
R.string.wifi_ap_unable_to_handle_new_sta));
break;
default:
// "Saved"
summary.append(mContext.getString(R.string.wifi_remembered));
break;
}
}
}
if (isVerboseLoggingEnabled()) {
summary.append(WifiUtils.buildLoggingSummary(this, config));
}
// If Speed label and summary are both present, use the preference combination to combine
// the two, else return the non-null one.
if (getSpeedLabel() != null && summary.length() != 0) {
return mContext.getResources().getString(
R.string.preference_summary_default_combination,
getSpeedLabel(),
summary.toString());
} else if (getSpeedLabel() != null) {
return getSpeedLabel();
} else {
return summary.toString();
}
}
/**
* Return whether this is the active connection.
* For ephemeral connections (networkId is invalid), this returns false if the network is
* disconnected.
*/
public boolean isActive() {
return mNetworkInfo != null &&
(networkId != WifiConfiguration.INVALID_NETWORK_ID ||
mNetworkInfo.getState() != State.DISCONNECTED);
}
public boolean isConnectable() {
return getLevel() != -1 && getDetailedState() == null;
}
public boolean isEphemeral() {
return mInfo != null && mInfo.isEphemeral() &&
mNetworkInfo != null && mNetworkInfo.getState() != State.DISCONNECTED;
}
/**
* Return true if this AccessPoint represents a Passpoint AP.
*/
public boolean isPasspoint() {
return mConfig != null && mConfig.isPasspoint();
}
/**
* Return true if this AccessPoint represents a Passpoint provider configuration.
*/
public boolean isPasspointConfig() {
return mFqdn != null;
}
/**
* Return whether the given {@link WifiInfo} is for this access point.
* If the current AP does not have a network Id then the config is used to
* match based on SSID and security.
*/
private boolean isInfoForThisAccessPoint(WifiConfiguration config, WifiInfo info) {
if (isPasspoint() == false && networkId != WifiConfiguration.INVALID_NETWORK_ID) {
return networkId == info.getNetworkId();
} else if (config != null) {
return matches(config);
}
else {
// Might be an ephemeral connection with no WifiConfiguration. Try matching on SSID.
// (Note that we only do this if the WifiConfiguration explicitly equals INVALID).
// TODO: Handle hex string SSIDs.
return ssid.equals(removeDoubleQuotes(info.getSSID()));
}
}
public boolean isSaved() {
return networkId != WifiConfiguration.INVALID_NETWORK_ID;
}
public Object getTag() {
return mTag;
}
public void setTag(Object tag) {
mTag = tag;
}
/**
* Generate and save a default wifiConfiguration with common values.
* Can only be called for unsecured networks.
*/
public void generateOpenNetworkConfig() {
if (security != SECURITY_NONE)
throw new IllegalStateException();
if (mConfig != null)
return;
mConfig = new WifiConfiguration();
mConfig.SSID = AccessPoint.convertToQuotedString(ssid);
mConfig.allowedKeyManagement.set(KeyMgmt.NONE);
}
public void saveWifiState(Bundle savedState) {
if (ssid != null) savedState.putString(KEY_SSID, getSsidStr());
savedState.putInt(KEY_SECURITY, security);
savedState.putInt(KEY_SPEED, mSpeed);
savedState.putInt(KEY_PSKTYPE, pskType);
if (mConfig != null) savedState.putParcelable(KEY_CONFIG, mConfig);
savedState.putParcelable(KEY_WIFIINFO, mInfo);
savedState.putParcelableArray(KEY_SCANRESULTS,
mScanResults.toArray(new Parcelable[mScanResults.size()]));
savedState.putParcelableArrayList(KEY_SCOREDNETWORKCACHE,
new ArrayList<>(mScoredNetworkCache.values()));
if (mNetworkInfo != null) {
savedState.putParcelable(KEY_NETWORKINFO, mNetworkInfo);
}
if (mFqdn != null) {
savedState.putString(KEY_FQDN, mFqdn);
}
if (mProviderFriendlyName != null) {
savedState.putString(KEY_PROVIDER_FRIENDLY_NAME, mProviderFriendlyName);
}
savedState.putBoolean(KEY_IS_CARRIER_AP, mIsCarrierAp);
savedState.putInt(KEY_CARRIER_AP_EAP_TYPE, mCarrierApEapType);
savedState.putString(KEY_CARRIER_NAME, mCarrierName);
}
public void setListener(AccessPointListener listener) {
mAccessPointListener = listener;
}
/**
* Sets {@link #mScanResults} to the given collection.
*
* @param scanResults a collection of scan results to add to the internal set
* @throws IllegalArgumentException if any of the given ScanResults did not belong to this AP
*/
void setScanResults(Collection All methods are invoked on the Main Thread.
*/
public interface AccessPointListener {
/**
* Indicates a change to the externally visible state of the AccessPoint trigger by an
* update of ScanResults, saved configuration state, connection state, or score
* (labels/metered) state.
*
* Clients should refresh their view of the AccessPoint to match the updated state when
* this is invoked. Overall this method is extraneous if clients are listening to
* {@link WifiTracker.WifiListener#onAccessPointsChanged()} callbacks.
*
* Examples of changes include signal strength, connection state, speed label, and
* generally anything that would impact the summary string.
*
* @param accessPoint The accessPoint object the listener was registered on which has
* changed
*/
@MainThread void onAccessPointChanged(AccessPoint accessPoint);
/**
* Indicates the "wifi pie signal level" has changed, retrieved via calls to
* {@link AccessPoint#getLevel()}.
*
* This call is a subset of {@link #onAccessPointChanged(AccessPoint)} , hence is also
* extraneous if the client is already reacting to that or the
* {@link WifiTracker.WifiListener#onAccessPointsChanged()} callbacks.
*
* @param accessPoint The accessPoint object the listener was registered on whose level has
* changed
*/
@MainThread void onLevelChanged(AccessPoint accessPoint);
}
private static boolean isVerboseLoggingEnabled() {
return WifiTracker.sVerboseLogging || Log.isLoggable(TAG, Log.VERBOSE);
}
}