/*
|
* 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);
|
}
|
|
}
|