/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.wifi;

import android.content.Context;

import android.net.wifi.ScanResult;

import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiConfiguration.NetworkSelectionStatus;
import android.net.wifi.WifiSsid;
import android.os.Environment;
import android.os.Process;
import android.text.TextUtils;

import android.util.LocalLog;
import android.util.Log;

import com.android.server.net.DelayedDiskWrite;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.FileInputStream;
import java.io.IOException;
import java.text.DateFormat;
import java.util.BitSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Provides an API to read and write the network history from WifiConfigurations to file
 * This is largely separate and extra to the supplicant config file.
 */
public class WifiNetworkHistory {
    public static final String TAG = "WifiNetworkHistory";
    private static final boolean DBG = true;
    private static final boolean VDBG = true;
    static final String NETWORK_HISTORY_CONFIG_FILE = Environment.getDataDirectory()
            + "/misc/wifi/networkHistory.txt";
    /* Network History Keys */
    private static final String SSID_KEY = "SSID";
    static final String CONFIG_KEY = "CONFIG";
    private static final String CONFIG_BSSID_KEY = "CONFIG_BSSID";
    private static final String CHOICE_KEY = "CHOICE";
    private static final String CHOICE_TIME_KEY = "CHOICE_TIME";
    private static final String LINK_KEY = "LINK";
    private static final String BSSID_KEY = "BSSID";
    private static final String BSSID_KEY_END = "/BSSID";
    private static final String RSSI_KEY = "RSSI";
    private static final String FREQ_KEY = "FREQ";
    private static final String DATE_KEY = "DATE";
    private static final String MILLI_KEY = "MILLI";
    private static final String NETWORK_ID_KEY = "ID";
    private static final String PRIORITY_KEY = "PRIORITY";
    private static final String DEFAULT_GW_KEY = "DEFAULT_GW";
    private static final String AUTH_KEY = "AUTH";
    private static final String BSSID_STATUS_KEY = "BSSID_STATUS";
    private static final String SELF_ADDED_KEY = "SELF_ADDED";
    private static final String FAILURE_KEY = "FAILURE";
    private static final String DID_SELF_ADD_KEY = "DID_SELF_ADD";
    private static final String PEER_CONFIGURATION_KEY = "PEER_CONFIGURATION";
    static final String CREATOR_UID_KEY = "CREATOR_UID_KEY";
    private static final String CONNECT_UID_KEY = "CONNECT_UID_KEY";
    private static final String UPDATE_UID_KEY = "UPDATE_UID";
    private static final String FQDN_KEY = "FQDN";
    private static final String SCORER_OVERRIDE_KEY = "SCORER_OVERRIDE";
    private static final String SCORER_OVERRIDE_AND_SWITCH_KEY = "SCORER_OVERRIDE_AND_SWITCH";
    private static final String VALIDATED_INTERNET_ACCESS_KEY = "VALIDATED_INTERNET_ACCESS";
    private static final String NO_INTERNET_ACCESS_REPORTS_KEY = "NO_INTERNET_ACCESS_REPORTS";
    private static final String NO_INTERNET_ACCESS_EXPECTED_KEY = "NO_INTERNET_ACCESS_EXPECTED";
    private static final String EPHEMERAL_KEY = "EPHEMERAL";
    private static final String USE_EXTERNAL_SCORES_KEY = "USE_EXTERNAL_SCORES";
    private static final String METERED_HINT_KEY = "METERED_HINT";
    private static final String NUM_ASSOCIATION_KEY = "NUM_ASSOCIATION";
    private static final String DELETED_EPHEMERAL_KEY = "DELETED_EPHEMERAL";
    private static final String CREATOR_NAME_KEY = "CREATOR_NAME";
    private static final String UPDATE_NAME_KEY = "UPDATE_NAME";
    private static final String USER_APPROVED_KEY = "USER_APPROVED";
    private static final String CREATION_TIME_KEY = "CREATION_TIME";
    private static final String UPDATE_TIME_KEY = "UPDATE_TIME";
    static final String SHARED_KEY = "SHARED";
    private static final String NETWORK_SELECTION_STATUS_KEY = "NETWORK_SELECTION_STATUS";
    private static final String NETWORK_SELECTION_DISABLE_REASON_KEY =
            "NETWORK_SELECTION_DISABLE_REASON";
    private static final String HAS_EVER_CONNECTED_KEY = "HAS_EVER_CONNECTED";

    private static final String SEPARATOR = ":  ";
    private static final String NL = "\n";

    protected final DelayedDiskWrite mWriter;
    Context mContext;
    private final LocalLog mLocalLog;
    /*
     * Lost config list, whenever we read a config from networkHistory.txt that was not in
     * wpa_supplicant.conf
     */
    HashSet<String> mLostConfigsDbg = new HashSet<String>();

    public WifiNetworkHistory(Context c, LocalLog localLog, DelayedDiskWrite writer) {
        mContext = c;
        mWriter = writer;
        mLocalLog = localLog;
    }

    /**
     * Write network history to file, for configured networks
     *
     * @param networks List of ConfiguredNetworks to write to NetworkHistory
     */
    public void writeKnownNetworkHistory(final List<WifiConfiguration> networks,
            final ConcurrentHashMap<Integer, ScanDetailCache> scanDetailCaches,
            final Set<String> deletedEphemeralSSIDs) {

        /* Make a copy */
        //final List<WifiConfiguration> networks = new ArrayList<WifiConfiguration>();

        //for (WifiConfiguration config : mConfiguredNetworks.valuesForAllUsers()) {
        //    networks.add(new WifiConfiguration(config));
        //}

        mWriter.write(NETWORK_HISTORY_CONFIG_FILE, new DelayedDiskWrite.Writer() {
            public void onWriteCalled(DataOutputStream out) throws IOException {
                for (WifiConfiguration config : networks) {
                    //loge("onWriteCalled write SSID: " + config.SSID);
                   /* if (config.getLinkProperties() != null)
                        loge(" lp " + config.getLinkProperties().toString());
                    else
                        loge("attempt config w/o lp");
                    */
                    NetworkSelectionStatus status = config.getNetworkSelectionStatus();
                    if (VDBG) {
                        int numlink = 0;
                        if (config.linkedConfigurations != null) {
                            numlink = config.linkedConfigurations.size();
                        }
                        String disableTime;
                        if (config.getNetworkSelectionStatus().isNetworkEnabled()) {
                            disableTime = "";
                        } else {
                            disableTime = "Disable time: " + DateFormat.getInstance().format(
                                    config.getNetworkSelectionStatus().getDisableTime());
                        }
                        logd("saving network history: " + config.configKey()  + " gw: "
                                + config.defaultGwMacAddress + " Network Selection-status: "
                                + status.getNetworkStatusString()
                                + disableTime + " ephemeral=" + config.ephemeral
                                + " choice:" + status.getConnectChoice()
                                + " link:" + numlink
                                + " status:" + config.status
                                + " nid:" + config.networkId
                                + " hasEverConnected: " + status.getHasEverConnected());
                    }

                    if (!isValid(config)) {
                        continue;
                    }

                    if (config.SSID == null) {
                        if (VDBG) {
                            logv("writeKnownNetworkHistory trying to write config with null SSID");
                        }
                        continue;
                    }
                    if (VDBG) {
                        logv("writeKnownNetworkHistory write config " + config.configKey());
                    }
                    out.writeUTF(CONFIG_KEY + SEPARATOR + config.configKey() + NL);

                    if (config.SSID != null) {
                        out.writeUTF(SSID_KEY + SEPARATOR + config.SSID + NL);
                    }
                    if (config.BSSID != null) {
                        out.writeUTF(CONFIG_BSSID_KEY + SEPARATOR + config.BSSID + NL);
                    } else {
                        out.writeUTF(CONFIG_BSSID_KEY + SEPARATOR + "null" + NL);
                    }
                    if (config.FQDN != null) {
                        out.writeUTF(FQDN_KEY + SEPARATOR + config.FQDN + NL);
                    }

                    out.writeUTF(PRIORITY_KEY + SEPARATOR + Integer.toString(config.priority) + NL);
                    out.writeUTF(NETWORK_ID_KEY + SEPARATOR
                            + Integer.toString(config.networkId) + NL);
                    out.writeUTF(SELF_ADDED_KEY + SEPARATOR
                            + Boolean.toString(config.selfAdded) + NL);
                    out.writeUTF(DID_SELF_ADD_KEY + SEPARATOR
                            + Boolean.toString(config.didSelfAdd) + NL);
                    out.writeUTF(NO_INTERNET_ACCESS_REPORTS_KEY + SEPARATOR
                            + Integer.toString(config.numNoInternetAccessReports) + NL);
                    out.writeUTF(VALIDATED_INTERNET_ACCESS_KEY + SEPARATOR
                            + Boolean.toString(config.validatedInternetAccess) + NL);
                    out.writeUTF(NO_INTERNET_ACCESS_EXPECTED_KEY + SEPARATOR +
                            Boolean.toString(config.noInternetAccessExpected) + NL);
                    out.writeUTF(EPHEMERAL_KEY + SEPARATOR
                            + Boolean.toString(config.ephemeral) + NL);
                    out.writeUTF(METERED_HINT_KEY + SEPARATOR
                            + Boolean.toString(config.meteredHint) + NL);
                    out.writeUTF(USE_EXTERNAL_SCORES_KEY + SEPARATOR
                            + Boolean.toString(config.useExternalScores) + NL);
                    if (config.creationTime != null) {
                        out.writeUTF(CREATION_TIME_KEY + SEPARATOR + config.creationTime + NL);
                    }
                    if (config.updateTime != null) {
                        out.writeUTF(UPDATE_TIME_KEY + SEPARATOR + config.updateTime + NL);
                    }
                    if (config.peerWifiConfiguration != null) {
                        out.writeUTF(PEER_CONFIGURATION_KEY + SEPARATOR
                                + config.peerWifiConfiguration + NL);
                    }
                    out.writeUTF(SCORER_OVERRIDE_KEY + SEPARATOR
                            + Integer.toString(config.numScorerOverride) + NL);
                    out.writeUTF(SCORER_OVERRIDE_AND_SWITCH_KEY + SEPARATOR
                            + Integer.toString(config.numScorerOverrideAndSwitchedNetwork) + NL);
                    out.writeUTF(NUM_ASSOCIATION_KEY + SEPARATOR
                            + Integer.toString(config.numAssociation) + NL);
                    out.writeUTF(CREATOR_UID_KEY + SEPARATOR
                            + Integer.toString(config.creatorUid) + NL);
                    out.writeUTF(CONNECT_UID_KEY + SEPARATOR
                            + Integer.toString(config.lastConnectUid) + NL);
                    out.writeUTF(UPDATE_UID_KEY + SEPARATOR
                            + Integer.toString(config.lastUpdateUid) + NL);
                    out.writeUTF(CREATOR_NAME_KEY + SEPARATOR
                            + config.creatorName + NL);
                    out.writeUTF(UPDATE_NAME_KEY + SEPARATOR
                            + config.lastUpdateName + NL);
                    out.writeUTF(USER_APPROVED_KEY + SEPARATOR
                            + Integer.toString(config.userApproved) + NL);
                    out.writeUTF(SHARED_KEY + SEPARATOR + Boolean.toString(config.shared) + NL);
                    String allowedKeyManagementString =
                            makeString(config.allowedKeyManagement,
                                    WifiConfiguration.KeyMgmt.strings);
                    out.writeUTF(AUTH_KEY + SEPARATOR
                            + allowedKeyManagementString + NL);
                    out.writeUTF(NETWORK_SELECTION_STATUS_KEY + SEPARATOR
                            + status.getNetworkSelectionStatus() + NL);
                    out.writeUTF(NETWORK_SELECTION_DISABLE_REASON_KEY + SEPARATOR
                            + status.getNetworkSelectionDisableReason() + NL);

                    if (status.getConnectChoice() != null) {
                        out.writeUTF(CHOICE_KEY + SEPARATOR + status.getConnectChoice() + NL);
                        out.writeUTF(CHOICE_TIME_KEY + SEPARATOR
                                + status.getConnectChoiceTimestamp() + NL);
                    }

                    if (config.linkedConfigurations != null) {
                        log("writeKnownNetworkHistory write linked "
                                + config.linkedConfigurations.size());

                        for (String key : config.linkedConfigurations.keySet()) {
                            out.writeUTF(LINK_KEY + SEPARATOR + key + NL);
                        }
                    }

                    String macAddress = config.defaultGwMacAddress;
                    if (macAddress != null) {
                        out.writeUTF(DEFAULT_GW_KEY + SEPARATOR + macAddress + NL);
                    }

                    if (getScanDetailCache(config, scanDetailCaches) != null) {
                        for (ScanDetail scanDetail : getScanDetailCache(config,
                                    scanDetailCaches).values()) {
                            ScanResult result = scanDetail.getScanResult();
                            out.writeUTF(BSSID_KEY + SEPARATOR
                                    + result.BSSID + NL);
                            out.writeUTF(FREQ_KEY + SEPARATOR
                                    + Integer.toString(result.frequency) + NL);

                            out.writeUTF(RSSI_KEY + SEPARATOR
                                    + Integer.toString(result.level) + NL);

                            out.writeUTF(BSSID_KEY_END + NL);
                        }
                    }
                    if (config.lastFailure != null) {
                        out.writeUTF(FAILURE_KEY + SEPARATOR + config.lastFailure + NL);
                    }
                    out.writeUTF(HAS_EVER_CONNECTED_KEY + SEPARATOR
                            + Boolean.toString(status.getHasEverConnected()) + NL);
                    out.writeUTF(NL);
                    // Add extra blank lines for clarity
                    out.writeUTF(NL);
                    out.writeUTF(NL);
                }
                if (deletedEphemeralSSIDs != null && deletedEphemeralSSIDs.size() > 0) {
                    for (String ssid : deletedEphemeralSSIDs) {
                        out.writeUTF(DELETED_EPHEMERAL_KEY);
                        out.writeUTF(ssid);
                        out.writeUTF(NL);
                    }
                }
            }
        });
    }

    /**
     * Adds information stored in networkHistory.txt to the given configs. The configs are provided
     * as a mapping from configKey to WifiConfiguration, because the WifiConfigurations themselves
     * do not contain sufficient information to compute their configKeys until after the information
     * that is stored in networkHistory.txt has been added to them.
     *
     * @param configs mapping from configKey to a WifiConfiguration that contains the information
     *         information read from wpa_supplicant.conf
     */
    public void readNetworkHistory(Map<String, WifiConfiguration> configs,
            ConcurrentHashMap<Integer, ScanDetailCache> scanDetailCaches,
            Set<String> deletedEphemeralSSIDs) {
        localLog("readNetworkHistory() path:" + NETWORK_HISTORY_CONFIG_FILE);

        try (DataInputStream in =
                     new DataInputStream(new BufferedInputStream(
                             new FileInputStream(NETWORK_HISTORY_CONFIG_FILE)))) {

            String bssid = null;
            String ssid = null;

            int freq = 0;
            int status = 0;
            long seen = 0;
            int rssi = WifiConfiguration.INVALID_RSSI;
            String caps = null;

            WifiConfiguration config = null;
            while (true) {
                String line = in.readUTF();
                if (line == null) {
                    break;
                }
                int colon = line.indexOf(':');
                if (colon < 0) {
                    continue;
                }

                String key = line.substring(0, colon).trim();
                String value = line.substring(colon + 1).trim();

                if (key.equals(CONFIG_KEY)) {
                    config = configs.get(value);

                    // skip reading that configuration data
                    // since we don't have a corresponding network ID
                    if (config == null) {
                        localLog("readNetworkHistory didnt find netid for hash="
                                + Integer.toString(value.hashCode())
                                + " key: " + value);
                        mLostConfigsDbg.add(value);
                        continue;
                    } else {
                        // After an upgrade count old connections as owned by system
                        if (config.creatorName == null || config.lastUpdateName == null) {
                            config.creatorName =
                                mContext.getPackageManager().getNameForUid(Process.SYSTEM_UID);
                            config.lastUpdateName = config.creatorName;

                            if (DBG) {
                                Log.w(TAG, "Upgrading network " + config.networkId
                                        + " to " + config.creatorName);
                            }
                        }
                    }
                } else if (config != null) {
                    NetworkSelectionStatus networkStatus = config.getNetworkSelectionStatus();
                    switch (key) {
                        case SSID_KEY:
                            if (config.isPasspoint()) {
                                break;
                            }
                            ssid = value;
                            if (config.SSID != null && !config.SSID.equals(ssid)) {
                                loge("Error parsing network history file, mismatched SSIDs");
                                config = null; //error
                                ssid = null;
                            } else {
                                config.SSID = ssid;
                            }
                            break;
                        case CONFIG_BSSID_KEY:
                            config.BSSID = value.equals("null") ? null : value;
                            break;
                        case FQDN_KEY:
                            // Check for literal 'null' to be backwards compatible.
                            config.FQDN = value.equals("null") ? null : value;
                            break;
                        case DEFAULT_GW_KEY:
                            config.defaultGwMacAddress = value;
                            break;
                        case SELF_ADDED_KEY:
                            config.selfAdded = Boolean.parseBoolean(value);
                            break;
                        case DID_SELF_ADD_KEY:
                            config.didSelfAdd = Boolean.parseBoolean(value);
                            break;
                        case NO_INTERNET_ACCESS_REPORTS_KEY:
                            config.numNoInternetAccessReports = Integer.parseInt(value);
                            break;
                        case VALIDATED_INTERNET_ACCESS_KEY:
                            config.validatedInternetAccess = Boolean.parseBoolean(value);
                            break;
                        case NO_INTERNET_ACCESS_EXPECTED_KEY:
                            config.noInternetAccessExpected = Boolean.parseBoolean(value);
                            break;
                        case CREATION_TIME_KEY:
                            config.creationTime = value;
                            break;
                        case UPDATE_TIME_KEY:
                            config.updateTime = value;
                            break;
                        case EPHEMERAL_KEY:
                            config.ephemeral = Boolean.parseBoolean(value);
                            break;
                        case METERED_HINT_KEY:
                            config.meteredHint = Boolean.parseBoolean(value);
                            break;
                        case USE_EXTERNAL_SCORES_KEY:
                            config.useExternalScores = Boolean.parseBoolean(value);
                            break;
                        case CREATOR_UID_KEY:
                            config.creatorUid = Integer.parseInt(value);
                            break;
                        case SCORER_OVERRIDE_KEY:
                            config.numScorerOverride = Integer.parseInt(value);
                            break;
                        case SCORER_OVERRIDE_AND_SWITCH_KEY:
                            config.numScorerOverrideAndSwitchedNetwork = Integer.parseInt(value);
                            break;
                        case NUM_ASSOCIATION_KEY:
                            config.numAssociation = Integer.parseInt(value);
                            break;
                        case CONNECT_UID_KEY:
                            config.lastConnectUid = Integer.parseInt(value);
                            break;
                        case UPDATE_UID_KEY:
                            config.lastUpdateUid = Integer.parseInt(value);
                            break;
                        case FAILURE_KEY:
                            config.lastFailure = value;
                            break;
                        case PEER_CONFIGURATION_KEY:
                            config.peerWifiConfiguration = value;
                            break;
                        case NETWORK_SELECTION_STATUS_KEY:
                            int networkStatusValue = Integer.parseInt(value);
                            // Reset temporarily disabled network status
                            if (networkStatusValue ==
                                    NetworkSelectionStatus.NETWORK_SELECTION_TEMPORARY_DISABLED) {
                                networkStatusValue =
                                        NetworkSelectionStatus.NETWORK_SELECTION_ENABLED;
                            }
                            networkStatus.setNetworkSelectionStatus(networkStatusValue);
                            break;
                        case NETWORK_SELECTION_DISABLE_REASON_KEY:
                            networkStatus.setNetworkSelectionDisableReason(Integer.parseInt(value));
                            break;
                        case CHOICE_KEY:
                            networkStatus.setConnectChoice(value);
                            break;
                        case CHOICE_TIME_KEY:
                            networkStatus.setConnectChoiceTimestamp(Long.parseLong(value));
                            break;
                        case LINK_KEY:
                            if (config.linkedConfigurations == null) {
                                config.linkedConfigurations = new HashMap<>();
                            } else {
                                config.linkedConfigurations.put(value, -1);
                            }
                            break;
                        case BSSID_KEY:
                            status = 0;
                            ssid = null;
                            bssid = null;
                            freq = 0;
                            seen = 0;
                            rssi = WifiConfiguration.INVALID_RSSI;
                            caps = "";
                            break;
                        case RSSI_KEY:
                            rssi = Integer.parseInt(value);
                            break;
                        case FREQ_KEY:
                            freq = Integer.parseInt(value);
                            break;
                        case DATE_KEY:
                            /*
                             * when reading the configuration from file we don't update the date
                             * so as to avoid reading back stale or non-sensical data that would
                             * depend on network time.
                             * The date of a WifiConfiguration should only come from actual scan
                             * result.
                             *
                            String s = key.replace(FREQ_KEY, "");
                            seen = Integer.getInteger(s);
                            */
                            break;
                        case BSSID_KEY_END:
                            if ((bssid != null) && (ssid != null)) {
                                if (getScanDetailCache(config, scanDetailCaches) != null) {
                                    WifiSsid wssid = WifiSsid.createFromAsciiEncoded(ssid);
                                    ScanDetail scanDetail = new ScanDetail(wssid, bssid,
                                            caps, rssi, freq, (long) 0, seen);
                                    getScanDetailCache(config, scanDetailCaches).put(scanDetail);
                                }
                            }
                            break;
                        case DELETED_EPHEMERAL_KEY:
                            if (!TextUtils.isEmpty(value)) {
                                deletedEphemeralSSIDs.add(value);
                            }
                            break;
                        case CREATOR_NAME_KEY:
                            config.creatorName = value;
                            break;
                        case UPDATE_NAME_KEY:
                            config.lastUpdateName = value;
                            break;
                        case USER_APPROVED_KEY:
                            config.userApproved = Integer.parseInt(value);
                            break;
                        case SHARED_KEY:
                            config.shared = Boolean.parseBoolean(value);
                            break;
                        case HAS_EVER_CONNECTED_KEY:
                            networkStatus.setHasEverConnected(Boolean.parseBoolean(value));
                            break;
                    }
                }
            }
        } catch (NumberFormatException e) {
            Log.e(TAG, "readNetworkHistory: failed to read, revert to default, " + e, e);
        } catch (EOFException e) {
            // do nothing
        } catch (IOException e) {
            Log.e(TAG, "readNetworkHistory: No config file, revert to default, " + e, e);
        }
    }

    /**
     * Ported this out of WifiServiceImpl, I have no idea what it's doing
     * <TODO> figure out what/why this is doing
     * <TODO> Port it into WifiConfiguration, then remove all the silly business from ServiceImpl
     */
    public boolean isValid(WifiConfiguration config) {
        if (config.allowedKeyManagement == null) {
            return false;
        }
        if (config.allowedKeyManagement.cardinality() > 1) {
            if (config.allowedKeyManagement.cardinality() != 2) {
                return false;
            }
            if (!config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_EAP)) {
                return false;
            }
            if ((!config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.IEEE8021X))
                    && (!config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_PSK))) {
                return false;
            }
        }
        return true;
    }

    private static String makeString(BitSet set, String[] strings) {
        StringBuffer buf = new StringBuffer();
        int nextSetBit = -1;

        /* Make sure all set bits are in [0, strings.length) to avoid
         * going out of bounds on strings.  (Shouldn't happen, but...) */
        set = set.get(0, strings.length);

        while ((nextSetBit = set.nextSetBit(nextSetBit + 1)) != -1) {
            buf.append(strings[nextSetBit].replace('_', '-')).append(' ');
        }

        // remove trailing space
        if (set.cardinality() > 0) {
            buf.setLength(buf.length() - 1);
        }

        return buf.toString();
    }

    protected void logv(String s) {
        Log.v(TAG, s);
    }
    protected void logd(String s) {
        Log.d(TAG, s);
    }
    protected void log(String s) {
        Log.d(TAG, s);
    }
    protected void loge(String s) {
        loge(s, false);
    }
    protected void loge(String s, boolean stack) {
        if (stack) {
            Log.e(TAG, s + " stack:" + Thread.currentThread().getStackTrace()[2].getMethodName()
                    + " - " + Thread.currentThread().getStackTrace()[3].getMethodName()
                    + " - " + Thread.currentThread().getStackTrace()[4].getMethodName()
                    + " - " + Thread.currentThread().getStackTrace()[5].getMethodName());
        } else {
            Log.e(TAG, s);
        }
    }

    private void localLog(String s) {
        if (mLocalLog != null) {
            mLocalLog.log(s);
        }
    }

    private ScanDetailCache getScanDetailCache(WifiConfiguration config,
            ConcurrentHashMap<Integer, ScanDetailCache> scanDetailCaches) {
        if (config == null || scanDetailCaches == null) return null;
        ScanDetailCache cache = scanDetailCaches.get(config.networkId);
        if (cache == null && config.networkId != WifiConfiguration.INVALID_NETWORK_ID) {
            cache = new ScanDetailCache(config);
            scanDetailCaches.put(config.networkId, cache);
        }
        return cache;
    }
}
