blob: e800e4832fe6a01a198a6c6a947e1c02c317505e [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.wifitrackerlib;
import static android.net.wifi.WifiInfo.INVALID_RSSI;
import static androidx.core.util.Preconditions.checkNotNull;
import static com.android.wifitrackerlib.Utils.getSpeedFromWifiInfo;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkUtils;
import android.net.RouteInfo;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiNetworkScoreCache;
import android.os.Handler;
import androidx.annotation.AnyThread;
import androidx.annotation.IntDef;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;
import java.util.stream.Collectors;
/**
* Abstract base class for an entry representing a Wi-Fi network in a Wi-Fi picker/settings.
*
* Clients implementing a Wi-Fi picker/settings should receive WifiEntry objects from classes
* implementing BaseWifiTracker, and rely on the given API for all user-displayable information and
* actions on the represented network.
*/
public abstract class WifiEntry implements Comparable<WifiEntry> {
/**
* Security type based on WifiConfiguration.KeyMgmt
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
SECURITY_NONE,
SECURITY_OWE,
SECURITY_WEP,
SECURITY_PSK,
SECURITY_SAE,
SECURITY_EAP,
SECURITY_EAP_SUITE_B,
})
public @interface Security {}
public static final int SECURITY_NONE = 0;
public static final int SECURITY_WEP = 1;
public static final int SECURITY_PSK = 2;
public static final int SECURITY_EAP = 3;
public static final int SECURITY_OWE = 4;
public static final int SECURITY_SAE = 5;
public static final int SECURITY_EAP_SUITE_B = 6;
public static final int NUM_SECURITY_TYPES = 7;
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
CONNECTED_STATE_DISCONNECTED,
CONNECTED_STATE_CONNECTED,
CONNECTED_STATE_CONNECTING
})
public @interface ConnectedState {}
public static final int CONNECTED_STATE_DISCONNECTED = 0;
public static final int CONNECTED_STATE_CONNECTING = 1;
public static final int CONNECTED_STATE_CONNECTED = 2;
// Wi-Fi signal levels for displaying signal strength.
public static final int WIFI_LEVEL_MIN = 0;
public static final int WIFI_LEVEL_MAX = 4;
public static final int WIFI_LEVEL_UNREACHABLE = -1;
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
SPEED_NONE,
SPEED_SLOW,
SPEED_MODERATE,
SPEED_FAST,
SPEED_VERY_FAST
})
public @interface Speed {}
public static final int SPEED_NONE = 0;
public static final int SPEED_SLOW = 5;
public static final int SPEED_MODERATE = 10;
public static final int SPEED_FAST = 20;
public static final int SPEED_VERY_FAST = 30;
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
METERED_CHOICE_AUTO,
METERED_CHOICE_METERED,
METERED_CHOICE_UNMETERED,
})
public @interface MeteredChoice {}
// User's choice whether to treat a network as metered.
public static final int METERED_CHOICE_AUTO = 0;
public static final int METERED_CHOICE_METERED = 1;
public static final int METERED_CHOICE_UNMETERED = 2;
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
PRIVACY_DEVICE_MAC,
PRIVACY_RANDOMIZED_MAC,
PRIVACY_UNKNOWN
})
public @interface Privacy {}
public static final int PRIVACY_DEVICE_MAC = 0;
public static final int PRIVACY_RANDOMIZED_MAC = 1;
public static final int PRIVACY_UNKNOWN = 2;
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
FREQUENCY_2_4_GHZ,
FREQUENCY_5_GHZ,
FREQUENCY_6_GHZ,
FREQUENCY_UNKNOWN
})
public @interface Frequency {}
public static final int FREQUENCY_2_4_GHZ = 2_400;
public static final int FREQUENCY_5_GHZ = 5_000;
public static final int FREQUENCY_6_GHZ = 6_000;
public static final int FREQUENCY_UNKNOWN = -1;
/**
* Min bound on the 2.4 GHz (802.11b/g/n) WLAN channels.
*/
public static final int MIN_FREQ_24GHZ = 2400;
/**
* Max bound on the 2.4 GHz (802.11b/g/n) WLAN channels.
*/
public static final int MAX_FREQ_24GHZ = 2500;
/**
* Min bound on the 5.0 GHz (802.11a/h/j/n/ac) WLAN channels.
*/
public static final int MIN_FREQ_5GHZ = 4900;
/**
* Max bound on the 5.0 GHz (802.11a/h/j/n/ac) WLAN channels.
*/
public static final int MAX_FREQ_5GHZ = 5900;
/**
* Min bound on the 6.0 GHz (802.11ax) WLAN channels.
*/
public static final int MIN_FREQ_6GHZ = 5925;
/**
* Max bound on the 6.0 GHz (802.11ax) WLAN channels.
*/
public static final int MAX_FREQ_6GHZ = 7125;
/**
* Max ScanResult information displayed of Wi-Fi Verbose Logging.
*/
protected static final int MAX_VERBOSE_LOG_DISPLAY_SCANRESULT_COUNT = 4;
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
final boolean mForSavedNetworksPage;
protected final WifiManager mWifiManager;
// Callback associated with this WifiEntry. Subclasses should call its methods appropriately.
private WifiEntryCallback mListener;
protected Handler mCallbackHandler;
protected int mLevel = WIFI_LEVEL_UNREACHABLE;
protected int mSpeed = SPEED_NONE;
protected WifiInfo mWifiInfo;
protected NetworkInfo mNetworkInfo;
protected NetworkCapabilities mNetworkCapabilities;
protected ConnectedInfo mConnectedInfo;
protected WifiNetworkScoreCache mScoreCache;
protected ConnectCallback mConnectCallback;
protected DisconnectCallback mDisconnectCallback;
protected ForgetCallback mForgetCallback;
protected boolean mCalledConnect = false;
protected boolean mCalledDisconnect = false;
WifiEntry(@NonNull Handler callbackHandler, @NonNull WifiManager wifiManager,
@NonNull WifiNetworkScoreCache scoreCache,
boolean forSavedNetworksPage) throws IllegalArgumentException {
checkNotNull(callbackHandler, "Cannot construct with null handler!");
checkNotNull(wifiManager, "Cannot construct with null WifiManager!");
mCallbackHandler = callbackHandler;
mForSavedNetworksPage = forSavedNetworksPage;
mWifiManager = wifiManager;
mScoreCache = scoreCache;
}
// Info available for all WifiEntries //
/** The unique key defining a WifiEntry */
public abstract String getKey();
/** Returns connection state of the network defined by the CONNECTED_STATE constants */
@ConnectedState
public int getConnectedState() {
if (mNetworkInfo == null) {
return CONNECTED_STATE_DISCONNECTED;
}
switch (mNetworkInfo.getDetailedState()) {
case SCANNING:
case CONNECTING:
case AUTHENTICATING:
case OBTAINING_IPADDR:
case VERIFYING_POOR_LINK:
case CAPTIVE_PORTAL_CHECK:
return CONNECTED_STATE_CONNECTING;
case CONNECTED:
return CONNECTED_STATE_CONNECTED;
default:
return CONNECTED_STATE_DISCONNECTED;
}
}
/** Returns the display title. This is most commonly the SSID of a network. */
public abstract String getTitle();
/** Returns the display summary, it's a concise summary. */
public String getSummary() {
return getSummary(true /* concise */);
}
/** Returns the second summary, it's for additional information of the WifiEntry */
public CharSequence getSecondSummary() {
return "";
}
/**
* Returns the display summary.
* @param concise Whether to show more information. e.g., verbose logging.
*/
public abstract String getSummary(boolean concise);
/**
* Returns the signal strength level within [WIFI_LEVEL_MIN, WIFI_LEVEL_MAX].
* A value of WIFI_LEVEL_UNREACHABLE indicates an out of range network.
*/
public int getLevel() {
return mLevel;
};
/** Returns the speed value of the network defined by the SPEED constants */
@Speed
public int getSpeed() {
return mSpeed;
};
/**
* Returns the SSID of the entry, if applicable. Null otherwise.
*/
public abstract String getSsid();
/** Returns the security type defined by the SECURITY constants */
@Security
public abstract int getSecurity();
/** Returns the string representation of the security of the WifiEntry. */
public String getSecurityString() {
// TODO (b/70983952) Implement this
return null;
}
/** Returns the MAC address of the connection */
public abstract String getMacAddress();
/**
* Indicates when a network is metered or the user marked the network as metered.
*/
public abstract boolean isMetered();
/**
* Indicates whether or not an entry is for a saved configuration.
*/
public abstract boolean isSaved();
/**
* Indicates whether or not an entry is for a saved configuration.
*/
public abstract boolean isSuggestion();
/**
* Indicates whether or not an entry is for a subscription.
*/
public abstract boolean isSubscription();
/**
* Returns the WifiConfiguration of an entry or null if unavailable. This should be used when
* information on the WifiConfiguration needs to be modified and saved via
* {@link WifiManager#save(WifiConfiguration, WifiManager.ActionListener)}.
*/
public abstract WifiConfiguration getWifiConfiguration();
/**
* Returns the ConnectedInfo object pertaining to an active connection.
*
* Returns null if getConnectedState() != CONNECTED_STATE_CONNECTED.
*/
public ConnectedInfo getConnectedInfo() {
if (getConnectedState() != CONNECTED_STATE_CONNECTED) {
return null;
}
return mConnectedInfo;
}
/**
* Info associated with the active connection.
*/
public static class ConnectedInfo {
@Frequency
public int frequencyMhz;
public List<String> dnsServers = new ArrayList<>();
public int linkSpeedMbps;
public String ipAddress;
public List<String> ipv6Addresses = new ArrayList<>();
public String gateway;
public String subnetMask;
}
// User actions on a network
/** Returns whether the entry should show a connect option */
public abstract boolean canConnect();
/** Connects to the network */
public abstract void connect(@Nullable ConnectCallback callback);
/** Returns whether the entry should show a disconnect option */
public abstract boolean canDisconnect();
/** Disconnects from the network */
public abstract void disconnect(@Nullable DisconnectCallback callback);
/** Returns whether the entry should show a forget option */
public abstract boolean canForget();
/** Forgets the network */
public abstract void forget(@Nullable ForgetCallback callback);
/** Returns whether the network can be signed-in to */
public abstract boolean canSignIn();
/** Sign-in to the network. For captive portals. */
public abstract void signIn(@Nullable SignInCallback callback);
/** Returns whether the network can be shared via QR code */
public abstract boolean canShare();
/** Returns whether the user can use Easy Connect to onboard a device to the network */
public abstract boolean canEasyConnect();
/** Returns the QR code string for the network */
public abstract String getQrCodeString();
// Modifiable settings
/** Returns whether the entry should show a password input */
public abstract boolean canSetPassword();
/** Sets the user's password to a network */
public abstract void setPassword(@NonNull String password);
/**
* Returns the user's choice whether to treat a network as metered,
* defined by the METERED_CHOICE constants
*/
@MeteredChoice
public abstract int getMeteredChoice();
/** Returns whether the entry should let the user choose the metered treatment of a network */
public abstract boolean canSetMeteredChoice();
/**
* Sets the user's choice for treating a network as metered,
* defined by the METERED_CHOICE constants
*/
public abstract void setMeteredChoice(@MeteredChoice int meteredChoice);
/** Returns whether the entry should let the user choose the MAC randomization setting */
public abstract boolean canSetPrivacy();
/** Returns the MAC randomization setting defined by the PRIVACY constants */
@Privacy
public abstract int getPrivacy();
/** Sets the user's choice for MAC randomization defined by the PRIVACY constants */
public abstract void setPrivacy(@Privacy int privacy);
/** Returns whether the network has auto-join enabled */
public abstract boolean isAutoJoinEnabled();
/** Returns whether the user can enable/disable auto-join */
public abstract boolean canSetAutoJoinEnabled();
/** Sets whether a network will be auto-joined or not */
public abstract void setAutoJoinEnabled(boolean enabled);
/** Returns the string displayed for @Security */
public abstract String getSecurityString(boolean concise);
/** Returns whether subscription of the entry is expired */
public abstract boolean isExpired();
/** Returns whether a user can manage their subscription through this WifiEntry */
public boolean canManageSubscription() {
// Subclasses should implement this method.
return false;
};
/**
* Return the URI string value of help, if it is not null, WifiPicker may show
* help icon and route the user to help page specified by the URI string.
* see {@link Intent#parseUri}
*/
@Nullable
public String getHelpUriString() {
return null;
}
/** Allows the user to manage their subscription via an external flow */
public void manageSubscription() {
// Subclasses should implement this method.
};
/** Returns the ScanResult information of a WifiEntry */
abstract String getScanResultDescription();
/** Returns the network selection information of a WifiEntry */
String getNetworkSelectionDescription() {
return "";
}
/**
* In Wi-Fi picker, when users click a saved network, it will connect to the Wi-Fi network.
* However, for some special cases, Wi-Fi picker should show Wi-Fi editor UI for users to edit
* security or password before connecting. Or users will always get connection fail results.
*/
public boolean shouldEditBeforeConnect() {
return false;
}
/**
* Sets the callback listener for WifiEntryCallback methods.
* Subsequent calls will overwrite the previous listener.
*/
public void setListener(WifiEntryCallback listener) {
mListener = listener;
}
/**
* Listener for changes to the state of the WifiEntry.
* This callback will be invoked on the main thread.
*/
public interface WifiEntryCallback {
/**
* Indicates the state of the WifiEntry has changed and clients may retrieve updates through
* the WifiEntry getter methods.
*/
@MainThread
void onUpdated();
}
@AnyThread
protected void notifyOnUpdated() {
if (mListener != null) {
mCallbackHandler.post(() -> mListener.onUpdated());
}
}
/**
* Listener for changes to the state of the WifiEntry.
* This callback will be invoked on the main thread.
*/
public interface ConnectCallback {
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
CONNECT_STATUS_SUCCESS,
CONNECT_STATUS_FAILURE_NO_CONFIG,
CONNECT_STATUS_FAILURE_UNKNOWN
})
public @interface ConnectStatus {}
int CONNECT_STATUS_SUCCESS = 0;
int CONNECT_STATUS_FAILURE_NO_CONFIG = 1;
int CONNECT_STATUS_FAILURE_UNKNOWN = 2;
/**
* Result of the connect request indicated by the CONNECT_STATUS constants.
*/
@MainThread
void onConnectResult(@ConnectStatus int status);
}
/**
* Listener for changes to the state of the WifiEntry.
* This callback will be invoked on the main thread.
*/
public interface DisconnectCallback {
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
DISCONNECT_STATUS_SUCCESS,
DISCONNECT_STATUS_FAILURE_UNKNOWN
})
public @interface DisconnectStatus {}
int DISCONNECT_STATUS_SUCCESS = 0;
int DISCONNECT_STATUS_FAILURE_UNKNOWN = 1;
/**
* Result of the disconnect request indicated by the DISCONNECT_STATUS constants.
*/
@MainThread
void onDisconnectResult(@DisconnectStatus int status);
}
/**
* Listener for changes to the state of the WifiEntry.
* This callback will be invoked on the main thread.
*/
public interface ForgetCallback {
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
FORGET_STATUS_SUCCESS,
FORGET_STATUS_FAILURE_UNKNOWN
})
public @interface ForgetStatus {}
int FORGET_STATUS_SUCCESS = 0;
int FORGET_STATUS_FAILURE_UNKNOWN = 1;
/**
* Result of the forget request indicated by the FORGET_STATUS constants.
*/
@MainThread
void onForgetResult(@ForgetStatus int status);
}
/**
* Listener for changes to the state of the WifiEntry.
* This callback will be invoked on the main thread.
*/
public interface SignInCallback {
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
SIGNIN_STATUS_SUCCESS,
SIGNIN_STATUS_FAILURE_UNKNOWN
})
public @interface SignInStatus {}
int SIGNIN_STATUS_SUCCESS = 0;
int SIGNIN_STATUS_FAILURE_UNKNOWN = 1;
/**
* Result of the sign-in request indicated by the SIGNIN_STATUS constants.
*/
@MainThread
void onSignInResult(@SignInStatus int status);
}
/**
* Returns whether or not the supplied WifiInfo and NetworkInfo represent this WifiEntry
*/
protected abstract boolean connectionInfoMatches(@NonNull WifiInfo wifiInfo,
@NonNull NetworkInfo networkInfo);
/**
* Updates information regarding the current network connection. If the supplied WifiInfo and
* NetworkInfo do not match this WifiEntry, then the WifiEntry will update to be
* unconnected.
*/
@WorkerThread
void updateConnectionInfo(@Nullable WifiInfo wifiInfo, @Nullable NetworkInfo networkInfo) {
if (wifiInfo != null && networkInfo != null
&& connectionInfoMatches(wifiInfo, networkInfo)) {
// Connection info matches, so the WifiInfo/NetworkInfo represent this network and
// the network is currently connecting or connected.
mWifiInfo = wifiInfo;
mNetworkInfo = networkInfo;
final int wifiInfoRssi = wifiInfo.getRssi();
if (wifiInfoRssi != INVALID_RSSI) {
mLevel = mWifiManager.calculateSignalLevel(wifiInfoRssi);
mSpeed = getSpeedFromWifiInfo(mScoreCache, wifiInfo);
}
if (getConnectedState() == CONNECTED_STATE_CONNECTED) {
if (mCalledConnect) {
mCalledConnect = false;
mCallbackHandler.post(() -> {
if (mConnectCallback != null) {
mConnectCallback.onConnectResult(
ConnectCallback.CONNECT_STATUS_SUCCESS);
}
});
}
if (mConnectedInfo == null) {
mConnectedInfo = new ConnectedInfo();
}
mConnectedInfo.frequencyMhz = wifiInfo.getFrequency();
mConnectedInfo.linkSpeedMbps = wifiInfo.getLinkSpeed();
}
} else { // Connection info doesn't matched, so this network is disconnected
mNetworkInfo = null;
mNetworkCapabilities = null;
mConnectedInfo = null;
if (mCalledDisconnect) {
mCalledDisconnect = false;
mCallbackHandler.post(() -> {
if (mDisconnectCallback != null) {
mDisconnectCallback.onDisconnectResult(
DisconnectCallback.DISCONNECT_STATUS_SUCCESS);
}
});
}
}
notifyOnUpdated();
}
// Method for WifiTracker to update the link properties, which is valid for all WifiEntry types.
@WorkerThread
void updateLinkProperties(@Nullable LinkProperties linkProperties) {
if (linkProperties == null || getConnectedState() != CONNECTED_STATE_CONNECTED) {
mConnectedInfo = null;
notifyOnUpdated();
return;
}
if (mConnectedInfo == null) {
mConnectedInfo = new ConnectedInfo();
}
// Find IPv4 and IPv6 addresses, and subnet mask
List<String> ipv6Addresses = new ArrayList<>();
for (LinkAddress addr : linkProperties.getLinkAddresses()) {
if (addr.getAddress() instanceof Inet4Address) {
mConnectedInfo.ipAddress = addr.getAddress().getHostAddress();
try {
InetAddress all = InetAddress.getByAddress(
new byte[]{(byte) 255, (byte) 255, (byte) 255, (byte) 255});
mConnectedInfo.subnetMask = NetworkUtils.getNetworkPart(
all, addr.getPrefixLength()).getHostAddress();
} catch (UnknownHostException e) {
// Leave subnet null;
}
} else if (addr.getAddress() instanceof Inet6Address) {
ipv6Addresses.add(addr.getAddress().getHostAddress());
}
}
mConnectedInfo.ipv6Addresses = ipv6Addresses;
// Find IPv4 default gateway.
for (RouteInfo routeInfo : linkProperties.getRoutes()) {
if (routeInfo.isIPv4Default() && routeInfo.hasGateway()) {
mConnectedInfo.gateway = routeInfo.getGateway().getHostAddress();
break;
}
}
// Find DNS servers
mConnectedInfo.dnsServers = linkProperties.getDnsServers().stream()
.map(InetAddress::getHostAddress).collect(Collectors.toList());
notifyOnUpdated();
}
// Method for WifiTracker to update a connected WifiEntry's network capabilities.
@WorkerThread
void updateNetworkCapabilities(@Nullable NetworkCapabilities capabilities) {
mNetworkCapabilities = capabilities;
}
String getWifiInfoDescription() {
final StringJoiner sj = new StringJoiner(" ");
if (getConnectedState() == CONNECTED_STATE_CONNECTED && mWifiInfo != null) {
sj.add("f = " + mWifiInfo.getFrequency());
final String bssid = mWifiInfo.getBSSID();
if (bssid != null) {
sj.add(bssid);
}
sj.add("standard = " + mWifiInfo.getWifiStandard());
sj.add("rssi = " + mWifiInfo.getRssi());
sj.add("score = " + mWifiInfo.getScore());
sj.add(String.format(" tx=%.1f,", mWifiInfo.getSuccessfulTxPacketsPerSecond()));
sj.add(String.format("%.1f,", mWifiInfo.getRetriedTxPacketsPerSecond()));
sj.add(String.format("%.1f ", mWifiInfo.getLostTxPacketsPerSecond()));
sj.add(String.format("rx=%.1f", mWifiInfo.getSuccessfulRxPacketsPerSecond()));
}
return sj.toString();
}
protected class ConnectActionListener implements WifiManager.ActionListener {
@Override
public void onSuccess() {
mCalledConnect = true;
// If we aren't connected to the network after 10 seconds, trigger the failure callback
mCallbackHandler.postDelayed(() -> {
if (mConnectCallback != null && mCalledConnect
&& getConnectedState() == CONNECTED_STATE_DISCONNECTED) {
mConnectCallback.onConnectResult(
ConnectCallback.CONNECT_STATUS_FAILURE_UNKNOWN);
mCalledConnect = false;
}
}, 10_000 /* delayMillis */);
}
@Override
public void onFailure(int i) {
mCallbackHandler.post(() -> {
if (mConnectCallback != null) {
mConnectCallback.onConnectResult(
mConnectCallback.CONNECT_STATUS_FAILURE_UNKNOWN);
}
});
}
}
protected class ForgetActionListener implements WifiManager.ActionListener {
@Override
public void onSuccess() {
mCallbackHandler.post(() -> {
if (mForgetCallback != null) {
mForgetCallback.onForgetResult(ForgetCallback.FORGET_STATUS_SUCCESS);
}
});
}
@Override
public void onFailure(int i) {
mCallbackHandler.post(() -> {
if (mForgetCallback != null) {
mForgetCallback.onForgetResult(ForgetCallback.FORGET_STATUS_FAILURE_UNKNOWN);
}
});
}
}
// TODO (b/70983952) Come up with a sorting scheme that does the right thing.
@Override
public int compareTo(@NonNull WifiEntry other) {
if (getLevel() != WIFI_LEVEL_UNREACHABLE && other.getLevel() == WIFI_LEVEL_UNREACHABLE) {
return -1;
}
if (getLevel() == WIFI_LEVEL_UNREACHABLE && other.getLevel() != WIFI_LEVEL_UNREACHABLE) {
return 1;
}
if (isSubscription() && !other.isSubscription()) return -1;
if (!isSubscription() && other.isSubscription()) return 1;
if (isSaved() && !other.isSaved()) return -1;
if (!isSaved() && other.isSaved()) return 1;
if (isSuggestion() && !other.isSuggestion()) return -1;
if (!isSuggestion() && other.isSuggestion()) return 1;
if (getLevel() > other.getLevel()) return -1;
if (getLevel() < other.getLevel()) return 1;
return getTitle().compareTo(other.getTitle());
}
@Override
public boolean equals(Object other) {
if (!(other instanceof WifiEntry)) return false;
return getKey().equals(((WifiEntry) other).getKey());
}
@Override
public String toString() {
return new StringBuilder()
.append(getKey())
.append(",title:")
.append(getTitle())
.append(",summary:")
.append(getSummary())
.append(",isSaved:")
.append(isSaved())
.append(",isSubscription:")
.append(isSubscription())
.append(",isSuggestion:")
.append(isSuggestion())
.append(",level:")
.append(getLevel())
.append(",security:")
.append(getSecurity())
.append(",connected:")
.append(getConnectedState() == CONNECTED_STATE_CONNECTED ? "true" : "false")
.append(",connectedInfo:")
.append(getConnectedInfo())
.toString();
}
}