/* * Copyright 2018 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 static android.net.wifi.WifiInfo.DEFAULT_MAC_ADDRESS; import static android.net.wifi.WifiInfo.INVALID_RSSI; import android.annotation.NonNull; import android.annotation.Nullable; import android.net.MacAddress; import android.net.wifi.SupplicantState; import android.net.wifi.WifiSsid; import android.util.ArrayMap; import android.util.Base64; import android.util.Log; import android.util.Pair; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; import com.android.server.wifi.WifiScoreCardProto.AccessPoint; import com.android.server.wifi.WifiScoreCardProto.Event; import com.android.server.wifi.WifiScoreCardProto.Network; import com.android.server.wifi.WifiScoreCardProto.NetworkList; import com.android.server.wifi.WifiScoreCardProto.SecurityType; import com.android.server.wifi.WifiScoreCardProto.Signal; import com.android.server.wifi.WifiScoreCardProto.UnivariateStatistic; import com.android.server.wifi.util.NativeUtil; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.concurrent.NotThreadSafe; /** * Retains statistical information about the performance of various * access points, as experienced by this device. * * The purpose is to better inform future network selection and switching * by this device. */ @NotThreadSafe public class WifiScoreCard { public static final String DUMP_ARG = "WifiScoreCard"; private static final String TAG = "WifiScoreCard"; private static final boolean DBG = false; private final Clock mClock; private final String mL2KeySeed; private MemoryStore mMemoryStore; /** Our view of the memory store */ public interface MemoryStore { /** Requests a read, with asynchronous reply */ void read(String key, BlobListener blobListener); /** Requests a write, does not wait for completion */ void write(String key, byte[] value); } /** Asynchronous response to a read request */ public interface BlobListener { /** Provides the previously stored value, or null if none */ void onBlobRetrieved(@Nullable byte[] value); } /** * Installs a memory store. * * Normally this happens just once, shortly after we start. But wifi can * come up before the disk is ready, and we might not yet have a valid wall * clock when we start up, so we need to be prepared to begin recording data * even if the MemoryStore is not yet available. * * When the store is installed for the first time, we want to merge any * recently recorded data together with data already in the store. But if * the store restarts and has to be reinstalled, we don't want to do * this merge, because that would risk double-counting the old data. * */ public void installMemoryStore(@NonNull MemoryStore memoryStore) { Preconditions.checkNotNull(memoryStore); if (mMemoryStore == null) { mMemoryStore = memoryStore; Log.i(TAG, "Installing MemoryStore"); requestReadForAllChanged(); } else { mMemoryStore = memoryStore; Log.e(TAG, "Reinstalling MemoryStore"); // Our caller will call doWrites() eventually, so nothing more to do here. } } /** * Timestamp of the start of the most recent connection attempt. * * Based on mClock.getElapsedSinceBootMillis(). * * This is for calculating the time to connect and the duration of the connection. * Any negative value means we are not currently connected. */ private long mTsConnectionAttemptStart = TS_NONE; private static final long TS_NONE = -1; /** * Timestamp captured when we find out about a firmware roam */ private long mTsRoam = TS_NONE; /** * Becomes true the first time we see a poll with a valid RSSI in a connection */ private boolean mPolled = false; /** * Records validation success for the current connection. * * We want to gather statistics only on the first success. */ private boolean mValidated = false; /** * A note to ourself that we are attempting a network switch */ private boolean mAttemptingSwitch = false; /** * @param clock is the time source * @param l2KeySeed is for making our L2Keys usable only on this device */ public WifiScoreCard(Clock clock, String l2KeySeed) { mClock = clock; mL2KeySeed = l2KeySeed; mDummyPerBssid = new PerBssid("", MacAddress.fromString(DEFAULT_MAC_ADDRESS)); } /** * Gets the L2Key and GroupHint associated with the connection. */ public @NonNull Pair getL2KeyAndGroupHint(ExtendedWifiInfo wifiInfo) { PerBssid perBssid = lookupBssid(wifiInfo.getSSID(), wifiInfo.getBSSID()); if (perBssid == mDummyPerBssid) { return new Pair<>(null, null); } final long groupIdHash = computeHashLong(perBssid.ssid, mDummyPerBssid.bssid); return new Pair<>(perBssid.l2Key, groupHintFromLong(groupIdHash)); } /** * Resets the connection state */ public void resetConnectionState() { if (DBG && mTsConnectionAttemptStart > TS_NONE && !mAttemptingSwitch) { Log.v(TAG, "resetConnectionState", new Exception()); } resetConnectionStateInternal(true); } /** * @param calledFromResetConnectionState says the call is from outside the class, * indicating that we need to resepect the value of mAttemptingSwitch. */ private void resetConnectionStateInternal(boolean calledFromResetConnectionState) { if (!calledFromResetConnectionState) { mAttemptingSwitch = false; } if (!mAttemptingSwitch) { mTsConnectionAttemptStart = TS_NONE; } mTsRoam = TS_NONE; mPolled = false; mValidated = false; } /** * Updates the score card using relevant parts of WifiInfo * * @param wifiInfo object holding relevant values. */ private void update(WifiScoreCardProto.Event event, ExtendedWifiInfo wifiInfo) { PerBssid perBssid = lookupBssid(wifiInfo.getSSID(), wifiInfo.getBSSID()); perBssid.updateEventStats(event, wifiInfo.getFrequency(), wifiInfo.getRssi(), wifiInfo.getLinkSpeed()); perBssid.setNetworkConfigId(wifiInfo.getNetworkId()); if (DBG) Log.d(TAG, event.toString() + " ID: " + perBssid.id + " " + wifiInfo); } /** * Updates the score card after a signal poll * * @param wifiInfo object holding relevant values */ public void noteSignalPoll(ExtendedWifiInfo wifiInfo) { if (!mPolled && wifiInfo.getRssi() != INVALID_RSSI) { update(Event.FIRST_POLL_AFTER_CONNECTION, wifiInfo); mPolled = true; } update(Event.SIGNAL_POLL, wifiInfo); if (mTsRoam > TS_NONE && wifiInfo.getRssi() != INVALID_RSSI) { long duration = mClock.getElapsedSinceBootMillis() - mTsRoam; if (duration >= SUCCESS_MILLIS_SINCE_ROAM) { update(Event.ROAM_SUCCESS, wifiInfo); mTsRoam = TS_NONE; doWrites(); } } } /** Wait a few seconds before considering the roam successful */ private static final long SUCCESS_MILLIS_SINCE_ROAM = 4_000; /** * Updates the score card after IP configuration * * @param wifiInfo object holding relevant values */ public void noteIpConfiguration(ExtendedWifiInfo wifiInfo) { update(Event.IP_CONFIGURATION_SUCCESS, wifiInfo); mAttemptingSwitch = false; doWrites(); } /** * Updates the score card after network validation success. * * @param wifiInfo object holding relevant values */ public void noteValidationSuccess(ExtendedWifiInfo wifiInfo) { if (mValidated) return; // Only once per connection update(Event.VALIDATION_SUCCESS, wifiInfo); mValidated = true; } /** * Records the start of a connection attempt * * @param wifiInfo may have state about an existing connection */ public void noteConnectionAttempt(ExtendedWifiInfo wifiInfo) { // We may or may not be currently connected. If not, simply record the start. // But if we are connected, wrap up the old one first. if (mTsConnectionAttemptStart > TS_NONE) { if (mPolled) { update(Event.LAST_POLL_BEFORE_SWITCH, wifiInfo); } mAttemptingSwitch = true; } mTsConnectionAttemptStart = mClock.getElapsedSinceBootMillis(); mPolled = false; if (DBG) Log.d(TAG, "CONNECTION_ATTEMPT" + (mAttemptingSwitch ? " X " : " ") + wifiInfo); } /** * Records a newly assigned NetworkAgent netId. */ public void noteNetworkAgentCreated(ExtendedWifiInfo wifiInfo, int networkAgentId) { PerBssid perBssid = lookupBssid(wifiInfo.getSSID(), wifiInfo.getBSSID()); if (DBG) { Log.d(TAG, "NETWORK_AGENT_ID: " + networkAgentId + " ID: " + perBssid.id); } perBssid.mNetworkAgentId = networkAgentId; } /** * Updates the score card after a failed connection attempt * * @param wifiInfo object holding relevant values */ public void noteConnectionFailure(ExtendedWifiInfo wifiInfo, int codeMetrics, int codeMetricsProto) { if (DBG) { Log.d(TAG, "noteConnectionFailure(..., " + codeMetrics + ", " + codeMetricsProto + ")"); } // TODO(b/112196799) Need to sort out the reasons better. Also, we get here // when we disconnect from below, so it should sometimes get counted as a // disconnection rather than a connection failure. update(Event.CONNECTION_FAILURE, wifiInfo); resetConnectionStateInternal(false); } /** * Updates the score card after network reachability failure * * @param wifiInfo object holding relevant values */ public void noteIpReachabilityLost(ExtendedWifiInfo wifiInfo) { update(Event.IP_REACHABILITY_LOST, wifiInfo); if (mTsRoam > TS_NONE) { mTsConnectionAttemptStart = mTsRoam; // just to update elapsed update(Event.ROAM_FAILURE, wifiInfo); } resetConnectionStateInternal(false); doWrites(); } /** * Updates the score card before a roam * * We may have already done a firmware roam, but wifiInfo has not yet * been updated, so we still have the old state. * * @param wifiInfo object holding relevant values */ public void noteRoam(ExtendedWifiInfo wifiInfo) { update(Event.LAST_POLL_BEFORE_ROAM, wifiInfo); mTsRoam = mClock.getElapsedSinceBootMillis(); } /** * Called when the supplicant state is about to change, before wifiInfo is updated * * @param wifiInfo object holding old values * @param state the new supplicant state */ public void noteSupplicantStateChanging(ExtendedWifiInfo wifiInfo, SupplicantState state) { if (DBG) { Log.d(TAG, "Changing state to " + state + " " + wifiInfo); } } /** * Called after the supplicant state changed * * @param wifiInfo object holding old values */ public void noteSupplicantStateChanged(ExtendedWifiInfo wifiInfo) { if (DBG) { Log.d(TAG, "STATE " + wifiInfo); } } /** * Updates the score card after wifi is disabled * * @param wifiInfo object holding relevant values */ public void noteWifiDisabled(ExtendedWifiInfo wifiInfo) { update(Event.WIFI_DISABLED, wifiInfo); resetConnectionStateInternal(false); doWrites(); } final class PerBssid { public int id; public final String l2Key; public final String ssid; public final MacAddress bssid; public boolean changed; private SecurityType mSecurityType = null; private int mNetworkAgentId = Integer.MIN_VALUE; private int mNetworkConfigId = Integer.MIN_VALUE; private final Map, PerSignal> mSignalForEventAndFrequency = new ArrayMap<>(); PerBssid(String ssid, MacAddress bssid) { this.ssid = ssid; this.bssid = bssid; final long hash = computeHashLong(ssid, bssid); this.l2Key = l2KeyFromLong(hash); this.id = idFromLong(hash); this.changed = false; } void updateEventStats(Event event, int frequency, int rssi, int linkspeed) { PerSignal perSignal = lookupSignal(event, frequency); if (rssi != INVALID_RSSI) { perSignal.rssi.update(rssi); } if (linkspeed > 0) { perSignal.linkspeed.update(linkspeed); } if (perSignal.elapsedMs != null && mTsConnectionAttemptStart > TS_NONE) { long millis = mClock.getElapsedSinceBootMillis() - mTsConnectionAttemptStart; if (millis >= 0) { perSignal.elapsedMs.update(millis); } } changed = true; } PerSignal lookupSignal(Event event, int frequency) { finishPendingRead(); Pair key = new Pair<>(event, frequency); PerSignal ans = mSignalForEventAndFrequency.get(key); if (ans == null) { ans = new PerSignal(event, frequency); mSignalForEventAndFrequency.put(key, ans); } return ans; } SecurityType getSecurityType() { finishPendingRead(); return mSecurityType; } void setSecurityType(SecurityType securityType) { finishPendingRead(); if (!Objects.equals(securityType, mSecurityType)) { mSecurityType = securityType; changed = true; } } void setNetworkConfigId(int networkConfigId) { // Not serialized, so don't need to set changed, etc. if (networkConfigId >= 0) { mNetworkConfigId = networkConfigId; } } AccessPoint toAccessPoint() { return toAccessPoint(false); } AccessPoint toAccessPoint(boolean obfuscate) { finishPendingRead(); AccessPoint.Builder builder = AccessPoint.newBuilder(); builder.setId(id); if (!obfuscate) { builder.setBssid(ByteString.copyFrom(bssid.toByteArray())); } if (mSecurityType != null) { builder.setSecurityType(mSecurityType); } for (PerSignal sig: mSignalForEventAndFrequency.values()) { builder.addEventStats(sig.toSignal()); } return builder.build(); } PerBssid merge(AccessPoint ap) { if (ap.hasId() && this.id != ap.getId()) { return this; } if (ap.hasSecurityType()) { SecurityType prev = ap.getSecurityType(); if (mSecurityType == null) { mSecurityType = prev; } else if (!mSecurityType.equals(prev)) { if (DBG) { Log.i(TAG, "ID: " + id + "SecurityType changed: " + prev + " to " + mSecurityType); } changed = true; } } for (Signal signal: ap.getEventStatsList()) { Pair key = new Pair<>(signal.getEvent(), signal.getFrequency()); PerSignal perSignal = mSignalForEventAndFrequency.get(key); if (perSignal == null) { mSignalForEventAndFrequency.put(key, new PerSignal(signal)); // No need to set changed for this, since we are in sync with what's stored } else { perSignal.merge(signal); changed = true; } } return this; } String getL2Key() { return l2Key.toString(); } /** * Called when the (asynchronous) answer to a read request comes back. */ void lazyMerge(byte[] serialized) { if (serialized == null) return; byte[] old = mPendingReadFromStore.getAndSet(serialized); if (old != null) { Log.e(TAG, "More answers than we expected!"); } } /** * Handles (when convenient) the arrival of previously stored data. * * The response from IpMemoryStore arrives on a different thread, so we * defer handling it until here, when we're on our favorite thread and * in a good position to deal with it. We may have already collected some * data before now, so we need to be prepared to merge the new and old together. */ void finishPendingRead() { final byte[] serialized = mPendingReadFromStore.getAndSet(null); if (serialized == null) return; AccessPoint ap; try { ap = AccessPoint.parseFrom(serialized); } catch (InvalidProtocolBufferException e) { Log.e(TAG, "Failed to deserialize", e); return; } merge(ap); } private final AtomicReference mPendingReadFromStore = new AtomicReference<>(); } // Returned by lookupBssid when the BSSID is not available, // for instance when we are not associated. private final PerBssid mDummyPerBssid; private final Map mApForBssid = new ArrayMap<>(); // TODO should be private, but WifiCandidates needs it @NonNull PerBssid lookupBssid(String ssid, String bssid) { MacAddress mac; if (ssid == null || WifiSsid.NONE.equals(ssid) || bssid == null) { return mDummyPerBssid; } try { mac = MacAddress.fromString(bssid); } catch (IllegalArgumentException e) { return mDummyPerBssid; } PerBssid ans = mApForBssid.get(mac); if (ans == null || !ans.ssid.equals(ssid)) { ans = new PerBssid(ssid, mac); PerBssid old = mApForBssid.put(mac, ans); if (old != null) { Log.i(TAG, "Discarding stats for score card (ssid changed) ID: " + old.id); } requestReadForPerBssid(ans); } return ans; } private void requestReadForPerBssid(final PerBssid perBssid) { if (mMemoryStore != null) { mMemoryStore.read(perBssid.getL2Key(), (value) -> perBssid.lazyMerge(value)); } } private void requestReadForAllChanged() { for (PerBssid perBssid : mApForBssid.values()) { if (perBssid.changed) { requestReadForPerBssid(perBssid); } } } /** * Issues write requests for all changed entries. * * This should be called from time to time to save the state to persistent * storage. Since we always check internal state first, this does not need * to be called very often, but it should be called before shutdown. * * @returns number of writes issued. */ public int doWrites() { if (mMemoryStore == null) return 0; int count = 0; int bytes = 0; for (PerBssid perBssid : mApForBssid.values()) { if (perBssid.changed) { perBssid.finishPendingRead(); byte[] serialized = perBssid.toAccessPoint(/* No BSSID */ true).toByteArray(); mMemoryStore.write(perBssid.getL2Key(), serialized); perBssid.changed = false; count++; bytes += serialized.length; } } if (DBG && count > 0) { Log.v(TAG, "Write count: " + count + ", bytes: " + bytes); } return count; } private long computeHashLong(String ssid, MacAddress mac) { byte[][] parts = { // Our seed keeps the L2Keys specific to this device mL2KeySeed.getBytes(), // ssid is either quoted utf8 or hex-encoded bytes; turn it into plain bytes. NativeUtil.byteArrayFromArrayList(NativeUtil.decodeSsid(ssid)), // And the BSSID mac.toByteArray() }; // Assemble the parts into one, with single-byte lengths before each. int n = 0; for (int i = 0; i < parts.length; i++) { n += 1 + parts[i].length; } byte[] mashed = new byte[n]; int p = 0; for (int i = 0; i < parts.length; i++) { byte[] part = parts[i]; mashed[p++] = (byte) part.length; for (int j = 0; j < part.length; j++) { mashed[p++] = part[j]; } } // Finally, turn that into a long MessageDigest md; try { md = MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException e) { Log.e(TAG, "SHA-256 not supported."); return 0; } ByteBuffer buffer = ByteBuffer.wrap(md.digest(mashed)); return buffer.getLong(); } private static int idFromLong(long hash) { return (int) hash & 0x7fffffff; } private static String l2KeyFromLong(long hash) { return "W" + Long.toHexString(hash); } private static String groupHintFromLong(long hash) { return "G" + Long.toHexString(hash); } @VisibleForTesting PerBssid fetchByBssid(MacAddress mac) { return mApForBssid.get(mac); } @VisibleForTesting PerBssid perBssidFromAccessPoint(String ssid, AccessPoint ap) { MacAddress bssid = MacAddress.fromBytes(ap.getBssid().toByteArray()); return new PerBssid(ssid, bssid).merge(ap); } final class PerSignal { public final Event event; public final int frequency; public final PerUnivariateStatistic rssi; public final PerUnivariateStatistic linkspeed; @Nullable public final PerUnivariateStatistic elapsedMs; PerSignal(Event event, int frequency) { this.event = event; this.frequency = frequency; this.rssi = new PerUnivariateStatistic(); this.linkspeed = new PerUnivariateStatistic(); switch (event) { case FIRST_POLL_AFTER_CONNECTION: case IP_CONFIGURATION_SUCCESS: case VALIDATION_SUCCESS: case CONNECTION_FAILURE: case WIFI_DISABLED: case ROAM_FAILURE: this.elapsedMs = new PerUnivariateStatistic(); break; default: this.elapsedMs = null; break; } } PerSignal(Signal signal) { this.event = signal.getEvent(); this.frequency = signal.getFrequency(); this.rssi = new PerUnivariateStatistic(signal.getRssi()); this.linkspeed = new PerUnivariateStatistic(signal.getLinkspeed()); if (signal.hasElapsedMs()) { this.elapsedMs = new PerUnivariateStatistic(signal.getElapsedMs()); } else { this.elapsedMs = null; } } void merge(Signal signal) { Preconditions.checkArgument(event == signal.getEvent()); Preconditions.checkArgument(frequency == signal.getFrequency()); rssi.merge(signal.getRssi()); linkspeed.merge(signal.getLinkspeed()); if (signal.hasElapsedMs()) { elapsedMs.merge(signal.getElapsedMs()); } } Signal toSignal() { Signal.Builder builder = Signal.newBuilder(); builder.setEvent(event) .setFrequency(frequency) .setRssi(rssi.toUnivariateStatistic()) .setLinkspeed(linkspeed.toUnivariateStatistic()); if (elapsedMs != null) { builder.setElapsedMs(elapsedMs.toUnivariateStatistic()); } return builder.build(); } } final class PerUnivariateStatistic { public long count = 0; public double sum = 0.0; public double sumOfSquares = 0.0; public double minValue = Double.POSITIVE_INFINITY; public double maxValue = Double.NEGATIVE_INFINITY; public double historicalMean = 0.0; public double historicalVariance = Double.POSITIVE_INFINITY; PerUnivariateStatistic() {} PerUnivariateStatistic(UnivariateStatistic stats) { if (stats.hasCount()) { this.count = stats.getCount(); this.sum = stats.getSum(); this.sumOfSquares = stats.getSumOfSquares(); } if (stats.hasMinValue()) { this.minValue = stats.getMinValue(); } if (stats.hasMaxValue()) { this.maxValue = stats.getMaxValue(); } if (stats.hasHistoricalMean()) { this.historicalMean = stats.getHistoricalMean(); } if (stats.hasHistoricalVariance()) { this.historicalVariance = stats.getHistoricalVariance(); } } void update(double value) { count++; sum += value; sumOfSquares += value * value; minValue = Math.min(minValue, value); maxValue = Math.max(maxValue, value); } void age() { //TODO Fold the current stats into the historical stats } void merge(UnivariateStatistic stats) { if (stats.hasCount()) { count += stats.getCount(); sum += stats.getSum(); sumOfSquares += stats.getSumOfSquares(); } if (stats.hasMinValue()) { minValue = Math.min(minValue, stats.getMinValue()); } if (stats.hasMaxValue()) { maxValue = Math.max(maxValue, stats.getMaxValue()); } if (stats.hasHistoricalVariance()) { if (historicalVariance < Double.POSITIVE_INFINITY) { // Combine the estimates; c.f. // Maybeck, Stochasic Models, Estimation, and Control, Vol. 1 // equations (1-3) and (1-4) double numer1 = stats.getHistoricalVariance(); double numer2 = historicalVariance; double denom = numer1 + numer2; historicalMean = (numer1 * historicalMean + numer2 * stats.getHistoricalMean()) / denom; historicalVariance = numer1 * numer2 / denom; } else { historicalMean = stats.getHistoricalMean(); historicalVariance = stats.getHistoricalVariance(); } } } UnivariateStatistic toUnivariateStatistic() { UnivariateStatistic.Builder builder = UnivariateStatistic.newBuilder(); if (count != 0) { builder.setCount(count) .setSum(sum) .setSumOfSquares(sumOfSquares) .setMinValue(minValue) .setMaxValue(maxValue); } if (historicalVariance < Double.POSITIVE_INFINITY) { builder.setHistoricalMean(historicalMean) .setHistoricalVariance(historicalVariance); } return builder.build(); } } /** * Returns the current scorecard in the form of a protobuf com_android_server_wifi.NetworkList * * Synchronization is the caller's responsibility. * * @param obfuscate - if true, ssids and bssids are omitted (short id only) */ public byte[] getNetworkListByteArray(boolean obfuscate) { Map networks = new ArrayMap<>(); for (PerBssid perBssid: mApForBssid.values()) { String key = perBssid.ssid; Network.Builder network = networks.get(key); if (network == null) { network = Network.newBuilder(); networks.put(key, network); if (!obfuscate) { network.setSsid(perBssid.ssid); } if (perBssid.mSecurityType != null) { network.setSecurityType(perBssid.mSecurityType); } if (perBssid.mNetworkAgentId >= network.getNetworkAgentId()) { network.setNetworkAgentId(perBssid.mNetworkAgentId); } if (perBssid.mNetworkConfigId >= network.getNetworkConfigId()) { network.setNetworkConfigId(perBssid.mNetworkConfigId); } } network.addAccessPoints(perBssid.toAccessPoint(obfuscate)); } NetworkList.Builder builder = NetworkList.newBuilder(); for (Network.Builder network: networks.values()) { builder.addNetworks(network); } return builder.build().toByteArray(); } /** * Returns the current scorecard as a base64-encoded protobuf * * Synchronization is the caller's responsibility. * * @param obfuscate - if true, bssids are omitted (short id only) */ public String getNetworkListBase64(boolean obfuscate) { byte[] raw = getNetworkListByteArray(obfuscate); return Base64.encodeToString(raw, Base64.DEFAULT); } /** * Clears the internal state. * * This is called in response to a factoryReset call from Settings. * The memory store will be called after we are called, to wipe the stable * storage as well. Since we will have just removed all of our networks, * it is very unlikely that we're connected, or will connect immediately. * Any in-flight reads will land in the objects we are dropping here, and * the memory store should drop the in-flight writes. Ideally we would * avoid issuing reads until we were sure that the memory store had * received the factoryReset. */ public void clear() { mApForBssid.clear(); resetConnectionStateInternal(false); } }