blob: b5d8b75c1badba2e244b147c759e0926f6bad55d [file] [log] [blame]
/*
* 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 static android.net.wifi.WifiInfo.LINK_SPEED_UNKNOWN;
import static com.android.server.wifi.WifiHealthMonitor.HEALTH_MONITOR_COUNT_TX_SPEED_MIN_MBPS;
import static com.android.server.wifi.WifiHealthMonitor.HEALTH_MONITOR_MIN_TX_PACKET_PER_SEC;
import static com.android.server.wifi.WifiHealthMonitor.REASON_ASSOC_REJECTION;
import static com.android.server.wifi.WifiHealthMonitor.REASON_ASSOC_TIMEOUT;
import static com.android.server.wifi.WifiHealthMonitor.REASON_AUTH_FAILURE;
import static com.android.server.wifi.WifiHealthMonitor.REASON_CONNECTION_FAILURE;
import static com.android.server.wifi.WifiHealthMonitor.REASON_CONNECTION_FAILURE_DISCONNECTION;
import static com.android.server.wifi.WifiHealthMonitor.REASON_DISCONNECTION_NONLOCAL;
import static com.android.server.wifi.WifiHealthMonitor.REASON_NO_FAILURE;
import static com.android.server.wifi.WifiHealthMonitor.REASON_SHORT_CONNECTION_NONLOCAL;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.net.MacAddress;
import android.net.wifi.ScanResult;
import android.net.wifi.SupplicantState;
import android.net.wifi.WifiManager;
import android.util.ArrayMap;
import android.util.Base64;
import android.util.LocalLog;
import android.util.Log;
import android.util.Pair;
import android.util.SparseLongArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import com.android.server.wifi.WifiBlocklistMonitor.FailureReason;
import com.android.server.wifi.WifiHealthMonitor.FailureStats;
import com.android.server.wifi.proto.WifiScoreCardProto;
import com.android.server.wifi.proto.WifiScoreCardProto.AccessPoint;
import com.android.server.wifi.proto.WifiScoreCardProto.BandwidthStats;
import com.android.server.wifi.proto.WifiScoreCardProto.BandwidthStatsAll;
import com.android.server.wifi.proto.WifiScoreCardProto.BandwidthStatsAllLevel;
import com.android.server.wifi.proto.WifiScoreCardProto.BandwidthStatsAllLink;
import com.android.server.wifi.proto.WifiScoreCardProto.ConnectionStats;
import com.android.server.wifi.proto.WifiScoreCardProto.Event;
import com.android.server.wifi.proto.WifiScoreCardProto.HistogramBucket;
import com.android.server.wifi.proto.WifiScoreCardProto.Network;
import com.android.server.wifi.proto.WifiScoreCardProto.NetworkList;
import com.android.server.wifi.proto.WifiScoreCardProto.NetworkStats;
import com.android.server.wifi.proto.WifiScoreCardProto.SecurityType;
import com.android.server.wifi.proto.WifiScoreCardProto.Signal;
import com.android.server.wifi.proto.WifiScoreCardProto.UnivariateStatistic;
import com.android.server.wifi.proto.nano.WifiMetricsProto.BandwidthEstimatorStats;
import com.android.server.wifi.util.IntHistogram;
import com.android.server.wifi.util.LruList;
import com.android.server.wifi.util.NativeUtil;
import com.android.server.wifi.util.RssiUtil;
import com.android.wifi.resources.R;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import javax.annotation.concurrent.NotThreadSafe;
/**
* Retains statistical information about the performance of various
* access points and networks, as experienced by this device.
*
* The purpose is to better inform future network selection and switching
* by this device and help health monitor detect network issues.
*/
@NotThreadSafe
public class WifiScoreCard {
public static final String DUMP_ARG = "WifiScoreCard";
private static final String TAG = "WifiScoreCard";
private boolean mVerboseLoggingEnabled = false;
@VisibleForTesting
boolean mPersistentHistograms = true;
private static final int TARGET_IN_MEMORY_ENTRIES = 50;
private static final int UNKNOWN_REASON = -1;
public static final String PER_BSSID_DATA_NAME = "scorecard.proto";
public static final String PER_NETWORK_DATA_NAME = "perNetworkData";
static final int INSUFFICIENT_RECENT_STATS = 0;
static final int SUFFICIENT_RECENT_STATS_ONLY = 1;
static final int SUFFICIENT_RECENT_PREV_STATS = 2;
private static final int MAX_FREQUENCIES_PER_SSID = 10;
private static final int MAX_TRAFFIC_STATS_POLL_TIME_DELTA_MS = 6_000;
private final Clock mClock;
private final String mL2KeySeed;
private MemoryStore mMemoryStore;
private final DeviceConfigFacade mDeviceConfigFacade;
private final FrameworkFacade mFrameworkFacade;
private final Context mContext;
private final LocalLog mLocalLog = new LocalLog(256);
private final long[][][] mL2ErrorAccPercent =
new long[NUM_LINK_BAND][NUM_LINK_DIRECTION][NUM_SIGNAL_LEVEL];
private final long[][][] mBwEstErrorAccPercent =
new long[NUM_LINK_BAND][NUM_LINK_DIRECTION][NUM_SIGNAL_LEVEL];
private final long[][][] mBwEstValue =
new long[NUM_LINK_BAND][NUM_LINK_DIRECTION][NUM_SIGNAL_LEVEL];
private final int[][][] mBwEstCount =
new int[NUM_LINK_BAND][NUM_LINK_DIRECTION][NUM_SIGNAL_LEVEL];
@VisibleForTesting
static final int[] RSSI_BUCKETS = intsInRange(-100, -20);
private static int[] intsInRange(int min, int max) {
int[] a = new int[max - min + 1];
for (int i = 0; i < a.length; i++) {
a[i] = min + i;
}
return a;
}
/** Our view of the memory store */
public interface MemoryStore {
/** Requests a read, with asynchronous reply */
void read(String key, String name, BlobListener blobListener);
/** Requests a write, does not wait for completion */
void write(String key, String name, byte[] value);
/** Sets the cluster identifier */
void setCluster(String key, String cluster);
/** Requests removal of all entries matching the cluster */
void removeCluster(String cluster);
}
/** 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.
}
}
/**
* Enable/Disable verbose logging.
*
* @param verbose true to enable and false to disable.
*/
public void enableVerboseLogging(boolean verbose) {
mVerboseLoggingEnabled = verbose;
}
@VisibleForTesting
static final long TS_NONE = -1;
/** Tracks the connection status per Wifi interface. */
private static final class IfaceInfo {
/**
* 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.
*/
public long tsConnectionAttemptStart = TS_NONE;
/**
* Timestamp captured when we find out about a firmware roam
*/
public long tsRoam = TS_NONE;
/**
* Becomes true the first time we see a poll with a valid RSSI in a connection
*/
public boolean polled = false;
/**
* Records validation success for the current connection.
*
* We want to gather statistics only on the first success.
*/
public boolean validatedThisConnectionAtLeastOnce = false;
/**
* A note to ourself that we are attempting a network switch
*/
public boolean attemptingSwitch = false;
/**
* SSID of currently connected or connecting network. Used during disconnection
*/
public String ssidCurr = "";
/**
* SSID of previously connected network. Used during disconnection when connection attempt
* of current network is issued before the disconnection of previous network.
*/
public String ssidPrev = "";
/**
* A flag that notes that current disconnection is not generated by wpa_supplicant
* which may indicate abnormal disconnection.
*/
public boolean nonlocalDisconnection = false;
public int disconnectionReason;
public long firmwareAlertTimeMs = TS_NONE;
}
/**
* String key: iface name
* IfaceInfo value: current status of iface
*/
private final Map<String, IfaceInfo> mIfaceToInfoMap = new ArrayMap<>();
/** Gets the IfaceInfo, or create it if it doesn't exist. */
private IfaceInfo getIfaceInfo(String ifaceName) {
return mIfaceToInfoMap.computeIfAbsent(ifaceName, k -> new IfaceInfo());
}
/**
* @param clock is the time source
* @param l2KeySeed is for making our L2Keys usable only on this device
*/
public WifiScoreCard(Clock clock, String l2KeySeed, DeviceConfigFacade deviceConfigFacade,
FrameworkFacade frameworkFacade, Context context) {
mClock = clock;
mContext = context;
mL2KeySeed = l2KeySeed;
mPlaceholderPerBssid = new PerBssid("", MacAddress.fromString(DEFAULT_MAC_ADDRESS));
mPlaceholderPerNetwork = new PerNetwork("");
mDeviceConfigFacade = deviceConfigFacade;
mFrameworkFacade = frameworkFacade;
}
/**
* 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 == mPlaceholderPerBssid) {
return new Pair<>(null, null);
}
return new Pair<>(perBssid.getL2Key(), groupHintFromSsid(perBssid.ssid));
}
/**
* Computes the GroupHint associated with the given ssid.
*/
public @NonNull String groupHintFromSsid(String ssid) {
final long groupIdHash = computeHashLong(ssid, mPlaceholderPerBssid.bssid, mL2KeySeed);
return groupHintFromLong(groupIdHash);
}
/** Handle network disconnection. */
public void resetConnectionState(String ifaceName) {
IfaceInfo ifaceInfo = getIfaceInfo(ifaceName);
noteDisconnectionForIface(ifaceInfo);
resetConnectionStateForIfaceInternal(ifaceInfo, true);
}
/** Handle shutdown event. */
public void resetAllConnectionStates() {
for (IfaceInfo ifaceInfo : mIfaceToInfoMap.values()) {
noteDisconnectionForIface(ifaceInfo);
resetConnectionStateForIfaceInternal(ifaceInfo, true);
}
}
private void noteDisconnectionForIface(IfaceInfo ifaceInfo) {
String ssidDisconnected = ifaceInfo.attemptingSwitch
? ifaceInfo.ssidPrev : ifaceInfo.ssidCurr;
updatePerNetwork(Event.DISCONNECTION, ssidDisconnected, INVALID_RSSI, LINK_SPEED_UNKNOWN,
UNKNOWN_REASON, ifaceInfo);
if (mVerboseLoggingEnabled && ifaceInfo.tsConnectionAttemptStart > TS_NONE
&& !ifaceInfo.attemptingSwitch) {
Log.v(TAG, "handleNetworkDisconnect", new Exception());
}
}
private void resetAllConnectionStatesInternal() {
for (IfaceInfo ifaceInfo : mIfaceToInfoMap.values()) {
resetConnectionStateForIfaceInternal(ifaceInfo, false);
}
}
/**
* @param calledFromResetConnectionState says the call is from outside the class,
* indicating that we need to respect the value of mAttemptingSwitch.
*/
private void resetConnectionStateForIfaceInternal(IfaceInfo ifaceInfo,
boolean calledFromResetConnectionState) {
if (!calledFromResetConnectionState) {
ifaceInfo.attemptingSwitch = false;
}
if (!ifaceInfo.attemptingSwitch) {
ifaceInfo.tsConnectionAttemptStart = TS_NONE;
}
ifaceInfo.tsRoam = TS_NONE;
ifaceInfo.polled = false;
ifaceInfo.validatedThisConnectionAtLeastOnce = false;
ifaceInfo.nonlocalDisconnection = false;
ifaceInfo.firmwareAlertTimeMs = TS_NONE;
}
/**
* Updates perBssid using relevant parts of WifiInfo
*
* @param wifiInfo object holding relevant values.
*/
private void updatePerBssid(WifiScoreCardProto.Event event, ExtendedWifiInfo wifiInfo) {
PerBssid perBssid = lookupBssid(wifiInfo.getSSID(), wifiInfo.getBSSID());
perBssid.updateEventStats(event,
wifiInfo.getFrequency(),
wifiInfo.getRssi(),
wifiInfo.getLinkSpeed(),
wifiInfo.getIfaceName());
perBssid.setNetworkConfigId(wifiInfo.getNetworkId());
logd("BSSID update " + event + " ID: " + perBssid.id + " " + wifiInfo);
}
/**
* Updates perNetwork with SSID, current RSSI and failureReason. failureReason is meaningful
* only during connection failure.
*/
private void updatePerNetwork(WifiScoreCardProto.Event event, String ssid, int rssi,
int txSpeed, int failureReason, IfaceInfo ifaceInfo) {
PerNetwork perNetwork = lookupNetwork(ssid);
logd("network update " + event + ((ssid == null) ? " " : " "
+ ssid) + " ID: " + perNetwork.id + " RSSI " + rssi + " txSpeed " + txSpeed);
perNetwork.updateEventStats(event, rssi, txSpeed, failureReason, ifaceInfo);
}
/**
* Updates the score card after a signal poll
*
* @param wifiInfo object holding relevant values
*/
public void noteSignalPoll(@NonNull ExtendedWifiInfo wifiInfo) {
IfaceInfo ifaceInfo = getIfaceInfo(wifiInfo.getIfaceName());
if (!ifaceInfo.polled && wifiInfo.getRssi() != INVALID_RSSI) {
updatePerBssid(Event.FIRST_POLL_AFTER_CONNECTION, wifiInfo);
ifaceInfo.polled = true;
}
updatePerBssid(Event.SIGNAL_POLL, wifiInfo);
int validTxSpeed = geTxLinkSpeedWithSufficientTxRate(wifiInfo);
updatePerNetwork(Event.SIGNAL_POLL, wifiInfo.getSSID(), wifiInfo.getRssi(),
validTxSpeed, UNKNOWN_REASON, ifaceInfo);
if (ifaceInfo.tsRoam > TS_NONE && wifiInfo.getRssi() != INVALID_RSSI) {
long duration = mClock.getElapsedSinceBootMillis() - ifaceInfo.tsRoam;
if (duration >= SUCCESS_MILLIS_SINCE_ROAM) {
updatePerBssid(Event.ROAM_SUCCESS, wifiInfo);
ifaceInfo.tsRoam = TS_NONE;
doWritesBssid();
}
}
}
private int geTxLinkSpeedWithSufficientTxRate(@NonNull ExtendedWifiInfo wifiInfo) {
int txRate = (int) Math.ceil(wifiInfo.getSuccessfulTxPacketsPerSecond()
+ wifiInfo.getLostTxPacketsPerSecond()
+ wifiInfo.getRetriedTxPacketsPerSecond());
int txSpeed = wifiInfo.getTxLinkSpeedMbps();
logd("txRate: " + txRate + " txSpeed: " + txSpeed);
return (txRate >= HEALTH_MONITOR_MIN_TX_PACKET_PER_SEC) ? txSpeed : LINK_SPEED_UNKNOWN;
}
/** 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(@NonNull ExtendedWifiInfo wifiInfo) {
IfaceInfo ifaceInfo = getIfaceInfo(wifiInfo.getIfaceName());
updatePerBssid(Event.IP_CONFIGURATION_SUCCESS, wifiInfo);
updatePerNetwork(Event.IP_CONFIGURATION_SUCCESS, wifiInfo.getSSID(), wifiInfo.getRssi(),
wifiInfo.getTxLinkSpeedMbps(), UNKNOWN_REASON, ifaceInfo);
PerNetwork perNetwork = lookupNetwork(wifiInfo.getSSID());
perNetwork.initBandwidthFilter(wifiInfo);
ifaceInfo.attemptingSwitch = false;
doWrites();
}
/**
* Updates the score card after network validation success.
*
* @param wifiInfo object holding relevant values
*/
public void noteValidationSuccess(@NonNull ExtendedWifiInfo wifiInfo) {
IfaceInfo ifaceInfo = getIfaceInfo(wifiInfo.getIfaceName());
if (ifaceInfo.validatedThisConnectionAtLeastOnce) return; // Only once per connection
updatePerBssid(Event.VALIDATION_SUCCESS, wifiInfo);
ifaceInfo.validatedThisConnectionAtLeastOnce = true;
doWrites();
}
/**
* Updates the score card after network validation failure
*
* @param wifiInfo object holding relevant values
*/
public void noteValidationFailure(@NonNull ExtendedWifiInfo wifiInfo) {
// VALIDATION_FAILURE is not currently recorded.
}
/**
* Records the start of a connection attempt
*
* @param wifiInfo may have state about an existing connection
* @param scanRssi is the highest RSSI of recent scan found from scanDetailCache
* @param ssid is the network SSID of connection attempt
*/
public void noteConnectionAttempt(@NonNull ExtendedWifiInfo wifiInfo,
int scanRssi, String ssid) {
IfaceInfo ifaceInfo = getIfaceInfo(wifiInfo.getIfaceName());
// 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 (ifaceInfo.tsConnectionAttemptStart > TS_NONE) {
if (ifaceInfo.polled) {
updatePerBssid(Event.LAST_POLL_BEFORE_SWITCH, wifiInfo);
}
ifaceInfo.attemptingSwitch = true;
}
ifaceInfo.tsConnectionAttemptStart = mClock.getElapsedSinceBootMillis();
ifaceInfo.polled = false;
ifaceInfo.ssidPrev = ifaceInfo.ssidCurr;
ifaceInfo.ssidCurr = ssid;
ifaceInfo.firmwareAlertTimeMs = TS_NONE;
updatePerNetwork(Event.CONNECTION_ATTEMPT, ssid, scanRssi, LINK_SPEED_UNKNOWN,
UNKNOWN_REASON, ifaceInfo);
logd("CONNECTION_ATTEMPT" + (ifaceInfo.attemptingSwitch ? " X " : " ") + wifiInfo);
}
/**
* Records a newly assigned NetworkAgent netId.
*/
public void noteNetworkAgentCreated(@NonNull ExtendedWifiInfo wifiInfo, int networkAgentId) {
PerBssid perBssid = lookupBssid(wifiInfo.getSSID(), wifiInfo.getBSSID());
logd("NETWORK_AGENT_ID: " + networkAgentId + " ID: " + perBssid.id);
perBssid.mNetworkAgentId = networkAgentId;
}
/**
* Record disconnection not initiated by wpa_supplicant in connected mode
* @param reason is detailed disconnection reason code
*/
public void noteNonlocalDisconnect(String ifaceName, int reason) {
IfaceInfo ifaceInfo = getIfaceInfo(ifaceName);
ifaceInfo.nonlocalDisconnection = true;
ifaceInfo.disconnectionReason = reason;
logd("nonlocal disconnection with reason: " + reason);
}
/**
* Record firmware alert timestamp and error code
*/
public void noteFirmwareAlert(int errorCode) {
long ts = mClock.getElapsedSinceBootMillis();
// Firmware alert is device-level, not per-iface. Thus, note firmware alert on all ifaces.
for (IfaceInfo ifaceInfo : mIfaceToInfoMap.values()) {
ifaceInfo.firmwareAlertTimeMs = ts;
}
logd("firmware alert with error code: " + errorCode);
}
/**
* Updates the score card after a failed connection attempt
*
* @param wifiInfo object holding relevant values.
* @param scanRssi is the highest RSSI of recent scan found from scanDetailCache
* @param ssid is the network SSID.
* @param failureReason is connection failure reason
*/
public void noteConnectionFailure(@NonNull ExtendedWifiInfo wifiInfo,
int scanRssi, String ssid, @FailureReason int failureReason) {
IfaceInfo ifaceInfo = getIfaceInfo(wifiInfo.getIfaceName());
// TODO: add the breakdown of level2FailureReason
updatePerBssid(Event.CONNECTION_FAILURE, wifiInfo);
updatePerNetwork(Event.CONNECTION_FAILURE, ssid, scanRssi, LINK_SPEED_UNKNOWN,
failureReason, ifaceInfo);
resetConnectionStateForIfaceInternal(ifaceInfo, false);
}
/**
* Updates the score card after network reachability failure
*
* @param wifiInfo object holding relevant values
*/
public void noteIpReachabilityLost(@NonNull ExtendedWifiInfo wifiInfo) {
IfaceInfo ifaceInfo = getIfaceInfo(wifiInfo.getIfaceName());
if (ifaceInfo.tsRoam > TS_NONE) {
ifaceInfo.tsConnectionAttemptStart = ifaceInfo.tsRoam; // just to update elapsed
updatePerBssid(Event.ROAM_FAILURE, wifiInfo);
} else {
updatePerBssid(Event.IP_REACHABILITY_LOST, wifiInfo);
}
// No need to call resetConnectionStateInternal() because
// resetConnectionState() will be called after WifiNative.disconnect() in ClientModeImpl
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
*/
private void noteRoam(IfaceInfo ifaceInfo, @NonNull ExtendedWifiInfo wifiInfo) {
updatePerBssid(Event.LAST_POLL_BEFORE_ROAM, wifiInfo);
ifaceInfo.tsRoam = 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(@NonNull ExtendedWifiInfo wifiInfo,
SupplicantState state) {
IfaceInfo ifaceInfo = getIfaceInfo(wifiInfo.getIfaceName());
if (state == SupplicantState.COMPLETED && wifiInfo.getSupplicantState() == state) {
// Our signal that a firmware roam has occurred
noteRoam(ifaceInfo, wifiInfo);
}
logd("Changing state to " + state + " " + wifiInfo);
}
/**
* Called after the supplicant state changed
*
* @param wifiInfo object holding old values
*/
public void noteSupplicantStateChanged(ExtendedWifiInfo wifiInfo) {
logd("ifaceName=" + wifiInfo.getIfaceName() + ",wifiInfo=" + wifiInfo);
}
/**
* Updates the score card when wifi is disabled
*
* @param wifiInfo object holding relevant values
*/
public void noteWifiDisabled(@NonNull ExtendedWifiInfo wifiInfo) {
updatePerBssid(Event.WIFI_DISABLED, wifiInfo);
}
/**
* Records the last successful L2 connection timestamp for a BSSID.
* @return the previous BSSID connection time.
*/
public long setBssidConnectionTimestampMs(String ssid, String bssid, long timeMs) {
PerBssid perBssid = lookupBssid(ssid, bssid);
long prev = perBssid.lastConnectionTimestampMs;
perBssid.lastConnectionTimestampMs = timeMs;
return prev;
}
/**
* Returns the last successful L2 connection time for this BSSID.
*/
public long getBssidConnectionTimestampMs(String ssid, String bssid) {
return lookupBssid(ssid, bssid).lastConnectionTimestampMs;
}
/**
* Increment the blocklist streak count for a failure reason on an AP.
* @return the updated count
*/
public int incrementBssidBlocklistStreak(String ssid, String bssid,
@WifiBlocklistMonitor.FailureReason int reason) {
PerBssid perBssid = lookupBssid(ssid, bssid);
return ++perBssid.blocklistStreakCount[reason];
}
/**
* Get the blocklist streak count for a failure reason on an AP.
* @return the blocklist streak count
*/
public int getBssidBlocklistStreak(String ssid, String bssid,
@WifiBlocklistMonitor.FailureReason int reason) {
return lookupBssid(ssid, bssid).blocklistStreakCount[reason];
}
/**
* Clear the blocklist streak count for a failure reason on an AP.
*/
public void resetBssidBlocklistStreak(String ssid, String bssid,
@WifiBlocklistMonitor.FailureReason int reason) {
lookupBssid(ssid, bssid).blocklistStreakCount[reason] = 0;
}
/**
* Clear the blocklist streak count for all APs that belong to this SSID.
*/
public void resetBssidBlocklistStreakForSsid(@NonNull String ssid) {
Iterator<Map.Entry<MacAddress, PerBssid>> it = mApForBssid.entrySet().iterator();
while (it.hasNext()) {
PerBssid perBssid = it.next().getValue();
if (!ssid.equals(perBssid.ssid)) {
continue;
}
for (int i = 0; i < perBssid.blocklistStreakCount.length; i++) {
perBssid.blocklistStreakCount[i] = 0;
}
}
}
/**
* Detect abnormal disconnection at high RSSI with a high rate
*/
public int detectAbnormalDisconnection(String ifaceName) {
IfaceInfo ifaceInfo = getIfaceInfo(ifaceName);
String ssid = ifaceInfo.attemptingSwitch ? ifaceInfo.ssidPrev : ifaceInfo.ssidCurr;
PerNetwork perNetwork = lookupNetwork(ssid);
NetworkConnectionStats recentStats = perNetwork.getRecentStats();
if (recentStats.getRecentCountCode() == CNT_SHORT_CONNECTION_NONLOCAL) {
return detectAbnormalFailureReason(recentStats, CNT_SHORT_CONNECTION_NONLOCAL,
REASON_SHORT_CONNECTION_NONLOCAL,
mDeviceConfigFacade.getShortConnectionNonlocalHighThrPercent(),
mDeviceConfigFacade.getShortConnectionNonlocalCountMin(),
CNT_DISCONNECTION);
} else if (recentStats.getRecentCountCode() == CNT_DISCONNECTION_NONLOCAL) {
return detectAbnormalFailureReason(recentStats, CNT_DISCONNECTION_NONLOCAL,
REASON_DISCONNECTION_NONLOCAL,
mDeviceConfigFacade.getDisconnectionNonlocalHighThrPercent(),
mDeviceConfigFacade.getDisconnectionNonlocalCountMin(),
CNT_DISCONNECTION);
} else {
return REASON_NO_FAILURE;
}
}
/**
* Detect abnormal connection failure at high RSSI with a high rate
*/
public int detectAbnormalConnectionFailure(String ssid) {
PerNetwork perNetwork = lookupNetwork(ssid);
NetworkConnectionStats recentStats = perNetwork.getRecentStats();
int recentCountCode = recentStats.getRecentCountCode();
if (recentCountCode == CNT_AUTHENTICATION_FAILURE) {
return detectAbnormalFailureReason(recentStats, CNT_AUTHENTICATION_FAILURE,
REASON_AUTH_FAILURE,
mDeviceConfigFacade.getAuthFailureHighThrPercent(),
mDeviceConfigFacade.getAuthFailureCountMin(),
CNT_CONNECTION_ATTEMPT);
} else if (recentCountCode == CNT_ASSOCIATION_REJECTION) {
return detectAbnormalFailureReason(recentStats, CNT_ASSOCIATION_REJECTION,
REASON_ASSOC_REJECTION,
mDeviceConfigFacade.getAssocRejectionHighThrPercent(),
mDeviceConfigFacade.getAssocRejectionCountMin(),
CNT_CONNECTION_ATTEMPT);
} else if (recentCountCode == CNT_ASSOCIATION_TIMEOUT) {
return detectAbnormalFailureReason(recentStats, CNT_ASSOCIATION_TIMEOUT,
REASON_ASSOC_TIMEOUT,
mDeviceConfigFacade.getAssocTimeoutHighThrPercent(),
mDeviceConfigFacade.getAssocTimeoutCountMin(),
CNT_CONNECTION_ATTEMPT);
} else if (recentCountCode == CNT_DISCONNECTION_NONLOCAL_CONNECTING) {
return detectAbnormalFailureReason(recentStats, CNT_DISCONNECTION_NONLOCAL_CONNECTING,
REASON_CONNECTION_FAILURE_DISCONNECTION,
mDeviceConfigFacade.getConnectionFailureDisconnectionHighThrPercent(),
mDeviceConfigFacade.getConnectionFailureDisconnectionCountMin(),
CNT_CONNECTION_ATTEMPT);
} else if (recentCountCode == CNT_CONNECTION_FAILURE) {
return detectAbnormalFailureReason(recentStats, CNT_CONNECTION_FAILURE,
REASON_CONNECTION_FAILURE,
mDeviceConfigFacade.getConnectionFailureHighThrPercent(),
mDeviceConfigFacade.getConnectionFailureCountMin(),
CNT_CONNECTION_ATTEMPT);
} else {
return REASON_NO_FAILURE;
}
}
private int detectAbnormalFailureReason(NetworkConnectionStats stats, int countCode,
int reasonCode, int highThresholdPercent, int minCount, int refCountCode) {
// To detect abnormal failure which may trigger bugReport,
// increase the detection threshold by thresholdRatio
int thresholdRatio =
mDeviceConfigFacade.getBugReportThresholdExtraRatio();
if (isHighPercentageAndEnoughCount(stats, countCode, reasonCode,
highThresholdPercent * thresholdRatio,
minCount * thresholdRatio,
refCountCode)) {
return reasonCode;
} else {
return REASON_NO_FAILURE;
}
}
private boolean isHighPercentageAndEnoughCount(NetworkConnectionStats stats, int countCode,
int reasonCode, int highThresholdPercent, int minCount, int refCountCode) {
highThresholdPercent = Math.min(highThresholdPercent, 100);
// Use Laplace's rule of succession, useful especially for a small
// connection attempt count
// R = (f+1)/(n+2) with a pseudo count of 2 (one for f and one for s)
return ((stats.getCount(countCode) >= minCount)
&& ((stats.getCount(countCode) + 1) * 100)
>= (highThresholdPercent * (stats.getCount(refCountCode) + 2)));
}
final class PerBssid extends MemoryStoreAccessBase {
public int id;
public final String ssid;
public final MacAddress bssid;
public final int[] blocklistStreakCount =
new int[WifiBlocklistMonitor.NUMBER_REASON_CODES];
public long[][][] bandwidthStatsValue =
new long[NUM_LINK_BAND][NUM_LINK_DIRECTION][NUM_SIGNAL_LEVEL];
public int[][][] bandwidthStatsCount =
new int[NUM_LINK_BAND][NUM_LINK_DIRECTION][NUM_SIGNAL_LEVEL];
// The wall clock time in milliseconds for the last successful l2 connection.
public long lastConnectionTimestampMs;
public boolean changed;
public boolean referenced;
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) {
super(computeHashLong(ssid, bssid, mL2KeySeed));
this.ssid = ssid;
this.bssid = bssid;
this.id = idFromLong();
this.changed = false;
this.referenced = false;
}
void updateEventStats(Event event, int frequency, int rssi, int linkspeed,
String ifaceName) {
PerSignal perSignal = lookupSignal(event, frequency);
if (rssi != INVALID_RSSI) {
perSignal.rssi.update(rssi);
changed = true;
}
if (linkspeed > 0) {
perSignal.linkspeed.update(linkspeed);
changed = true;
}
IfaceInfo ifaceInfo = getIfaceInfo(ifaceName);
if (perSignal.elapsedMs != null && ifaceInfo.tsConnectionAttemptStart > TS_NONE) {
long millis =
mClock.getElapsedSinceBootMillis() - ifaceInfo.tsConnectionAttemptStart;
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());
}
builder.setBandwidthStatsAll(toBandwidthStatsAll(
bandwidthStatsValue, bandwidthStatsCount));
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 (mVerboseLoggingEnabled) {
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(key.first, key.second).merge(signal));
// No need to set changed for this, since we are in sync with what's stored
} else {
perSignal.merge(signal);
changed = true;
}
}
if (ap.hasBandwidthStatsAll()) {
mergeBandwidthStatsAll(ap.getBandwidthStatsAll(),
bandwidthStatsValue, bandwidthStatsCount);
}
return this;
}
/**
* 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 = finishPendingReadBytes();
if (serialized == null) return;
AccessPoint ap;
try {
ap = AccessPoint.parseFrom(serialized);
} catch (InvalidProtocolBufferException e) {
Log.e(TAG, "Failed to deserialize", e);
return;
}
merge(ap);
}
/**
* Estimates the probability of getting internet access, based on the
* device experience.
*
* @return a probability, expressed as a percentage in the range 0 to 100
*/
public int estimatePercentInternetAvailability() {
// Initialize counts accoring to Laplace's rule of succession
int trials = 2;
int successes = 1;
// Aggregate over all of the frequencies
for (PerSignal s : mSignalForEventAndFrequency.values()) {
switch (s.event) {
case IP_CONFIGURATION_SUCCESS:
if (s.elapsedMs != null) {
trials += s.elapsedMs.count;
}
break;
case VALIDATION_SUCCESS:
if (s.elapsedMs != null) {
successes += s.elapsedMs.count;
}
break;
default:
break;
}
}
// Note that because of roaming it is possible to count successes
// without corresponding trials.
return Math.min(Math.max(Math.round(successes * 100.0f / trials), 0), 100);
}
}
private BandwidthStatsAll toBandwidthStatsAll(long[][][] values, int[][][] counts) {
BandwidthStatsAll.Builder builder = BandwidthStatsAll.newBuilder();
builder.setStats2G(toBandwidthStatsAllLink(values[0], counts[0]));
builder.setStatsAbove2G(toBandwidthStatsAllLink(values[1], counts[1]));
return builder.build();
}
private BandwidthStatsAllLink toBandwidthStatsAllLink(long[][] values, int[][] counts) {
BandwidthStatsAllLink.Builder builder = BandwidthStatsAllLink.newBuilder();
builder.setTx(toBandwidthStatsAllLevel(values[LINK_TX], counts[LINK_TX]));
builder.setRx(toBandwidthStatsAllLevel(values[LINK_RX], counts[LINK_RX]));
return builder.build();
}
private BandwidthStatsAllLevel toBandwidthStatsAllLevel(long[] values, int[] counts) {
BandwidthStatsAllLevel.Builder builder = BandwidthStatsAllLevel.newBuilder();
for (int i = 0; i < NUM_SIGNAL_LEVEL; i++) {
builder.addLevel(toBandwidthStats(values[i], counts[i]));
}
return builder.build();
}
private BandwidthStats toBandwidthStats(long value, int count) {
BandwidthStats.Builder builder = BandwidthStats.newBuilder();
builder.setValue(value);
builder.setCount(count);
return builder.build();
}
private void mergeBandwidthStatsAll(BandwidthStatsAll source,
long[][][] values, int[][][] counts) {
if (source.hasStats2G()) {
mergeBandwidthStatsAllLink(source.getStats2G(), values[0], counts[0]);
}
if (source.hasStatsAbove2G()) {
mergeBandwidthStatsAllLink(source.getStatsAbove2G(), values[1], counts[1]);
}
}
private void mergeBandwidthStatsAllLink(BandwidthStatsAllLink source,
long[][] values, int[][] counts) {
if (source.hasTx()) {
mergeBandwidthStatsAllLevel(source.getTx(), values[LINK_TX], counts[LINK_TX]);
}
if (source.hasRx()) {
mergeBandwidthStatsAllLevel(source.getRx(), values[LINK_RX], counts[LINK_RX]);
}
}
private void mergeBandwidthStatsAllLevel(BandwidthStatsAllLevel source,
long[] values, int[] counts) {
int levelCnt = source.getLevelCount();
for (int i = 0; i < levelCnt; i++) {
BandwidthStats stats = source.getLevel(i);
if (stats.hasValue()) {
values[i] += stats.getValue();
}
if (stats.hasCount()) {
counts[i] += stats.getCount();
}
}
}
// TODO: b/178641307 move the following parameters to config.xml
// Array dimension : int [NUM_LINK_BAND][NUM_LINK_DIRECTION][NUM_SIGNAL_LEVEL]
static final int[][][] LINK_BANDWIDTH_INIT_KBPS =
{{{500, 2500, 10000, 12000, 12000}, {500, 2500, 10000, 30000, 30000}},
{{1500, 7500, 12000, 12000, 12000}, {1500, 7500, 30000, 60000, 60000}}};
// To be used in link bandwidth estimation, each TrafficStats poll sample needs to be above
// the following values. Defined per signal level.
// int [NUM_LINK_DIRECTION][NUM_SIGNAL_LEVEL]
// Use the low Tx threshold because xDSL UL speed could be below 1Mbps.
static final int[][] LINK_BANDWIDTH_BYTE_DELTA_THR_KBYTE =
{{200, 300, 300, 300, 300}, {200, 500, 1000, 2000, 2000}};
// To be used in the long term avg, each count needs to be above the following value
static final int BANDWIDTH_STATS_COUNT_THR = 5;
private static final int TIME_CONSTANT_SMALL_SEC = 6;
// If RSSI changes by more than the below value, update BW filter with small time constant
private static final int RSSI_DELTA_THR_DB = 8;
private static final int FILTER_SCALE = 128;
// Force weight to 0 if the elapsed time is above LARGE_TIME_DECAY_RATIO * time constant
private static final int LARGE_TIME_DECAY_RATIO = 4;
// Used to derive byte count threshold from avg BW
private static final int LOW_BW_TO_AVG_BW_RATIO_NUM = 6;
private static final int LOW_BW_TO_AVG_BW_RATIO_DEN = 8;
// For some high speed connections, heavy DL traffic could falsely trigger UL BW update due to
// TCP ACK and the low Tx byte count threshold. To work around the issue, skip Tx BW update if
// Rx Bytes / Tx Bytes > RX_OVER_TX_BYTE_RATIO_MAX (heavy DL and light UL traffic)
private static final int RX_OVER_TX_BYTE_RATIO_MAX = 5;
// radio on time below the following value is ignored.
static final int RADIO_ON_TIME_MIN_MS = 200;
static final int RADIO_ON_ELAPSED_TIME_DELTA_MAX_MS = 200;
static final int NUM_SIGNAL_LEVEL = 5;
static final int LINK_TX = 0;
static final int LINK_RX = 1;
private static final int NUM_LINK_BAND = 2;
private static final int NUM_LINK_DIRECTION = 2;
private static final long BW_UPDATE_TIME_RESET_MS = TIME_CONSTANT_SMALL_SEC * 1000 * -10;
private static final int MAX_ERROR_PERCENT = 100 * 100;
private static final int EXTRA_SAMPLE_BW_FILTERING = 2;
/**
* A class collecting the connection and link bandwidth stats of one network or SSID.
*/
final class PerNetwork extends MemoryStoreAccessBase {
public int id;
public final String ssid;
public boolean changed;
private int mLastRssiPoll = INVALID_RSSI;
private int mLastTxSpeedPoll = LINK_SPEED_UNKNOWN;
private long mLastRssiPollTimeMs = TS_NONE;
private long mConnectionSessionStartTimeMs = TS_NONE;
private NetworkConnectionStats mRecentStats;
private NetworkConnectionStats mStatsCurrBuild;
private NetworkConnectionStats mStatsPrevBuild;
private LruList<Integer> mFrequencyList;
// In memory keep frequency with timestamp last time available, the elapsed time since boot.
private SparseLongArray mFreqTimestamp;
private long mLastRxBytes;
private long mLastTxBytes;
private boolean mLastTrafficValid = true;
private String mBssid = "";
private int mSignalLevel; // initialize to zero to handle possible race condition
private int mBandIdx; // initialize to zero to handle possible race condition
private int[] mByteDeltaAccThr = new int[NUM_LINK_DIRECTION];
private int[] mFilterKbps = new int[NUM_LINK_DIRECTION];
private int[] mBandwidthSampleKbps = new int[NUM_LINK_DIRECTION];
private int[] mAvgUsedKbps = new int[NUM_LINK_DIRECTION];
private int mBandwidthUpdateRssiDbm = -1;
private int mBandwidthUpdateBandIdx = -1;
private boolean[] mBandwidthSampleValid = new boolean[NUM_LINK_DIRECTION];
private long[] mBandwidthSampleValidTimeMs = new long[]{BW_UPDATE_TIME_RESET_MS,
BW_UPDATE_TIME_RESET_MS};
long[][][] mBandwidthStatsValue =
new long[NUM_LINK_BAND][NUM_LINK_DIRECTION][NUM_SIGNAL_LEVEL];
int[][][] mBandwidthStatsCount =
new int[NUM_LINK_BAND][NUM_LINK_DIRECTION][NUM_SIGNAL_LEVEL];
PerNetwork(String ssid) {
super(computeHashLong(ssid, MacAddress.fromString(DEFAULT_MAC_ADDRESS), mL2KeySeed));
this.ssid = ssid;
this.id = idFromLong();
this.changed = false;
mRecentStats = new NetworkConnectionStats();
mStatsCurrBuild = new NetworkConnectionStats();
mStatsPrevBuild = new NetworkConnectionStats();
mFrequencyList = new LruList<>(MAX_FREQUENCIES_PER_SSID);
mFreqTimestamp = new SparseLongArray();
}
void updateEventStats(Event event, int rssi, int txSpeed, int failureReason,
IfaceInfo ifaceInfo) {
finishPendingRead();
long currTimeMs = mClock.getElapsedSinceBootMillis();
switch (event) {
case SIGNAL_POLL:
mLastRssiPoll = rssi;
mLastRssiPollTimeMs = currTimeMs;
mLastTxSpeedPoll = txSpeed;
changed = true;
break;
case CONNECTION_ATTEMPT:
logd(" scan rssi: " + rssi);
if (rssi >= mDeviceConfigFacade.getHealthMonitorMinRssiThrDbm()) {
mRecentStats.incrementCount(CNT_CONNECTION_ATTEMPT);
}
mConnectionSessionStartTimeMs = currTimeMs;
changed = true;
break;
case CONNECTION_FAILURE:
mConnectionSessionStartTimeMs = TS_NONE;
if (rssi >= mDeviceConfigFacade.getHealthMonitorMinRssiThrDbm()) {
if (failureReason != WifiBlocklistMonitor.REASON_WRONG_PASSWORD) {
mRecentStats.incrementCount(CNT_CONNECTION_FAILURE);
mRecentStats.incrementCount(CNT_CONSECUTIVE_CONNECTION_FAILURE);
}
switch (failureReason) {
case WifiBlocklistMonitor.REASON_ASSOCIATION_REJECTION:
mRecentStats.incrementCount(CNT_ASSOCIATION_REJECTION);
break;
case WifiBlocklistMonitor.REASON_ASSOCIATION_TIMEOUT:
mRecentStats.incrementCount(CNT_ASSOCIATION_TIMEOUT);
break;
case WifiBlocklistMonitor.REASON_AUTHENTICATION_FAILURE:
case WifiBlocklistMonitor.REASON_EAP_FAILURE:
mRecentStats.incrementCount(CNT_AUTHENTICATION_FAILURE);
break;
case WifiBlocklistMonitor.REASON_NONLOCAL_DISCONNECT_CONNECTING:
mRecentStats.incrementCount(CNT_DISCONNECTION_NONLOCAL_CONNECTING);
break;
case WifiBlocklistMonitor.REASON_AP_UNABLE_TO_HANDLE_NEW_STA:
case WifiBlocklistMonitor.REASON_WRONG_PASSWORD:
case WifiBlocklistMonitor.REASON_DHCP_FAILURE:
default:
break;
}
}
changed = true;
break;
case IP_CONFIGURATION_SUCCESS:
// Reset CNT_CONSECUTIVE_CONNECTION_FAILURE since L3 is also connected
mRecentStats.clearCount(CNT_CONSECUTIVE_CONNECTION_FAILURE);
changed = true;
logd(this.toString());
break;
case WIFI_DISABLED:
case DISCONNECTION:
if (mConnectionSessionStartTimeMs <= TS_NONE) {
return;
}
handleDisconnectionAfterConnection(ifaceInfo);
mConnectionSessionStartTimeMs = TS_NONE;
mLastRssiPollTimeMs = TS_NONE;
mFilterKbps[LINK_TX] = 0;
mFilterKbps[LINK_RX] = 0;
mBandwidthUpdateRssiDbm = -1;
mBandwidthUpdateBandIdx = -1;
changed = true;
break;
default:
break;
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("SSID: ").append(ssid).append("\n");
if (mLastRssiPollTimeMs != TS_NONE) {
sb.append(" LastRssiPollTime: ");
sb.append(mLastRssiPollTimeMs);
}
sb.append(" LastRssiPoll: " + mLastRssiPoll);
sb.append(" LastTxSpeedPoll: " + mLastTxSpeedPoll);
sb.append("\n");
sb.append(" StatsRecent: ").append(mRecentStats).append("\n");
sb.append(" StatsCurr: ").append(mStatsCurrBuild).append("\n");
sb.append(" StatsPrev: ").append(mStatsPrevBuild);
sb.append(" BandwidthStats:\n");
for (int i = 0; i < NUM_LINK_BAND; i++) {
for (int j = 0; j < NUM_LINK_DIRECTION; j++) {
sb.append(" avgKbps: ");
for (int k = 0; k < NUM_SIGNAL_LEVEL; k++) {
int avgKbps = mBandwidthStatsCount[i][j][k] == 0 ? 0 : (int)
(mBandwidthStatsValue[i][j][k] / mBandwidthStatsCount[i][j][k]);
sb.append(" " + avgKbps);
}
sb.append("\n count: ");
for (int k = 0; k < NUM_SIGNAL_LEVEL; k++) {
sb.append(" " + mBandwidthStatsCount[i][j][k]);
}
sb.append("\n");
}
sb.append("\n");
}
return sb.toString();
}
private void handleDisconnectionAfterConnection(IfaceInfo ifaceInfo) {
long currTimeMs = mClock.getElapsedSinceBootMillis();
int currSessionDurationMs = (int) (currTimeMs - mConnectionSessionStartTimeMs);
int currSessionDurationSec = currSessionDurationMs / 1000;
mRecentStats.accumulate(CNT_CONNECTION_DURATION_SEC, currSessionDurationSec);
long timeSinceLastRssiPollMs = currTimeMs - mLastRssiPollTimeMs;
boolean hasRecentRssiPoll = mLastRssiPollTimeMs > TS_NONE
&& timeSinceLastRssiPollMs <= mDeviceConfigFacade
.getHealthMonitorRssiPollValidTimeMs();
if (hasRecentRssiPoll) {
mRecentStats.incrementCount(CNT_DISCONNECTION);
}
int fwAlertValidTimeMs = mDeviceConfigFacade.getHealthMonitorFwAlertValidTimeMs();
long timeSinceLastFirmAlert = currTimeMs - ifaceInfo.firmwareAlertTimeMs;
boolean isInvalidFwAlertTime = ifaceInfo.firmwareAlertTimeMs == TS_NONE;
boolean disableFwAlertCheck = fwAlertValidTimeMs == -1;
boolean passFirmwareAlertCheck = disableFwAlertCheck ? true : (isInvalidFwAlertTime
? false : timeSinceLastFirmAlert < fwAlertValidTimeMs);
boolean hasHighRssiOrHighTxSpeed =
mLastRssiPoll >= mDeviceConfigFacade.getHealthMonitorMinRssiThrDbm()
|| mLastTxSpeedPoll >= HEALTH_MONITOR_COUNT_TX_SPEED_MIN_MBPS;
if (ifaceInfo.nonlocalDisconnection && hasRecentRssiPoll
&& isAbnormalDisconnectionReason(ifaceInfo.disconnectionReason)
&& passFirmwareAlertCheck
&& hasHighRssiOrHighTxSpeed) {
mRecentStats.incrementCount(CNT_DISCONNECTION_NONLOCAL);
if (currSessionDurationMs <= mDeviceConfigFacade
.getHealthMonitorShortConnectionDurationThrMs()) {
mRecentStats.incrementCount(CNT_SHORT_CONNECTION_NONLOCAL);
}
}
}
private boolean isAbnormalDisconnectionReason(int disconnectionReason) {
long mask = mDeviceConfigFacade.getAbnormalDisconnectionReasonCodeMask();
return disconnectionReason >= 0 && disconnectionReason <= 63
&& ((mask >> disconnectionReason) & 0x1) == 0x1;
}
@NonNull NetworkConnectionStats getRecentStats() {
return mRecentStats;
}
@NonNull NetworkConnectionStats getStatsCurrBuild() {
return mStatsCurrBuild;
}
@NonNull NetworkConnectionStats getStatsPrevBuild() {
return mStatsPrevBuild;
}
/**
* Retrieve the list of frequencies seen for this network, with the most recent first.
* @param ageInMills Max age to filter the channels.
* @return a list of frequencies
*/
List<Integer> getFrequencies(Long ageInMills) {
List<Integer> results = new ArrayList<>();
Long nowInMills = mClock.getElapsedSinceBootMillis();
for (Integer freq : mFrequencyList.getEntries()) {
if (nowInMills - mFreqTimestamp.get(freq, 0L) > ageInMills) {
continue;
}
results.add(freq);
}
return results;
}
/**
* Add a frequency to the list of frequencies for this network.
* Will evict the least recently added frequency if the cache is full.
*/
void addFrequency(int frequency) {
mFrequencyList.add(frequency);
mFreqTimestamp.put(frequency, mClock.getElapsedSinceBootMillis());
}
/**
* Update link bandwidth estimates based on TrafficStats byte counts and radio on time
*/
void updateLinkBandwidth(WifiLinkLayerStats oldStats, WifiLinkLayerStats newStats,
ExtendedWifiInfo wifiInfo) {
mBandwidthSampleValid[LINK_TX] = false;
mBandwidthSampleValid[LINK_RX] = false;
long txBytes = mFrameworkFacade.getTotalTxBytes() - mFrameworkFacade.getMobileTxBytes();
long rxBytes = mFrameworkFacade.getTotalRxBytes() - mFrameworkFacade.getMobileRxBytes();
// Sometimes TrafficStats byte counts return invalid values
// Ignore next two polls if it happens
boolean trafficValid = txBytes >= mLastTxBytes && rxBytes >= mLastRxBytes;
if (!mLastTrafficValid || !trafficValid) {
mLastTrafficValid = trafficValid;
logv("invalid traffic count tx " + txBytes + " last " + mLastTxBytes
+ " rx " + rxBytes + " last " + mLastRxBytes);
mLastTxBytes = txBytes;
mLastRxBytes = rxBytes;
return;
}
updateWifiInfo(wifiInfo);
updateLinkBandwidthTxRxSample(oldStats, newStats, wifiInfo, txBytes, rxBytes);
mLastTxBytes = txBytes;
mLastRxBytes = rxBytes;
updateBandwidthWithFilterApplied(LINK_TX, wifiInfo);
updateBandwidthWithFilterApplied(LINK_RX, wifiInfo);
mBandwidthUpdateRssiDbm = wifiInfo.getRssi();
mBandwidthUpdateBandIdx = mBandIdx;
}
void updateWifiInfo(ExtendedWifiInfo wifiInfo) {
int rssi = wifiInfo.getRssi();
mSignalLevel = RssiUtil.calculateSignalLevel(mContext, rssi);
mSignalLevel = Math.min(mSignalLevel, NUM_SIGNAL_LEVEL - 1);
mBandIdx = getBandIdx(wifiInfo);
mBssid = wifiInfo.getBSSID();
mByteDeltaAccThr[LINK_TX] = getByteDeltaAccThr(LINK_TX);
mByteDeltaAccThr[LINK_RX] = getByteDeltaAccThr(LINK_RX);
}
private void updateLinkBandwidthTxRxSample(WifiLinkLayerStats oldStats,
WifiLinkLayerStats newStats, ExtendedWifiInfo wifiInfo,
long txBytes, long rxBytes) {
// oldStats is reset to null after screen off or disconnection
if (oldStats == null || newStats == null) {
return;
}
int elapsedTimeMs = (int) (newStats.timeStampInMs - oldStats.timeStampInMs);
if (elapsedTimeMs > MAX_TRAFFIC_STATS_POLL_TIME_DELTA_MS) {
return;
}
int onTimeMs = getTotalRadioOnTimeMs(newStats) - getTotalRadioOnTimeMs(oldStats);
if (onTimeMs <= RADIO_ON_TIME_MIN_MS
|| onTimeMs > RADIO_ON_ELAPSED_TIME_DELTA_MAX_MS + elapsedTimeMs) {
return;
}
onTimeMs = Math.min(elapsedTimeMs, onTimeMs);
long txBytesDelta = txBytes - mLastTxBytes;
long rxBytesDelta = rxBytes - mLastRxBytes;
if (txBytesDelta * RX_OVER_TX_BYTE_RATIO_MAX >= rxBytesDelta) {
updateBandwidthSample(txBytesDelta, LINK_TX, onTimeMs,
wifiInfo.getMaxSupportedTxLinkSpeedMbps());
}
updateBandwidthSample(rxBytesDelta, LINK_RX, onTimeMs,
wifiInfo.getMaxSupportedRxLinkSpeedMbps());
if (!mBandwidthSampleValid[LINK_RX] && !mBandwidthSampleValid[LINK_TX]) {
return;
}
StringBuilder sb = new StringBuilder();
logv(sb.append(" rssi ").append(wifiInfo.getRssi())
.append(" level ").append(mSignalLevel)
.append(" bssid ").append(wifiInfo.getBSSID())
.append(" freq ").append(wifiInfo.getFrequency())
.append(" onTimeMs ").append(onTimeMs)
.append(" txKB ").append(txBytesDelta / 1024)
.append(" rxKB ").append(rxBytesDelta / 1024)
.append(" txKBThr ").append(mByteDeltaAccThr[LINK_TX] / 1024)
.append(" rxKBThr ").append(mByteDeltaAccThr[LINK_RX] / 1024)
.toString());
}
private int getTotalRadioOnTimeMs(@NonNull WifiLinkLayerStats stats) {
if (stats.radioStats != null && stats.radioStats.length > 0) {
int totalRadioOnTime = 0;
for (WifiLinkLayerStats.RadioStat stat : stats.radioStats) {
totalRadioOnTime += stat.on_time;
}
return totalRadioOnTime;
}
return stats.on_time;
}
private int getBandIdx(ExtendedWifiInfo wifiInfo) {
return ScanResult.is24GHz(wifiInfo.getFrequency()) ? 0 : 1;
}
private void updateBandwidthSample(long bytesDelta, int link, int onTimeMs,
int maxSupportedLinkSpeedMbps) {
checkAndPossiblyResetBandwidthStats(link, maxSupportedLinkSpeedMbps);
if (bytesDelta < mByteDeltaAccThr[link]) {
return;
}
long speedKbps = bytesDelta / onTimeMs * 8;
if (speedKbps > (maxSupportedLinkSpeedMbps * 1000)) {
return;
}
int linkBandwidthKbps = (int) speedKbps;
changed = true;
mBandwidthSampleValid[link] = true;
mBandwidthSampleKbps[link] = linkBandwidthKbps;
// Update SSID level stats
mBandwidthStatsValue[mBandIdx][link][mSignalLevel] += linkBandwidthKbps;
mBandwidthStatsCount[mBandIdx][link][mSignalLevel]++;
// Update BSSID level stats
PerBssid perBssid = lookupBssid(ssid, mBssid);
if (perBssid != mPlaceholderPerBssid) {
perBssid.changed = true;
perBssid.bandwidthStatsValue[mBandIdx][link][mSignalLevel] += linkBandwidthKbps;
perBssid.bandwidthStatsCount[mBandIdx][link][mSignalLevel]++;
}
}
private void checkAndPossiblyResetBandwidthStats(int link, int maxSupportedLinkSpeedMbps) {
if (getAvgUsedLinkBandwidthKbps(link) > (maxSupportedLinkSpeedMbps * 1000)) {
resetBandwidthStats(link);
}
}
private void resetBandwidthStats(int link) {
changed = true;
// Reset SSID level stats
mBandwidthStatsValue[mBandIdx][link][mSignalLevel] = 0;
mBandwidthStatsCount[mBandIdx][link][mSignalLevel] = 0;
// Reset BSSID level stats
PerBssid perBssid = lookupBssid(ssid, mBssid);
if (perBssid != mPlaceholderPerBssid) {
perBssid.changed = true;
perBssid.bandwidthStatsValue[mBandIdx][link][mSignalLevel] = 0;
perBssid.bandwidthStatsCount[mBandIdx][link][mSignalLevel] = 0;
}
}
private int getByteDeltaAccThr(int link) {
int maxTimeDeltaMs = mContext.getResources().getInteger(
R.integer.config_wifiPollRssiIntervalMilliseconds);
int lowBytes = calculateByteCountThreshold(getAvgUsedLinkBandwidthKbps(link),
maxTimeDeltaMs);
// Start with a predefined value
int deltaAccThr = LINK_BANDWIDTH_BYTE_DELTA_THR_KBYTE[link][mSignalLevel] * 1024;
if (lowBytes > 0) {
// Raise the threshold if the avg usage BW is high
deltaAccThr = Math.max(lowBytes, deltaAccThr);
deltaAccThr = Math.min(deltaAccThr, mDeviceConfigFacade
.getTrafficStatsThresholdMaxKbyte() * 1024);
}
return deltaAccThr;
}
private void initBandwidthFilter(ExtendedWifiInfo wifiInfo) {
updateWifiInfo(wifiInfo);
for (int link = 0; link < NUM_LINK_DIRECTION; link++) {
mFilterKbps[link] = getAvgLinkBandwidthKbps(link);
}
}
private void updateBandwidthWithFilterApplied(int link, ExtendedWifiInfo wifiInfo) {
int avgKbps = getAvgLinkBandwidthKbps(link);
// Feed the filter with the long term avg if there is no valid BW sample so that filter
// will gradually converge the long term avg.
int filterInKbps = mBandwidthSampleValid[link] ? mBandwidthSampleKbps[link] : avgKbps;
long currTimeMs = mClock.getElapsedSinceBootMillis();
int timeDeltaSec = (int) (currTimeMs - mBandwidthSampleValidTimeMs[link]) / 1000;
// If the operation condition changes since the last valid sample or the current sample
// has higher BW, use a faster filter. Otherwise, use a slow filter
int timeConstantSec;
if (Math.abs(mBandwidthUpdateRssiDbm - wifiInfo.getRssi()) > RSSI_DELTA_THR_DB
|| (mBandwidthSampleValid[link] && mBandwidthSampleKbps[link] > avgKbps)
|| mBandwidthUpdateBandIdx != mBandIdx) {
timeConstantSec = TIME_CONSTANT_SMALL_SEC;
} else {
timeConstantSec = mDeviceConfigFacade.getBandwidthEstimatorLargeTimeConstantSec();
}
// Update timestamp for next iteration
if (mBandwidthSampleValid[link]) {
mBandwidthSampleValidTimeMs[link] = currTimeMs;
}
if (filterInKbps == mFilterKbps[link]) {
return;
}
int alpha = timeDeltaSec > LARGE_TIME_DECAY_RATIO * timeConstantSec ? 0
: (int) (FILTER_SCALE * Math.exp(-1.0 * timeDeltaSec / timeConstantSec));
if (alpha == 0) {
mFilterKbps[link] = filterInKbps;
return;
}
long filterOutKbps = (long) mFilterKbps[link] * alpha
+ filterInKbps * FILTER_SCALE - filterInKbps * alpha;
filterOutKbps = filterOutKbps / FILTER_SCALE;
mFilterKbps[link] = (int) Math.min(filterOutKbps, Integer.MAX_VALUE);
StringBuilder sb = new StringBuilder();
logd(sb.append(link)
.append(" lastSampleWeight=").append(alpha)
.append("/").append(FILTER_SCALE)
.append(" filterInKbps=").append(filterInKbps)
.append(" avgKbps=").append(avgKbps)
.append(" filterOutKbps=").append(mFilterKbps[link])
.toString());
}
private int getAvgLinkBandwidthKbps(int link) {
mAvgUsedKbps[link] = getAvgUsedLinkBandwidthKbps(link);
if (mAvgUsedKbps[link] > 0) {
return mAvgUsedKbps[link];
}
int avgBwAdjSignalKbps = getAvgUsedBandwidthAdjacentThreeLevelKbps(link);
if (avgBwAdjSignalKbps > 0) {
return avgBwAdjSignalKbps;
}
// Fall back to a cold-start value
return LINK_BANDWIDTH_INIT_KBPS[mBandIdx][link][mSignalLevel];
}
private int getAvgUsedLinkBandwidthKbps(int link) {
// Check if current BSSID/signal level has enough count
PerBssid perBssid = lookupBssid(ssid, mBssid);
int count = perBssid.bandwidthStatsCount[mBandIdx][link][mSignalLevel];
long value = perBssid.bandwidthStatsValue[mBandIdx][link][mSignalLevel];
if (count >= BANDWIDTH_STATS_COUNT_THR) {
return (int) (value / count);
}
// Check if current SSID/band/signal level has enough count
count = mBandwidthStatsCount[mBandIdx][link][mSignalLevel];
value = mBandwidthStatsValue[mBandIdx][link][mSignalLevel];
if (count >= BANDWIDTH_STATS_COUNT_THR) {
return (int) (value / count);
}
return -1;
}
private int getAvgUsedBandwidthAdjacentThreeLevelKbps(int link) {
int count = 0;
long value = 0;
for (int i = -1; i <= 1; i++) {
int currLevel = mSignalLevel + i;
if (currLevel < 0 || currLevel >= NUM_SIGNAL_LEVEL) {
continue;
}
count += mBandwidthStatsCount[mBandIdx][link][currLevel];
value += mBandwidthStatsValue[mBandIdx][link][currLevel];
}
if (count >= BANDWIDTH_STATS_COUNT_THR) {
return (int) (value / count);
}
return -1;
}
// Calculate a byte count threshold for the given avg BW and observation window size
private int calculateByteCountThreshold(int avgBwKbps, int durationMs) {
long avgBytes = (long) avgBwKbps / 8 * durationMs;
long result = avgBytes * LOW_BW_TO_AVG_BW_RATIO_NUM / LOW_BW_TO_AVG_BW_RATIO_DEN;
return (int) Math.min(result, Integer.MAX_VALUE);
}
/**
* Get the latest TrafficStats based end-to-end Tx link bandwidth estimation in Kbps
*/
public int getTxLinkBandwidthKbps() {
return (mFilterKbps[LINK_TX] > 0) ? mFilterKbps[LINK_TX]
: getAvgLinkBandwidthKbps(LINK_TX);
}
/**
* Get the latest TrafficStats based end-to-end Rx link bandwidth estimation in Kbps
*/
public int getRxLinkBandwidthKbps() {
return (mFilterKbps[LINK_RX] > 0) ? mFilterKbps[LINK_RX]
: getAvgLinkBandwidthKbps(LINK_RX);
}
/**
* Update Bandwidth metrics with the latest reported bandwidth and L2 BW values
*/
public void updateBwMetrics(int[] reportedKbps, int[] l2Kbps) {
for (int link = 0; link < NUM_LINK_DIRECTION; link++) {
calculateError(link, reportedKbps[link], l2Kbps[link]);
}
}
private void calculateError(int link, int reportedKbps, int l2Kbps) {
if (mBandwidthStatsCount[mBandIdx][link][mSignalLevel] < (BANDWIDTH_STATS_COUNT_THR
+ EXTRA_SAMPLE_BW_FILTERING) || !mBandwidthSampleValid[link]
|| mAvgUsedKbps[link] <= 0) {
return;
}
int bwSampleKbps = mBandwidthSampleKbps[link];
int bwEstExtErrPercent = calculateErrorPercent(reportedKbps, bwSampleKbps);
int bwEstIntErrPercent = calculateErrorPercent(mFilterKbps[link], bwSampleKbps);
int l2ErrPercent = calculateErrorPercent(l2Kbps, bwSampleKbps);
mBwEstErrorAccPercent[mBandIdx][link][mSignalLevel] += Math.abs(bwEstExtErrPercent);
mL2ErrorAccPercent[mBandIdx][link][mSignalLevel] += Math.abs(l2ErrPercent);
mBwEstValue[mBandIdx][link][mSignalLevel] += bwSampleKbps;
mBwEstCount[mBandIdx][link][mSignalLevel]++;
StringBuilder sb = new StringBuilder();
logv(sb.append(link)
.append(" sampKbps ").append(bwSampleKbps)
.append(" filtKbps ").append(mFilterKbps[link])
.append(" reportedKbps ").append(reportedKbps)
.append(" avgUsedKbps ").append(mAvgUsedKbps[link])
.append(" l2Kbps ").append(l2Kbps)
.append(" intErrPercent ").append(bwEstIntErrPercent)
.append(" extErrPercent ").append(bwEstExtErrPercent)
.append(" l2ErrPercent ").append(l2ErrPercent)
.toString());
}
private int calculateErrorPercent(int inKbps, int bwSampleKbps) {
long errorPercent = 100L * (inKbps - bwSampleKbps) / bwSampleKbps;
return (int) Math.max(-MAX_ERROR_PERCENT, Math.min(errorPercent, MAX_ERROR_PERCENT));
}
/**
/* Detect a significant failure stats change with historical data
/* or high failure stats without historical data.
/* @return 0 if recentStats doesn't have sufficient data
* 1 if recentStats has sufficient data while statsPrevBuild doesn't
* 2 if recentStats and statsPrevBuild have sufficient data
*/
int dailyDetection(FailureStats statsDec, FailureStats statsInc, FailureStats statsHigh) {
finishPendingRead();
dailyDetectionDisconnectionEvent(statsDec, statsInc, statsHigh);
return dailyDetectionConnectionEvent(statsDec, statsInc, statsHigh);
}
private int dailyDetectionConnectionEvent(FailureStats statsDec, FailureStats statsInc,
FailureStats statsHigh) {
// Skip daily detection if recentStats is not sufficient
if (!isRecentConnectionStatsSufficient()) return INSUFFICIENT_RECENT_STATS;
if (mStatsPrevBuild.getCount(CNT_CONNECTION_ATTEMPT)
< mDeviceConfigFacade.getHealthMonitorMinNumConnectionAttempt()) {
// don't have enough historical data,
// so only detect high failure stats without relying on mStatsPrevBuild.
recentStatsHighDetectionConnection(statsHigh);
return SUFFICIENT_RECENT_STATS_ONLY;
} else {
// mStatsPrevBuild has enough updates,
// detect improvement or degradation
statsDeltaDetectionConnection(statsDec, statsInc);
return SUFFICIENT_RECENT_PREV_STATS;
}
}
private void dailyDetectionDisconnectionEvent(FailureStats statsDec, FailureStats statsInc,
FailureStats statsHigh) {
// Skip daily detection if recentStats is not sufficient
int minConnectAttempt = mDeviceConfigFacade.getHealthMonitorMinNumConnectionAttempt();
if (mRecentStats.getCount(CNT_CONNECTION_ATTEMPT) < minConnectAttempt) {
return;
}
if (mStatsPrevBuild.getCount(CNT_CONNECTION_ATTEMPT) < minConnectAttempt) {
recentStatsHighDetectionDisconnection(statsHigh);
} else {
statsDeltaDetectionDisconnection(statsDec, statsInc);
}
}
private void statsDeltaDetectionConnection(FailureStats statsDec,
FailureStats statsInc) {
statsDeltaDetection(statsDec, statsInc, CNT_CONNECTION_FAILURE,
REASON_CONNECTION_FAILURE,
mDeviceConfigFacade.getConnectionFailureCountMin(),
CNT_CONNECTION_ATTEMPT);
statsDeltaDetection(statsDec, statsInc, CNT_DISCONNECTION_NONLOCAL_CONNECTING,
REASON_CONNECTION_FAILURE_DISCONNECTION,
mDeviceConfigFacade.getConnectionFailureDisconnectionCountMin(),
CNT_CONNECTION_ATTEMPT);
statsDeltaDetection(statsDec, statsInc, CNT_AUTHENTICATION_FAILURE,
REASON_AUTH_FAILURE,
mDeviceConfigFacade.getAuthFailureCountMin(),
CNT_CONNECTION_ATTEMPT);
statsDeltaDetection(statsDec, statsInc, CNT_ASSOCIATION_REJECTION,
REASON_ASSOC_REJECTION,
mDeviceConfigFacade.getAssocRejectionCountMin(),
CNT_CONNECTION_ATTEMPT);
statsDeltaDetection(statsDec, statsInc, CNT_ASSOCIATION_TIMEOUT,
REASON_ASSOC_TIMEOUT,
mDeviceConfigFacade.getAssocTimeoutCountMin(),
CNT_CONNECTION_ATTEMPT);
}
private void recentStatsHighDetectionConnection(FailureStats statsHigh) {
recentStatsHighDetection(statsHigh, CNT_CONNECTION_FAILURE,
REASON_CONNECTION_FAILURE,
mDeviceConfigFacade.getConnectionFailureHighThrPercent(),
mDeviceConfigFacade.getConnectionFailureCountMin(),
CNT_CONNECTION_ATTEMPT);
recentStatsHighDetection(statsHigh, CNT_DISCONNECTION_NONLOCAL_CONNECTING,
REASON_CONNECTION_FAILURE_DISCONNECTION,
mDeviceConfigFacade.getConnectionFailureDisconnectionHighThrPercent(),
mDeviceConfigFacade.getConnectionFailureDisconnectionCountMin(),
CNT_CONNECTION_ATTEMPT);
recentStatsHighDetection(statsHigh, CNT_AUTHENTICATION_FAILURE,
REASON_AUTH_FAILURE,
mDeviceConfigFacade.getAuthFailureHighThrPercent(),
mDeviceConfigFacade.getAuthFailureCountMin(),
CNT_CONNECTION_ATTEMPT);
recentStatsHighDetection(statsHigh, CNT_ASSOCIATION_REJECTION,
REASON_ASSOC_REJECTION,
mDeviceConfigFacade.getAssocRejectionHighThrPercent(),
mDeviceConfigFacade.getAssocRejectionCountMin(),
CNT_CONNECTION_ATTEMPT);
recentStatsHighDetection(statsHigh, CNT_ASSOCIATION_TIMEOUT,
REASON_ASSOC_TIMEOUT,
mDeviceConfigFacade.getAssocTimeoutHighThrPercent(),
mDeviceConfigFacade.getAssocTimeoutCountMin(),
CNT_CONNECTION_ATTEMPT);
}
private void statsDeltaDetectionDisconnection(FailureStats statsDec,
FailureStats statsInc) {
statsDeltaDetection(statsDec, statsInc, CNT_SHORT_CONNECTION_NONLOCAL,
REASON_SHORT_CONNECTION_NONLOCAL,
mDeviceConfigFacade.getShortConnectionNonlocalCountMin(),
CNT_CONNECTION_ATTEMPT);
statsDeltaDetection(statsDec, statsInc, CNT_DISCONNECTION_NONLOCAL,
REASON_DISCONNECTION_NONLOCAL,
mDeviceConfigFacade.getDisconnectionNonlocalCountMin(),
CNT_CONNECTION_ATTEMPT);
}
private void recentStatsHighDetectionDisconnection(FailureStats statsHigh) {
recentStatsHighDetection(statsHigh, CNT_SHORT_CONNECTION_NONLOCAL,
REASON_SHORT_CONNECTION_NONLOCAL,
mDeviceConfigFacade.getShortConnectionNonlocalHighThrPercent(),
mDeviceConfigFacade.getShortConnectionNonlocalCountMin(),
CNT_DISCONNECTION);
recentStatsHighDetection(statsHigh, CNT_DISCONNECTION_NONLOCAL,
REASON_DISCONNECTION_NONLOCAL,
mDeviceConfigFacade.getDisconnectionNonlocalHighThrPercent(),
mDeviceConfigFacade.getDisconnectionNonlocalCountMin(),
CNT_DISCONNECTION);
}
private boolean statsDeltaDetection(FailureStats statsDec,
FailureStats statsInc, int countCode, int reasonCode,
int minCount, int refCountCode) {
if (isRatioAboveThreshold(mRecentStats, mStatsPrevBuild, countCode, refCountCode)
&& mRecentStats.getCount(countCode) >= minCount) {
statsInc.incrementCount(reasonCode);
return true;
}
if (isRatioAboveThreshold(mStatsPrevBuild, mRecentStats, countCode, refCountCode)
&& mStatsPrevBuild.getCount(countCode) >= minCount) {
statsDec.incrementCount(reasonCode);
return true;
}
return false;
}
private boolean recentStatsHighDetection(FailureStats statsHigh, int countCode,
int reasonCode, int highThresholdPercent, int minCount, int refCountCode) {
if (isHighPercentageAndEnoughCount(mRecentStats, countCode, reasonCode,
highThresholdPercent, minCount, refCountCode)) {
statsHigh.incrementCount(reasonCode);
return true;
}
return false;
}
private boolean isRatioAboveThreshold(NetworkConnectionStats stats1,
NetworkConnectionStats stats2,
@ConnectionCountCode int countCode, int refCountCode) {
// Also with Laplace's rule of succession discussed above
// R1 = (stats1(countCode) + 1) / (stats1(refCountCode) + 2)
// R2 = (stats2(countCode) + 1) / (stats2(refCountCode) + 2)
// Check R1 / R2 >= ratioThr
return ((stats1.getCount(countCode) + 1) * (stats2.getCount(refCountCode) + 2)
* mDeviceConfigFacade.HEALTH_MONITOR_RATIO_THR_DENOMINATOR)
>= ((stats1.getCount(refCountCode) + 2) * (stats2.getCount(countCode) + 1)
* mDeviceConfigFacade.getHealthMonitorRatioThrNumerator());
}
private boolean isRecentConnectionStatsSufficient() {
return (mRecentStats.getCount(CNT_CONNECTION_ATTEMPT)
>= mDeviceConfigFacade.getHealthMonitorMinNumConnectionAttempt());
}
// Update StatsCurrBuild with recentStats and clear recentStats
void updateAfterDailyDetection() {
// Skip update if recentStats is not sufficient since daily detection is also skipped
if (!isRecentConnectionStatsSufficient()) return;
mStatsCurrBuild.accumulateAll(mRecentStats);
mRecentStats.clear();
changed = true;
}
// Refresh StatsPrevBuild with StatsCurrBuild which is cleared afterwards
void updateAfterSwBuildChange() {
finishPendingRead();
mStatsPrevBuild.copy(mStatsCurrBuild);
mRecentStats.clear();
mStatsCurrBuild.clear();
changed = true;
}
NetworkStats toNetworkStats() {
finishPendingRead();
NetworkStats.Builder builder = NetworkStats.newBuilder();
builder.setId(id);
builder.setRecentStats(toConnectionStats(mRecentStats));
builder.setStatsCurrBuild(toConnectionStats(mStatsCurrBuild));
builder.setStatsPrevBuild(toConnectionStats(mStatsPrevBuild));
if (mFrequencyList.size() > 0) {
builder.addAllFrequencies(mFrequencyList.getEntries());
}
builder.setBandwidthStatsAll(toBandwidthStatsAll(
mBandwidthStatsValue, mBandwidthStatsCount));
return builder.build();
}
private ConnectionStats toConnectionStats(NetworkConnectionStats stats) {
ConnectionStats.Builder builder = ConnectionStats.newBuilder();
builder.setNumConnectionAttempt(stats.getCount(CNT_CONNECTION_ATTEMPT));
builder.setNumConnectionFailure(stats.getCount(CNT_CONNECTION_FAILURE));
builder.setConnectionDurationSec(stats.getCount(CNT_CONNECTION_DURATION_SEC));
builder.setNumDisconnectionNonlocal(stats.getCount(CNT_DISCONNECTION_NONLOCAL));
builder.setNumDisconnection(stats.getCount(CNT_DISCONNECTION));
builder.setNumShortConnectionNonlocal(stats.getCount(CNT_SHORT_CONNECTION_NONLOCAL));
builder.setNumAssociationRejection(stats.getCount(CNT_ASSOCIATION_REJECTION));
builder.setNumAssociationTimeout(stats.getCount(CNT_ASSOCIATION_TIMEOUT));
builder.setNumAuthenticationFailure(stats.getCount(CNT_AUTHENTICATION_FAILURE));
builder.setNumDisconnectionNonlocalConnecting(
stats.getCount(CNT_DISCONNECTION_NONLOCAL_CONNECTING));
return builder.build();
}
void finishPendingRead() {
final byte[] serialized = finishPendingReadBytes();
if (serialized == null) return;
NetworkStats ns;
try {
ns = NetworkStats.parseFrom(serialized);
} catch (InvalidProtocolBufferException e) {
Log.e(TAG, "Failed to deserialize", e);
return;
}
mergeNetworkStatsFromMemory(ns);
changed = true;
}
PerNetwork mergeNetworkStatsFromMemory(@NonNull NetworkStats ns) {
if (ns.hasId() && this.id != ns.getId()) {
return this;
}
if (ns.hasRecentStats()) {
ConnectionStats recentStats = ns.getRecentStats();
mergeConnectionStats(recentStats, mRecentStats);
}
if (ns.hasStatsCurrBuild()) {
ConnectionStats statsCurr = ns.getStatsCurrBuild();
mStatsCurrBuild.clear();
mergeConnectionStats(statsCurr, mStatsCurrBuild);
}
if (ns.hasStatsPrevBuild()) {
ConnectionStats statsPrev = ns.getStatsPrevBuild();
mStatsPrevBuild.clear();
mergeConnectionStats(statsPrev, mStatsPrevBuild);
}
if (ns.getFrequenciesList().size() > 0) {
// This merge assumes that whatever data is in memory is more recent that what's
// in store
List<Integer> mergedFrequencyList = mFrequencyList.getEntries();
mergedFrequencyList.addAll(ns.getFrequenciesList());
mFrequencyList = new LruList<>(MAX_FREQUENCIES_PER_SSID);
for (int i = mergedFrequencyList.size() - 1; i >= 0; i--) {
mFrequencyList.add(mergedFrequencyList.get(i));
}
}
if (ns.hasBandwidthStatsAll()) {
mergeBandwidthStatsAll(ns.getBandwidthStatsAll(),
mBandwidthStatsValue, mBandwidthStatsCount);
}
return this;
}
private void mergeConnectionStats(ConnectionStats source, NetworkConnectionStats target) {
if (source.hasNumConnectionAttempt()) {
target.accumulate(CNT_CONNECTION_ATTEMPT, source.getNumConnectionAttempt());
}
if (source.hasNumConnectionFailure()) {
target.accumulate(CNT_CONNECTION_FAILURE, source.getNumConnectionFailure());
}
if (source.hasConnectionDurationSec()) {
target.accumulate(CNT_CONNECTION_DURATION_SEC, source.getConnectionDurationSec());
}
if (source.hasNumDisconnectionNonlocal()) {
target.accumulate(CNT_DISCONNECTION_NONLOCAL, source.getNumDisconnectionNonlocal());
}
if (source.hasNumDisconnection()) {
target.accumulate(CNT_DISCONNECTION, source.getNumDisconnection());
}
if (source.hasNumShortConnectionNonlocal()) {
target.accumulate(CNT_SHORT_CONNECTION_NONLOCAL,
source.getNumShortConnectionNonlocal());
}
if (source.hasNumAssociationRejection()) {
target.accumulate(CNT_ASSOCIATION_REJECTION, source.getNumAssociationRejection());
}
if (source.hasNumAssociationTimeout()) {
target.accumulate(CNT_ASSOCIATION_TIMEOUT, source.getNumAssociationTimeout());
}
if (source.hasNumAuthenticationFailure()) {
target.accumulate(CNT_AUTHENTICATION_FAILURE, source.getNumAuthenticationFailure());
}
if (source.hasNumDisconnectionNonlocalConnecting()) {
target.accumulate(CNT_DISCONNECTION_NONLOCAL_CONNECTING,
source.getNumDisconnectionNonlocalConnecting());
}
}
}
// Codes for various connection related counts
public static final int CNT_INVALID = -1;
public static final int CNT_CONNECTION_ATTEMPT = 0;
public static final int CNT_CONNECTION_FAILURE = 1;
public static final int CNT_CONNECTION_DURATION_SEC = 2;
public static final int CNT_ASSOCIATION_REJECTION = 3;
public static final int CNT_ASSOCIATION_TIMEOUT = 4;
public static final int CNT_AUTHENTICATION_FAILURE = 5;
public static final int CNT_SHORT_CONNECTION_NONLOCAL = 6;
public static final int CNT_DISCONNECTION_NONLOCAL = 7;
public static final int CNT_DISCONNECTION = 8;
public static final int CNT_CONSECUTIVE_CONNECTION_FAILURE = 9;
public static final int CNT_DISCONNECTION_NONLOCAL_CONNECTING = 10;
// Constant being used to keep track of how many counter there are.
public static final int NUMBER_CONNECTION_CNT_CODE = 11;
private static final String[] CONNECTION_CNT_NAME = {
" ConnectAttempt: ",
" ConnectFailure: ",
" ConnectDurSec: ",
" AssocRej: ",
" AssocTimeout: ",
" AuthFailure: ",
" ShortDiscNonlocal: ",
" DisconnectNonlocal: ",
" Disconnect: ",
" ConsecutiveConnectFailure: ",
" ConnectFailureDiscon: "
};
@IntDef(prefix = { "CNT_" }, value = {
CNT_CONNECTION_ATTEMPT,
CNT_CONNECTION_FAILURE,
CNT_CONNECTION_DURATION_SEC,
CNT_ASSOCIATION_REJECTION,
CNT_ASSOCIATION_TIMEOUT,
CNT_AUTHENTICATION_FAILURE,
CNT_SHORT_CONNECTION_NONLOCAL,
CNT_DISCONNECTION_NONLOCAL,
CNT_DISCONNECTION,
CNT_CONSECUTIVE_CONNECTION_FAILURE,
CNT_DISCONNECTION_NONLOCAL_CONNECTING
})
@Retention(RetentionPolicy.SOURCE)
public @interface ConnectionCountCode {}
/**
* A class maintaining the connection related statistics of a Wifi network.
*/
public static class NetworkConnectionStats {
private final int[] mCount = new int[NUMBER_CONNECTION_CNT_CODE];
private int mRecentCountCode = CNT_INVALID;
/**
* Copy all values
* @param src is the source of copy
*/
public void copy(NetworkConnectionStats src) {
for (int i = 0; i < NUMBER_CONNECTION_CNT_CODE; i++) {
mCount[i] = src.getCount(i);
}
mRecentCountCode = src.mRecentCountCode;
}
/**
* Clear all counters
*/
public void clear() {
for (int i = 0; i < NUMBER_CONNECTION_CNT_CODE; i++) {
mCount[i] = 0;
}
mRecentCountCode = CNT_INVALID;
}
/**
* Get counter value
* @param countCode is the selected counter
* @return the value of selected counter
*/
public int getCount(@ConnectionCountCode int countCode) {
return mCount[countCode];
}
/**
* Clear counter value
* @param countCode is the selected counter to be cleared
*/
public void clearCount(@ConnectionCountCode int countCode) {
mCount[countCode] = 0;
}
/**
* Increment count value by 1
* @param countCode is the selected counter
*/
public void incrementCount(@ConnectionCountCode int countCode) {
mCount[countCode]++;
mRecentCountCode = countCode;
}
/**
* Got the recent incremented count code
*/
public int getRecentCountCode() {
return mRecentCountCode;
}
/**
* Decrement count value by 1
* @param countCode is the selected counter
*/
public void decrementCount(@ConnectionCountCode int countCode) {
mCount[countCode]--;
}
/**
* Add and accumulate the selected counter
* @param countCode is the selected counter
* @param cnt is the value to be added to the counter
*/
public void accumulate(@ConnectionCountCode int countCode, int cnt) {
mCount[countCode] += cnt;
}
/**
* Accumulate daily stats to historical data
* @param recentStats are the raw daily counts
*/
public void accumulateAll(NetworkConnectionStats recentStats) {
// 32-bit counter in second can support connection duration up to 68 years.
// Similarly 32-bit counter can support up to continuous connection attempt
// up to 68 years with one attempt per second.
for (int i = 0; i < NUMBER_CONNECTION_CNT_CODE; i++) {
mCount[i] += recentStats.getCount(i);
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < NUMBER_CONNECTION_CNT_CODE; i++) {
sb.append(CONNECTION_CNT_NAME[i]);
sb.append(mCount[i]);
}
return sb.toString();
}
}
/**
* A base class dealing with common operations of MemoryStore.
*/
public static class MemoryStoreAccessBase {
private final String mL2Key;
private final long mHash;
private static final String TAG = "WifiMemoryStoreAccessBase";
private final AtomicReference<byte[]> mPendingReadFromStore = new AtomicReference<>();
MemoryStoreAccessBase(long hash) {
mHash = hash;
mL2Key = l2KeyFromLong();
}
String getL2Key() {
return mL2Key;
}
private String l2KeyFromLong() {
return "W" + Long.toHexString(mHash);
}
/**
* Callback function when MemoryStore read is done
* @param serialized is the readback value
*/
void readBackListener(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.
*/
byte[] finishPendingReadBytes() {
return mPendingReadFromStore.getAndSet(null);
}
int idFromLong() {
return (int) mHash & 0x7fffffff;
}
}
private void logd(String string) {
if (mVerboseLoggingEnabled) {
Log.d(TAG, string);
}
}
private void logv(String string) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, string);
}
mLocalLog.log(string);
}
// Returned by lookupBssid when the BSSID is not available,
// for instance when we are not associated.
private final PerBssid mPlaceholderPerBssid;
private final Map<MacAddress, PerBssid> mApForBssid = new ArrayMap<>();
private int mApForBssidTargetSize = TARGET_IN_MEMORY_ENTRIES;
private int mApForBssidReferenced = 0;
// TODO should be private, but WifiCandidates needs it
@NonNull PerBssid lookupBssid(String ssid, String bssid) {
MacAddress mac;
if (ssid == null || WifiManager.UNKNOWN_SSID.equals(ssid) || bssid == null) {
return mPlaceholderPerBssid;
}
try {
mac = MacAddress.fromString(bssid);
} catch (IllegalArgumentException e) {
return mPlaceholderPerBssid;
}
if (mac.equals(mPlaceholderPerBssid.bssid)) {
return mPlaceholderPerBssid;
}
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);
if (old.referenced) mApForBssidReferenced--;
}
requestReadBssid(ans);
}
if (!ans.referenced) {
ans.referenced = true;
mApForBssidReferenced++;
clean();
}
return ans;
}
private void requestReadBssid(final PerBssid perBssid) {
if (mMemoryStore != null) {
mMemoryStore.read(perBssid.getL2Key(), PER_BSSID_DATA_NAME,
(value) -> perBssid.readBackListener(value));
}
}
private void requestReadForAllChanged() {
for (PerBssid perBssid : mApForBssid.values()) {
if (perBssid.changed) {
requestReadBssid(perBssid);
}
}
}
// Returned by lookupNetwork when the network is not available,
// for instance when we are not associated.
private final PerNetwork mPlaceholderPerNetwork;
private final Map<String, PerNetwork> mApForNetwork = new ArrayMap<>();
@NonNull PerNetwork lookupNetwork(String ssid) {
if (ssid == null || WifiManager.UNKNOWN_SSID.equals(ssid)) {
return mPlaceholderPerNetwork;
}
PerNetwork ans = mApForNetwork.get(ssid);
if (ans == null) {
ans = new PerNetwork(ssid);
mApForNetwork.put(ssid, ans);
requestReadNetwork(ans);
}
return ans;
}
/**
* Remove network from cache and memory store
* @param ssid is the network SSID
*/
public void removeNetwork(String ssid) {
if (ssid == null || WifiManager.UNKNOWN_SSID.equals(ssid)) {
return;
}
mApForNetwork.remove(ssid);
mApForBssid.entrySet().removeIf(entry -> ssid.equals(entry.getValue().ssid));
if (mMemoryStore == null) return;
mMemoryStore.removeCluster(groupHintFromSsid(ssid));
}
void requestReadNetwork(final PerNetwork perNetwork) {
if (mMemoryStore != null) {
mMemoryStore.read(perNetwork.getL2Key(), PER_NETWORK_DATA_NAME,
(value) -> perNetwork.readBackListener(value));
}
}
/**
* 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() {
return doWritesBssid() + doWritesNetwork();
}
private int doWritesBssid() {
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.setCluster(perBssid.getL2Key(), groupHintFromSsid(perBssid.ssid));
mMemoryStore.write(perBssid.getL2Key(), PER_BSSID_DATA_NAME, serialized);
perBssid.changed = false;
count++;
bytes += serialized.length;
}
}
if (mVerboseLoggingEnabled && count > 0) {
Log.v(TAG, "Write count: " + count + ", bytes: " + bytes);
}
return count;
}
private int doWritesNetwork() {
if (mMemoryStore == null) return 0;
int count = 0;
int bytes = 0;
for (PerNetwork perNetwork : mApForNetwork.values()) {
if (perNetwork.changed) {
perNetwork.finishPendingRead();
byte[] serialized = perNetwork.toNetworkStats().toByteArray();
mMemoryStore.setCluster(perNetwork.getL2Key(), groupHintFromSsid(perNetwork.ssid));
mMemoryStore.write(perNetwork.getL2Key(), PER_NETWORK_DATA_NAME, serialized);
perNetwork.changed = false;
count++;
bytes += serialized.length;
}
}
if (mVerboseLoggingEnabled && count > 0) {
Log.v(TAG, "Write count: " + count + ", bytes: " + bytes);
}
return count;
}
/**
* Evicts older entries from memory.
*
* This uses an approximate least-recently-used method. When the number of
* referenced entries exceeds the target value, any items that have not been
* referenced since the last round are evicted, and the remaining entries
* are marked as unreferenced. The total count varies between the target
* value and twice the target value.
*/
private void clean() {
if (mMemoryStore == null) return;
if (mApForBssidReferenced >= mApForBssidTargetSize) {
doWritesBssid(); // Do not want to evict changed items
// Evict the unreferenced ones, and clear all the referenced bits for the next round.
Iterator<Map.Entry<MacAddress, PerBssid>> it = mApForBssid.entrySet().iterator();
while (it.hasNext()) {
PerBssid perBssid = it.next().getValue();
if (perBssid.referenced) {
perBssid.referenced = false;
} else {
it.remove();
if (mVerboseLoggingEnabled) Log.v(TAG, "Evict " + perBssid.id);
}
}
mApForBssidReferenced = 0;
}
}
/**
* Compute a hash value with the given SSID and MAC address
* @param ssid is the network SSID
* @param mac is the network MAC address
* @param l2KeySeed is the seed for hash generation
* @return
*/
public static long computeHashLong(String ssid, MacAddress mac, String l2KeySeed) {
final ArrayList<Byte> decodedSsid;
try {
decodedSsid = NativeUtil.decodeSsid(ssid);
} catch (IllegalArgumentException e) {
Log.e(TAG, "NativeUtil.decodeSsid failed: malformed string: " + ssid);
return 0;
}
byte[][] parts = {
// Our seed keeps the L2Keys specific to this device
l2KeySeed.getBytes(),
// ssid is either quoted utf8 or hex-encoded bytes; turn it into plain bytes.
NativeUtil.byteArrayFromArrayList(decodedSsid),
// 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 String groupHintFromLong(long hash) {
return "G" + Long.toHexString(hash);
}
@VisibleForTesting
PerBssid fetchByBssid(MacAddress mac) {
return mApForBssid.get(mac);
}
@VisibleForTesting
PerNetwork fetchByNetwork(String ssid) {
return mApForNetwork.get(ssid);
}
@VisibleForTesting
PerBssid perBssidFromAccessPoint(String ssid, AccessPoint ap) {
MacAddress bssid = MacAddress.fromBytes(ap.getBssid().toByteArray());
return new PerBssid(ssid, bssid).merge(ap);
}
@VisibleForTesting
PerNetwork perNetworkFromNetworkStats(String ssid, NetworkStats ns) {
return new PerNetwork(ssid).mergeNetworkStatsFromMemory(ns);
}
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;
switch (event) {
case SIGNAL_POLL:
case IP_CONFIGURATION_SUCCESS:
case IP_REACHABILITY_LOST:
this.rssi = new PerUnivariateStatistic(RSSI_BUCKETS);
break;
default:
this.rssi = new PerUnivariateStatistic();
break;
}
this.linkspeed = new PerUnivariateStatistic();
switch (event) {
case FIRST_POLL_AFTER_CONNECTION:
case IP_CONFIGURATION_SUCCESS:
case VALIDATION_SUCCESS:
case CONNECTION_FAILURE:
case DISCONNECTION:
case WIFI_DISABLED:
case ROAM_FAILURE:
this.elapsedMs = new PerUnivariateStatistic();
break;
default:
this.elapsedMs = null;
break;
}
}
PerSignal merge(Signal signal) {
Preconditions.checkArgument(event == signal.getEvent());
Preconditions.checkArgument(frequency == signal.getFrequency());
rssi.merge(signal.getRssi());
linkspeed.merge(signal.getLinkspeed());
if (elapsedMs != null && signal.hasElapsedMs()) {
elapsedMs.merge(signal.getElapsedMs());
}
return this;
}
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());
}
if (rssi.intHistogram != null
&& rssi.intHistogram.numNonEmptyBuckets() > 0) {
logd("Histogram " + event + " RSSI" + rssi.intHistogram);
}
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;
public IntHistogram intHistogram = null;
PerUnivariateStatistic() {}
PerUnivariateStatistic(int[] bucketBoundaries) {
intHistogram = new IntHistogram(bucketBoundaries);
}
void update(double value) {
count++;
sum += value;
sumOfSquares += value * value;
minValue = Math.min(minValue, value);
maxValue = Math.max(maxValue, value);
if (intHistogram != null) {
intHistogram.add(Math.round((float) value), 1);
}
}
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();
}
}
if (intHistogram != null) {
for (HistogramBucket bucket : stats.getBucketsList()) {
long low = bucket.getLow();
long count = bucket.getNumber();
if (low != (int) low || count != (int) count || count < 0) {
Log.e(TAG, "Found corrupted histogram! Clearing.");
intHistogram.clear();
break;
}
intHistogram.add((int) low, (int) count);
}
}
}
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);
}
if (mPersistentHistograms
&& intHistogram != null && intHistogram.numNonEmptyBuckets() > 0) {
for (IntHistogram.Bucket b : intHistogram) {
if (b.count == 0) continue;
builder.addBuckets(
HistogramBucket.newBuilder().setLow(b.start).setNumber(b.count));
}
}
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) {
// These are really grouped by ssid, ignoring the security type.
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.mNetworkAgentId >= network.getNetworkAgentId()) {
network.setNetworkAgentId(perBssid.mNetworkAgentId);
}
if (perBssid.mNetworkConfigId >= network.getNetworkConfigId()) {
network.setNetworkConfigId(perBssid.mNetworkConfigId);
}
network.addAccessPoints(perBssid.toAccessPoint(obfuscate));
}
for (PerNetwork perNetwork: mApForNetwork.values()) {
String key = perNetwork.ssid;
Network.Builder network = networks.get(key);
if (network != null) {
network.setNetworkStats(perNetwork.toNetworkStats());
}
}
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();
mApForNetwork.clear();
resetAllConnectionStatesInternal();
}
/**
* build bandwidth estimator stats proto and then clear all related counters
*/
public BandwidthEstimatorStats dumpBandwidthEstimatorStats() {
BandwidthEstimatorStats stats = new BandwidthEstimatorStats();
stats.stats2G = dumpBandwdithStatsPerBand(0);
stats.statsAbove2G = dumpBandwdithStatsPerBand(1);
return stats;
}
private BandwidthEstimatorStats.PerBand dumpBandwdithStatsPerBand(int bandIdx) {
BandwidthEstimatorStats.PerBand stats = new BandwidthEstimatorStats.PerBand();
stats.tx = dumpBandwidthStatsPerLink(bandIdx, LINK_TX);
stats.rx = dumpBandwidthStatsPerLink(bandIdx, LINK_RX);
return stats;
}
private BandwidthEstimatorStats.PerLink dumpBandwidthStatsPerLink(
int bandIdx, int linkIdx) {
BandwidthEstimatorStats.PerLink stats = new BandwidthEstimatorStats.PerLink();
List<BandwidthEstimatorStats.PerLevel> levels = new ArrayList<>();
for (int level = 0; level < NUM_SIGNAL_LEVEL; level++) {
BandwidthEstimatorStats.PerLevel currStats =
dumpBandwidthStatsPerLevel(bandIdx, linkIdx, level);
if (currStats != null) {
levels.add(currStats);
}
}
stats.level = levels.toArray(new BandwidthEstimatorStats.PerLevel[0]);
return stats;
}
private BandwidthEstimatorStats.PerLevel dumpBandwidthStatsPerLevel(
int bandIdx, int linkIdx, int level) {
int count = mBwEstCount[bandIdx][linkIdx][level];
if (count <= 0) {
return null;
}
BandwidthEstimatorStats.PerLevel stats = new BandwidthEstimatorStats.PerLevel();
stats.signalLevel = level;
stats.count = count;
stats.avgBandwidthKbps = calculateAvg(mBwEstValue[bandIdx][linkIdx][level], count);
stats.l2ErrorPercent = calculateAvg(
mL2ErrorAccPercent[bandIdx][linkIdx][level], count);
stats.bandwidthEstErrorPercent = calculateAvg(
mBwEstErrorAccPercent[bandIdx][linkIdx][level], count);
// reset counters for next run
mBwEstCount[bandIdx][linkIdx][level] = 0;
mBwEstValue[bandIdx][linkIdx][level] = 0;
mL2ErrorAccPercent[bandIdx][linkIdx][level] = 0;
mBwEstErrorAccPercent[bandIdx][linkIdx][level] = 0;
return stats;
}
private int calculateAvg(long acc, int count) {
return (count > 0) ? (int) (acc / count) : 0;
}
/**
* Dump the internal state and local logs
*/
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("Dump of WifiScoreCard");
pw.println("current SSID(s):" + mIfaceToInfoMap.entrySet().stream()
.map(entry ->
"{iface=" + entry.getKey() + ",ssid=" + entry.getValue().ssidCurr + "}")
.collect(Collectors.joining(",")));
try {
mLocalLog.dump(fd, pw, args);
} catch (Exception e) {
e.printStackTrace();
}
pw.println(" BW Estimation Stats");
for (int i = 0; i < 2; i++) {
pw.println((i == 0 ? "2G" : "5G"));
for (int j = 0; j < NUM_LINK_DIRECTION; j++) {
pw.println((j == 0 ? " Tx" : " Rx"));
pw.println(" Count");
printValues(mBwEstCount[i][j], pw);
pw.println(" AvgKbps");
printAvgStats(mBwEstValue[i][j], mBwEstCount[i][j], pw);
pw.println(" BwEst error");
printAvgStats(mBwEstErrorAccPercent[i][j], mBwEstCount[i][j], pw);
pw.println(" L2 error");
printAvgStats(mL2ErrorAccPercent[i][j], mBwEstCount[i][j], pw);
}
}
pw.println();
}
private void printValues(int[] values, PrintWriter pw) {
StringBuilder sb = new StringBuilder();
for (int k = 0; k < NUM_SIGNAL_LEVEL; k++) {
sb.append(" " + values[k]);
}
pw.println(sb.toString());
}
private void printAvgStats(long[] stats, int[] count, PrintWriter pw) {
StringBuilder sb = new StringBuilder();
for (int k = 0; k < NUM_SIGNAL_LEVEL; k++) {
sb.append(" " + calculateAvg(stats[k], count[k]));
}
pw.println(sb.toString());
}
}