content: /*
   * 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<String, String> 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<Pair<Event, Integer>, 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<Event, Integer> 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<Event, Integer> 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<byte[]> 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<MacAddress, PerBssid> 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<String, Network.Builder> 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);
      }
  
  }
  