/*
 * Copyright (C) 2007 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.settings.wifi;

import com.android.settings.R;

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.WifiConfiguration.AuthAlgorithm;
import android.net.wifi.WifiConfiguration.GroupCipher;
import android.net.wifi.WifiConfiguration.KeyMgmt;
import android.net.wifi.WifiConfiguration.PairwiseCipher;
import android.net.wifi.WifiConfiguration.Protocol;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Log;

public final class AccessPointState implements Comparable<AccessPointState>, Parcelable {

    private static final String TAG = "AccessPointState";
    
    // Constants used for different security types
    public static final String WPA2 = "WPA2";
    public static final String WPA = "WPA";
    public static final String WEP = "WEP";
    public static final String OPEN = "Open";

    /* For EAP Enterprise fields */
    public static final String WPA_EAP = "-EAP";
    public static final String IEEE8021X = "IEEE8021X";

    public static final String[] EAP_METHOD = { "PEAP", "TLS", "TTLS" };

    /** String present in capabilities if the scan result is ad-hoc */
    private static final String ADHOC_CAPABILITY = "[IBSS]";
    /** String present in capabilities if the scan result is enterprise secured */
    private static final String ENTERPRISE_CAPABILITY = "-EAP-";

    public static final String BSSID_ANY = "any";
    public static final int NETWORK_ID_NOT_SET = -1;
    /** This should be used with care! */
    static final int NETWORK_ID_ANY = -2;
    
    public static final int MATCH_NONE = 0;
    public static final int MATCH_WEAK = 1;
    public static final int MATCH_STRONG = 2;
    public static final int MATCH_EXACT = 3;
    
    // Don't set these directly, use the setters.
    public int networkId;
    public int priority;
    public boolean hiddenSsid;
    public int linkSpeed;
    public int ipAddress;
    public String bssid;
    public String ssid;
    public int signal;
    public boolean primary;
    public boolean seen;
    public boolean configured;
    public NetworkInfo.DetailedState status;
    public String security;
    public boolean disabled;
    
    /**
     * Use this for sorting based on signal strength. It is a heavily-damped
     * time-averaged weighted signal.
     */
    private float signalForSorting = Float.MIN_VALUE;
    
    private static final float DAMPING_FACTOR = 0.2f;
    
    /**
     * This will be a user entered password, and NOT taken from wpa_supplicant
     * (since it would give us *)
     */
    private String mPassword; 
    private boolean mConfigHadPassword;
    
    public static final int WEP_PASSWORD_AUTO = 0;
    public static final int WEP_PASSWORD_ASCII = 1;
    public static final int WEP_PASSWORD_HEX = 2;
    private int mWepPasswordType;

    /* Enterprise Fields */
    public static final int IDENTITY = 0;
    public static final int ANONYMOUS_IDENTITY = 1;
    public static final int PRIVATE_KEY_PASSWD = 2;
    public static final int CLIENT_CERT = 3;
    public static final int CA_CERT = 4;
    public static final int PRIVATE_KEY = 5;
    public static final int MAX_ENTRPRISE_FIELD = 6;
    private String mEnterpriseFields[] = new String[MAX_ENTRPRISE_FIELD];
    private String mEap;
    private String mPhase2;

    private Context mContext;

    /**
     * If > 0, don't refresh (changes are being batched), use
     * {@link #blockRefresh()} and {@link #unblockRefresh()} only.
     */
    private int mBlockRefresh;
    /**
     * This will be set by {@link #requestRefresh} and shouldn't be written to
     * elsewhere.
     */
    private boolean mNeedsRefresh;    
    
    private AccessPointStateCallback mCallback;
    
    private StringBuilder mSummaryBuilder = new StringBuilder();
    
    interface AccessPointStateCallback {
        void refreshAccessPointState();
    }
    
    public AccessPointState(Context context) {
        this();
        
        setContext(context);
    }

    private AccessPointState() {
        bssid = BSSID_ANY;
        ssid = "";
        networkId = NETWORK_ID_NOT_SET;
        hiddenSsid = false;
    }

    void setContext(Context context) {
        mContext = context;
    }

    public void setNetworkId(int networkId) {
        this.networkId = networkId;
    }
    
    public void setBssid(String bssid) {
        if (bssid != null) {
            // If the BSSID is a wildcard, do NOT let a specific BSSID replace it
            if (!this.bssid.equals(BSSID_ANY)) {
                this.bssid = bssid;
            }
        }
    }

    private String getWpaSupplicantBssid() {
        return bssid.equals(BSSID_ANY) ? null : bssid;
    }
    
    public static String convertToQuotedString(String string) {
        if (TextUtils.isEmpty(string)) {
            return "";
        }
        
        final int lastPos = string.length() - 1;
        if (lastPos < 0 || (string.charAt(0) == '"' && string.charAt(lastPos) == '"')) {
            return string;
        }
        
        return "\"" + string + "\"";
    }
    
    public void setPrimary(boolean primary) {
        if (this.primary != primary) {
            this.primary = primary;
            requestRefresh();
        }
    }
    
    public void setSeen(boolean seen) {
        if (this.seen != seen) {
            this.seen = seen;
            requestRefresh();
        }
    }
    
    public void setDisabled(boolean disabled) {
        if (this.disabled != disabled) {
            this.disabled = disabled;
            requestRefresh();
        }
    }
    
    public void setSignal(int signal) {

        if (signalForSorting == Float.MIN_VALUE) {
            signalForSorting = signal;
        } else {
            signalForSorting = (DAMPING_FACTOR * signal) + ((1-DAMPING_FACTOR) * signalForSorting);
        }

        if (this.signal != signal) {
            this.signal = signal;
            requestRefresh();
        }
    }
    
    public String getHumanReadableSsid() {
        if (TextUtils.isEmpty(ssid)) {
            return "";
        }
        
        final int lastPos = ssid.length() - 1;
        if (ssid.charAt(0) == '"' && ssid.charAt(lastPos) == '"') {
            return ssid.substring(1, lastPos);
        }
        
        return ssid;
    }
    
    public void setSsid(String ssid) {
        if (ssid != null) {
            this.ssid = convertToQuotedString(ssid);
            requestRefresh();
        }
    }
    
    public void setPriority(int priority) {
        if (this.priority != priority) {
            this.priority = priority;
            requestRefresh();
        }
    }

    public void setHiddenSsid(boolean hiddenSsid) {
        if (this.hiddenSsid != hiddenSsid) {
            this.hiddenSsid = hiddenSsid;
            requestRefresh();
        }
    }

    public void setLinkSpeed(int linkSpeed) {
        if (this.linkSpeed != linkSpeed) {
            this.linkSpeed = linkSpeed;
            requestRefresh();
        }
    }
    
    public void setIpAddress(int address) {
        if (ipAddress != address) {
            ipAddress = address;
            requestRefresh();
        }
    }
    
    public void setConfigured(boolean configured) {
        if (this.configured != configured) {
            this.configured = configured;
            requestRefresh();
        }
    }
    
    public void setStatus(NetworkInfo.DetailedState status) {
        if (this.status != status) {
            this.status = status;
            requestRefresh();
        }
    }

    public boolean isEnterprise() {
        return (WPA_EAP.equals(security) ||
                AccessPointState.IEEE8021X.equals(security));
    }

    public void setSecurity(String security) {
        if (TextUtils.isEmpty(this.security) || !this.security.equals(security)) {
            this.security = security;
            requestRefresh();
        }
    }

    public boolean hasSecurity() {
        return security != null && !security.contains(OPEN);
    }
    
    public String getHumanReadableSecurity() {
        if (security.equals(OPEN)) return mContext.getString(R.string.wifi_security_open);
        else if (security.equals(WEP)) return mContext.getString(R.string.wifi_security_wep);
        else if (security.equals(WPA)) return mContext.getString(R.string.wifi_security_wpa);
        else if (security.equals(WPA2)) return mContext.getString(R.string.wifi_security_wpa2);
        else if (security.equals(WPA_EAP)) return mContext.getString(R.string.wifi_security_wpa_eap);
        else if (security.equals(IEEE8021X)) return mContext.getString(R.string.wifi_security_ieee8021x);

        return mContext.getString(R.string.wifi_security_unknown);
    }
    
    public void updateFromScanResult(ScanResult scanResult) {
        blockRefresh();
        
        // We don't keep specific AP BSSIDs and instead leave that as wildcard
        
        setSeen(true);
        setSsid(scanResult.SSID);
        if (networkId == NETWORK_ID_NOT_SET) {
            // Since ScanResults don't cross-reference network ID, we set it as a wildcard
            setNetworkId(NETWORK_ID_ANY);
        }
        setSignal(scanResult.level);
        setSecurity(getScanResultSecurity(scanResult));
        unblockRefresh();
    }
    
    /**
     * @return The security of a given {@link ScanResult}.
     */
    public static String getScanResultSecurity(ScanResult scanResult) {
        final String cap = scanResult.capabilities;
        final String[] securityModes = { WEP, WPA, WPA2, WPA_EAP, IEEE8021X };
        for (int i = securityModes.length - 1; i >= 0; i--) {
            if (cap.contains(securityModes[i])) {
                return securityModes[i];
            }
        }
        
        return OPEN;
    }
    
    /**
     * @return Whether the given ScanResult represents an adhoc network.
     */
    public static boolean isAdhoc(ScanResult scanResult) {
        return scanResult.capabilities.contains(ADHOC_CAPABILITY);
    }
    
    /**
     * @return Whether the given ScanResult has enterprise security.
     */
    public static boolean isEnterprise(ScanResult scanResult) {
        return scanResult.capabilities.contains(ENTERPRISE_CAPABILITY);
    }
    
    public void updateFromWifiConfiguration(WifiConfiguration wifiConfig) {
        if (wifiConfig != null) {
            blockRefresh();
            setBssid(wifiConfig.BSSID);
            setNetworkId(wifiConfig.networkId);
            setPriority(wifiConfig.priority);
            setHiddenSsid(wifiConfig.hiddenSSID);
            setSsid(wifiConfig.SSID);
            setConfigured(true);
            setDisabled(wifiConfig.status == WifiConfiguration.Status.DISABLED);
            parseWifiConfigurationSecurity(wifiConfig);
            unblockRefresh();
        }
    }
    
    public void setPassword(String password) {
        setPassword(password, WEP_PASSWORD_AUTO);
    }
    
    public void setPassword(String password, int wepPasswordType) {
        mPassword = password;
        mWepPasswordType = wepPasswordType;
    }

    /* For Enterprise Fields */
    public void setEnterpriseField(int field, String value) {
        if ((value != null) && (field >= 0) && (field < MAX_ENTRPRISE_FIELD)) {
            this.mEnterpriseFields[field] = value;
            requestRefresh();
        }
    }

    public void setPhase2(String phase2) {
        if (!TextUtils.isEmpty(phase2) && (!phase2.equals("None"))) {
            mPhase2 = phase2;
        }
    }

    public String getPhase2() {
        return mPhase2;
    }

    public void setEap(int method) {
        mEap =  EAP_METHOD[method];
        requestRefresh();
    }

    public String getEap() {
        return mEap;
    }
    public String getEnterpriseField(int field) {
        if(field >=0 && field < MAX_ENTRPRISE_FIELD) {
            return mEnterpriseFields[field];
        }
        return null;
    }

    public boolean hasPassword() {
        return !TextUtils.isEmpty(mPassword) || mConfigHadPassword; 
    }
    
    private static boolean hasPassword(WifiConfiguration wifiConfig) {
        return !TextUtils.isEmpty(wifiConfig.preSharedKey)
                || !TextUtils.isEmpty(wifiConfig.wepKeys[0])
                || !TextUtils.isEmpty(wifiConfig.wepKeys[1])
                || !TextUtils.isEmpty(wifiConfig.wepKeys[2])
                || !TextUtils.isEmpty(wifiConfig.wepKeys[3]);        
    }
    
    private void parseWifiConfigurationSecurity(WifiConfiguration wifiConfig) {
        setSecurity(getWifiConfigurationSecurity(wifiConfig));
        mConfigHadPassword = hasPassword(wifiConfig);
    }
    
    /**
     * @return The security of a given {@link WifiConfiguration}.
     */
    public static String getWifiConfigurationSecurity(WifiConfiguration wifiConfig) {

        if (wifiConfig.allowedKeyManagement.get(KeyMgmt.NONE)) {
            // If we never set group ciphers, wpa_supplicant puts all of them.
            // For open, we don't set group ciphers.
            // For WEP, we specifically only set WEP40 and WEP104, so CCMP
            // and TKIP should not be there.
            if (!wifiConfig.allowedGroupCiphers.get(GroupCipher.CCMP)
                    && (wifiConfig.allowedGroupCiphers.get(GroupCipher.WEP40)
                            || wifiConfig.allowedGroupCiphers.get(GroupCipher.WEP104))) {
                return WEP;
            } else {
                return OPEN;
            }
        } else if (wifiConfig.allowedKeyManagement.get(KeyMgmt.WPA_EAP)) {
            return WPA_EAP;
        } else if (wifiConfig.allowedKeyManagement.get(KeyMgmt.IEEE8021X)) {
            return IEEE8021X;
        } else if (wifiConfig.allowedProtocols.get(Protocol.RSN)) {
            return WPA2;
        } else if (wifiConfig.allowedProtocols.get(Protocol.WPA)) {
            return WPA;
        } else {
            Log.w(TAG, "Unknown security type from WifiConfiguration, falling back on open.");
            return OPEN;
        }
    }
    
    public void updateFromWifiInfo(WifiInfo wifiInfo, NetworkInfo.DetailedState state) {
        if (wifiInfo != null) {
            blockRefresh();
            setBssid(wifiInfo.getBSSID());
            setLinkSpeed(wifiInfo.getLinkSpeed());
            setNetworkId(wifiInfo.getNetworkId());
            setIpAddress(wifiInfo.getIpAddress());
            setSsid(wifiInfo.getSSID());
            if (state != null) {
                setStatus(state);
            }
            setHiddenSsid(wifiInfo.getHiddenSSID());
            unblockRefresh();
        }
    }
    
    /**
     * @return Whether this AP can be connected to at the moment.
     */
    public boolean isConnectable() {
        return !primary && seen;
    }

    /**
     * @return Whether this AP can be forgotten at the moment.
     */
    public boolean isForgetable() {
        return configured;
    }
    
    /**
     * Updates the state as if it were never configured.
     * <p>
     * Note: This will not pass the forget call to the Wi-Fi API.
     */
    public void forget() {
        blockRefresh();
        setConfigured(false);
        setNetworkId(NETWORK_ID_NOT_SET);
        setPrimary(false);
        setStatus(null);
        setDisabled(false);
        unblockRefresh();
    }
    
    public void updateWifiConfiguration(WifiConfiguration config) {
        config.BSSID = getWpaSupplicantBssid();
        config.priority = priority;
        config.hiddenSSID = hiddenSsid;
        config.SSID = convertToQuotedString(ssid);
        config.eap = mEap;

        if (!TextUtils.isEmpty(mPhase2)) {
            config.phase2 = convertToQuotedString("auth=" + mPhase2);
        } else {
            config.phase2 = null;
        }
        if (!TextUtils.isEmpty(mEnterpriseFields[IDENTITY])) {
            config.identity =
                    convertToQuotedString(mEnterpriseFields[IDENTITY]);
        } else {
            config.identity = null;
        }
        if (!TextUtils.isEmpty(mEnterpriseFields[ANONYMOUS_IDENTITY])) {
            config.anonymousIdentity = convertToQuotedString(
                    mEnterpriseFields[ANONYMOUS_IDENTITY]);
        } else {
            config.anonymousIdentity = null;
        }
        if (!TextUtils.isEmpty(mEnterpriseFields[CLIENT_CERT])) {
            config.clientCert = convertToQuotedString(
                    mEnterpriseFields[CLIENT_CERT]);
        } else {
            config.clientCert = null;
        }
        if (!TextUtils.isEmpty(mEnterpriseFields[CA_CERT])) {
            config.caCert = convertToQuotedString(
                    mEnterpriseFields[CA_CERT]);
        } else {
            config.caCert = null;
        }
        if (!TextUtils.isEmpty(mEnterpriseFields[PRIVATE_KEY])) {
            config.privateKey = convertToQuotedString(
                    mEnterpriseFields[PRIVATE_KEY]);
        } else {
            config.privateKey = null;
        }
        if (!TextUtils.isEmpty(mEnterpriseFields[PRIVATE_KEY_PASSWD])) {
            config.privateKeyPasswd = convertToQuotedString(
                    mEnterpriseFields[PRIVATE_KEY_PASSWD]);
        } else {
            config.privateKeyPasswd = null;
        }
        setupSecurity(config);
    }
    
    private void setupSecurity(WifiConfiguration config) {
        config.allowedAuthAlgorithms.clear();
        config.allowedGroupCiphers.clear();
        config.allowedKeyManagement.clear();
        config.allowedPairwiseCiphers.clear();
        config.allowedProtocols.clear();
        
        if (TextUtils.isEmpty(security)) {
            security = OPEN;
            Log.w(TAG, "Empty security, assuming open");
        }
        
        if (security.equals(WEP)) {
            
            // If password is empty, it should be left untouched
            if (!TextUtils.isEmpty(mPassword)) {
                if (mWepPasswordType == WEP_PASSWORD_AUTO) {
                    if (isHexWepKey(mPassword)) {
                        config.wepKeys[0] = mPassword;
                    } else {
                        config.wepKeys[0] = convertToQuotedString(mPassword);
                    }
                } else {
                    config.wepKeys[0] = mWepPasswordType == WEP_PASSWORD_ASCII
                            ? convertToQuotedString(mPassword)
                            : mPassword;
                }
            }
            
            config.wepTxKeyIndex = 0;
            
            config.allowedAuthAlgorithms.set(AuthAlgorithm.OPEN);
            config.allowedAuthAlgorithms.set(AuthAlgorithm.SHARED);

            config.allowedKeyManagement.set(KeyMgmt.NONE);
            
            config.allowedGroupCiphers.set(GroupCipher.WEP40);
            config.allowedGroupCiphers.set(GroupCipher.WEP104);
            
        } else if (security.equals(WPA) || security.equals(WPA2)){
            config.allowedGroupCiphers.set(GroupCipher.TKIP);
            config.allowedGroupCiphers.set(GroupCipher.CCMP);
            
            config.allowedKeyManagement.set(KeyMgmt.WPA_PSK);
            
            config.allowedPairwiseCiphers.set(PairwiseCipher.CCMP);
            config.allowedPairwiseCiphers.set(PairwiseCipher.TKIP);

            config.allowedProtocols.set(security.equals(WPA2) ? Protocol.RSN : Protocol.WPA);
            
            // If password is empty, it should be left untouched
            if (!TextUtils.isEmpty(mPassword)) {
                if (mPassword.length() == 64 && isHex(mPassword)) {
                    // Goes unquoted as hex
                    config.preSharedKey = mPassword;
                } else {
                    // Goes quoted as ASCII
                    config.preSharedKey = convertToQuotedString(mPassword);
                }
            }
            
        } else if (security.equals(OPEN)) {
            config.allowedKeyManagement.set(KeyMgmt.NONE);
        } else if (security.equals(WPA_EAP) || security.equals(IEEE8021X)) {
            config.allowedGroupCiphers.set(GroupCipher.TKIP);
            config.allowedGroupCiphers.set(GroupCipher.CCMP);
            if (security.equals(WPA_EAP)) {
                config.allowedKeyManagement.set(KeyMgmt.WPA_EAP);
            } else {
                config.allowedKeyManagement.set(KeyMgmt.IEEE8021X);
            }
            if (!TextUtils.isEmpty(mPassword)) {
                config.password = convertToQuotedString(mPassword);
            }
        }
    }
    
    private static boolean isHexWepKey(String wepKey) {
        final int len = wepKey.length();
        
        // WEP-40, WEP-104, and some vendors using 256-bit WEP (WEP-232?)
        if (len != 10 && len != 26 && len != 58) {
            return false;
        }
        
        return isHex(wepKey);
    }
    
    private static boolean isHex(String key) {
        for (int i = key.length() - 1; i >= 0; i--) {
            final char c = key.charAt(i);
            if (!(c >= '0' && c <= '9' || c >= 'A' && c <= 'F' || c >= 'a' && c <= 'f')) {
                return false;
            }
        }
        
        return true;
    }
    
    public void setCallback(AccessPointStateCallback callback) {
        mCallback = callback;
    }
    
    void blockRefresh() {
        mBlockRefresh++;
    }
    
    void unblockRefresh() {
        if (--mBlockRefresh == 0 && mNeedsRefresh) {
            requestRefresh();
        }
    }
    
    private void requestRefresh() {
        if (mBlockRefresh > 0) {
            mNeedsRefresh = true;
            return;
        }
        
        if (mCallback != null) {
            mCallback.refreshAccessPointState();
        }
        
        mNeedsRefresh = false;
    }
    
    /**
     * {@inheritDoc}
     * @see #hashCode()
     * @see #equals(Object)
     */
    public int matches(int otherNetworkId, String otherBssid, String otherSsid,
            String otherSecurity) {
        
        // Whenever this method is touched, please ensure #equals and #hashCode
        // still work with the changes here!
        
        if (otherSsid == null) {
            if (WifiLayer.LOGV) {
                Log.w(TAG, "BSSID: " + otherBssid + ", SSID: " + otherSsid);
            }
            return MATCH_NONE;
        }

        /*
         * If we both have 'security' set, it must match (an open network still
         * has 'security' set to OPEN)
         */
        if (security != null && otherSecurity != null) {
            if (!security.equals(otherSecurity)) {
                return MATCH_NONE;
            }
        }
        
        // WifiConfiguration gives an empty bssid as a BSSID wildcard
        if (TextUtils.isEmpty(otherBssid)) {
            otherBssid = AccessPointState.BSSID_ANY;
        }

        final boolean networkIdMatches = networkId == otherNetworkId;
        if (!networkIdMatches && networkId != NETWORK_ID_ANY && otherNetworkId != NETWORK_ID_ANY) {
            // Network IDs don't match (e.g., 1 & 2 or unset & 1) and neither is a wildcard
            return MATCH_NONE;
        }
        
        if (networkIdMatches && otherNetworkId != NETWORK_ID_NOT_SET
                && otherNetworkId != NETWORK_ID_ANY) {
            // Network ID matches (they're set to the same ID)
            return MATCH_EXACT;
        }
        
        // So now, network IDs aren't set or at least one is a wildcard 
        
        final boolean bssidMatches = bssid.equals(otherBssid);
        final boolean otherBssidIsWildcard = otherBssid.equals(BSSID_ANY);
        if (bssidMatches && !otherBssidIsWildcard) {
            // BSSID matches (and neither is a wildcard)
            return MATCH_STRONG;
        }

        if (!bssidMatches && !bssid.equals(BSSID_ANY) && !otherBssidIsWildcard) {
            // BSSIDs don't match (e.g., 00:24:21:21:42:12 & 42:12:44:21:22:52)
            // and neither is a wildcard
            return MATCH_NONE;
        }
        
        // So now, BSSIDs are both wildcards
        
        final boolean ssidMatches = ssid.equals(otherSsid); 
        if (ssidMatches) {
            // SSID matches
            return MATCH_WEAK;
        }

        return MATCH_NONE;
    }
    
    /**
     * {@inheritDoc}
     * @see #matches(int, String, String)
     * @see #equals(Object)
     */
    @Override
    public int hashCode() {
        // Two equal() objects must have same hashCode.
        // With Wi-Fi, the broadest match is if two SSIDs are the same.  The finer-grained matches
        // imply this (for example, the same network IDs means the same WifiConfiguration which
        // means the same SSID).
        // See #matches for the exact matching algorithm we use.
        return ssid != null ? ssid.hashCode() : 0;
    }

    /**
     * {@inheritDoc}
     * @see #matches(int, String, String)
     * @see #hashCode()
     */
    @Override
    public boolean equals(Object o) {
        if (!o.getClass().equals(getClass())) {
            return false;
        }
        
        final AccessPointState other = (AccessPointState) o;

        // To see which conditions cause two AccessPointStates to be equal, see
        // where #matches returns MATCH_WEAK or greater.
        
        return matches(other.networkId, other.bssid, other.ssid, other.security) >= MATCH_WEAK;
    }

    public int matchesWifiConfiguration(WifiConfiguration wifiConfig) {
        String security = getWifiConfigurationSecurity(wifiConfig);
        return matches(wifiConfig.networkId, wifiConfig.BSSID, wifiConfig.SSID, security);
    }
    
    String getSummarizedStatus() {
        StringBuilder sb = mSummaryBuilder;
        sb.delete(0, sb.length());
        
        if (primary && status != null) {
            buildSummary(sb, WifiStatus.getPrintable(mContext, status), true);
            
        } else if (!seen) {
            buildSummary(sb, mContext.getString(R.string.summary_not_in_range), true);

            // Remembered comes second in this case
            if (!primary && configured) {
                buildSummary(sb, mContext.getString(R.string.summary_remembered), true);
            }
            
        } else {
            if (configured && disabled) {
                // The connection failure overrides all in this case
                return mContext.getString(R.string.summary_connection_failed);
            }

            // Remembered comes first in this case
            if (!primary && configured) {
                buildSummary(sb, mContext.getString(R.string.summary_remembered), true);
            }
            
            // If it is seen (and not the primary), show the security type
            String verboseSecurity = getVerboseSecurity();
            if (verboseSecurity != null) {
                buildSummary(sb, verboseSecurity, true);
            }
        }
        
        return sb.toString();
    }
    
    private String getVerboseSecurity() {
        if (WEP.equals(security)) {
            return mContext.getString(R.string.wifi_security_verbose_wep);
        } else if (WPA.equals(security)) {
            return mContext.getString(R.string.wifi_security_verbose_wpa);
        } else if (WPA2.equals(security)) {
            return mContext.getString(R.string.wifi_security_verbose_wpa2);
        } else if (OPEN.equals(security)) {
            return mContext.getString(R.string.wifi_security_verbose_open);
        } else if (WPA_EAP.equals(security)) {
            return mContext.getString(R.string.wifi_security_verbose_wpa_eap);
        } else if (IEEE8021X.equals(security)) {
            return mContext.getString(R.string.wifi_security_verbose_ieee8021x);
        } else {
            return null;
        }
    }

    private void buildSummary(StringBuilder sb, String string, boolean autoUpperCaseFirstLetter) {
        if (sb.length() == 0) {
            if (autoUpperCaseFirstLetter && string.length() > 1
                    && Character.isLowerCase(string.charAt(0))
                    && !Character.isUpperCase(string.charAt(1))) {
                sb.append(Character.toUpperCase(string.charAt(0))).append(string, 1,
                        string.length());
            } else {
                sb.append(string);
            }
        } else {
            sb.append(", ");
            sb.append(string);
        }
    }

    public int compareTo(AccessPointState other) {
        // This ranks the states for displaying in the AP list, not for
        // connecting to (wpa_supplicant does that using the WifiConfiguration's
        // priority field).
        
        // Clarity > efficiency, of this logic:
        int comparison;
        
        // Primary
        comparison = (other.primary ? 1 : 0) - (primary ? 1 : 0);
        if (comparison != 0) return comparison;
        
        // Currently seen (similar to, but not always the same as within range)
        comparison = (other.seen ? 1 : 0) - (seen ? 1 : 0);
        if (comparison != 0) return comparison;

        // Configured
        comparison = (other.configured ? 1 : 0) - (configured ? 1 : 0);
        if (comparison != 0) return comparison;

        if (!configured) {
            // Neither are configured

            // Open network
            comparison = (hasSecurity() ? 1 : 0) - (other.hasSecurity() ? 1 : 0);
            if (comparison != 0) return comparison;
        }

        // Signal strength
        comparison = (int) (other.signalForSorting - signalForSorting);
        if (comparison != 0) return comparison;

        // Alphabetical
        return ssid.compareToIgnoreCase(other.ssid);
    }

    public String toString() {
        return ssid + " (" + bssid + ", " + networkId + ", " + super.toString() + ")";
    }
    
    /** Implement the Parcelable interface */
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(bssid);
        dest.writeInt(configured ? 1 : 0);
        dest.writeInt(ipAddress);
        dest.writeInt(linkSpeed);
        dest.writeInt(networkId);
        dest.writeInt(primary ? 1 : 0);
        dest.writeInt(priority);
        dest.writeInt(hiddenSsid ? 1 : 0);
        dest.writeString(security);
        dest.writeInt(seen ? 1 : 0);
        dest.writeInt(disabled ? 1 : 0);
        dest.writeInt(signal);
        dest.writeString(ssid);
        dest.writeString(status != null ? status.toString() : null);
        dest.writeString(mPassword);
        dest.writeInt(mConfigHadPassword ? 1 : 0);
        dest.writeInt(mWepPasswordType);
    }

    /** Implement the Parcelable interface */
    public int describeContents() {
        return 0;
    }

    /** Implement the Parcelable interface */
    public static final Creator<AccessPointState> CREATOR =
        new Creator<AccessPointState>() {
            public AccessPointState createFromParcel(Parcel in) {
                AccessPointState state = new AccessPointState();
                state.bssid = in.readString();
                state.configured = in.readInt() == 1;
                state.ipAddress = in.readInt();
                state.linkSpeed = in.readInt();
                state.networkId = in.readInt();
                state.primary = in.readInt() == 1;
                state.priority = in.readInt();
                state.hiddenSsid = in.readInt() == 1;
                state.security = in.readString();
                state.seen = in.readInt() == 1;
                state.disabled = in.readInt() == 1;
                state.signal = in.readInt();
                state.ssid = in.readString();
                String statusStr = in.readString();
                if (statusStr != null) {
                    state.status = NetworkInfo.DetailedState.valueOf(statusStr);
                }
                state.mPassword = in.readString();
                state.mConfigHadPassword = in.readInt() == 1;
                state.mWepPasswordType = in.readInt();
                return state;
            }

            public AccessPointState[] newArray(int size) {
                return new AccessPointState[size];
            }
        };

        
}
