1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.wifi;
18 
19 import static android.net.wifi.WifiInfo.DEFAULT_MAC_ADDRESS;
20 import static android.net.wifi.WifiInfo.INVALID_RSSI;
21 import static android.net.wifi.WifiInfo.LINK_SPEED_UNKNOWN;
22 
23 import static com.android.server.wifi.WifiHealthMonitor.HEALTH_MONITOR_COUNT_TX_SPEED_MIN_MBPS;
24 import static com.android.server.wifi.WifiHealthMonitor.HEALTH_MONITOR_MIN_TX_PACKET_PER_SEC;
25 import static com.android.server.wifi.WifiHealthMonitor.REASON_ASSOC_REJECTION;
26 import static com.android.server.wifi.WifiHealthMonitor.REASON_ASSOC_TIMEOUT;
27 import static com.android.server.wifi.WifiHealthMonitor.REASON_AUTH_FAILURE;
28 import static com.android.server.wifi.WifiHealthMonitor.REASON_CONNECTION_FAILURE;
29 import static com.android.server.wifi.WifiHealthMonitor.REASON_DISCONNECTION_NONLOCAL;
30 import static com.android.server.wifi.WifiHealthMonitor.REASON_NO_FAILURE;
31 import static com.android.server.wifi.WifiHealthMonitor.REASON_SHORT_CONNECTION_NONLOCAL;
32 
33 import android.annotation.IntDef;
34 import android.annotation.NonNull;
35 import android.annotation.Nullable;
36 import android.net.MacAddress;
37 import android.net.wifi.SupplicantState;
38 import android.net.wifi.WifiManager;
39 import android.util.ArrayMap;
40 import android.util.Base64;
41 import android.util.Log;
42 import android.util.Pair;
43 import android.util.SparseLongArray;
44 
45 import com.android.internal.annotations.VisibleForTesting;
46 import com.android.internal.util.Preconditions;
47 import com.android.server.wifi.BssidBlocklistMonitor.FailureReason;
48 import com.android.server.wifi.WifiHealthMonitor.FailureStats;
49 import com.android.server.wifi.proto.WifiScoreCardProto;
50 import com.android.server.wifi.proto.WifiScoreCardProto.AccessPoint;
51 import com.android.server.wifi.proto.WifiScoreCardProto.ConnectionStats;
52 import com.android.server.wifi.proto.WifiScoreCardProto.Event;
53 import com.android.server.wifi.proto.WifiScoreCardProto.HistogramBucket;
54 import com.android.server.wifi.proto.WifiScoreCardProto.Network;
55 import com.android.server.wifi.proto.WifiScoreCardProto.NetworkList;
56 import com.android.server.wifi.proto.WifiScoreCardProto.NetworkStats;
57 import com.android.server.wifi.proto.WifiScoreCardProto.SecurityType;
58 import com.android.server.wifi.proto.WifiScoreCardProto.Signal;
59 import com.android.server.wifi.proto.WifiScoreCardProto.UnivariateStatistic;
60 import com.android.server.wifi.util.IntHistogram;
61 import com.android.server.wifi.util.LruList;
62 import com.android.server.wifi.util.NativeUtil;
63 
64 import com.google.protobuf.ByteString;
65 import com.google.protobuf.InvalidProtocolBufferException;
66 
67 import java.lang.annotation.Retention;
68 import java.lang.annotation.RetentionPolicy;
69 import java.nio.ByteBuffer;
70 import java.security.MessageDigest;
71 import java.security.NoSuchAlgorithmException;
72 import java.util.ArrayList;
73 import java.util.Iterator;
74 import java.util.List;
75 import java.util.Map;
76 import java.util.Objects;
77 import java.util.concurrent.atomic.AtomicReference;
78 
79 import javax.annotation.concurrent.NotThreadSafe;
80 
81 /**
82  * Retains statistical information about the performance of various
83  * access points and networks, as experienced by this device.
84  *
85  * The purpose is to better inform future network selection and switching
86  * by this device and help health monitor detect network issues.
87  */
88 @NotThreadSafe
89 public class WifiScoreCard {
90 
91     public static final String DUMP_ARG = "WifiScoreCard";
92 
93     private static final String TAG = "WifiScoreCard";
94     private boolean mVerboseLoggingEnabled = false;
95 
96     @VisibleForTesting
97     boolean mPersistentHistograms = true;
98 
99     private static final int TARGET_IN_MEMORY_ENTRIES = 50;
100     private static final int UNKNOWN_REASON = -1;
101 
102     public static final String PER_BSSID_DATA_NAME = "scorecard.proto";
103     public static final String PER_NETWORK_DATA_NAME = "perNetworkData";
104 
105     static final int INSUFFICIENT_RECENT_STATS = 0;
106     static final int SUFFICIENT_RECENT_STATS_ONLY = 1;
107     static final int SUFFICIENT_RECENT_PREV_STATS = 2;
108 
109     private static final int MAX_FREQUENCIES_PER_SSID = 10;
110 
111     private final Clock mClock;
112     private final String mL2KeySeed;
113     private MemoryStore mMemoryStore;
114     private final DeviceConfigFacade mDeviceConfigFacade;
115 
116     @VisibleForTesting
117     static final int[] RSSI_BUCKETS = intsInRange(-100, -20);
118 
intsInRange(int min, int max)119     private static int[] intsInRange(int min, int max) {
120         int[] a = new int[max - min + 1];
121         for (int i = 0; i < a.length; i++) {
122             a[i] = min + i;
123         }
124         return a;
125     }
126 
127     /** Our view of the memory store */
128     public interface MemoryStore {
129         /** Requests a read, with asynchronous reply */
read(String key, String name, BlobListener blobListener)130         void read(String key, String name, BlobListener blobListener);
131         /** Requests a write, does not wait for completion */
write(String key, String name, byte[] value)132         void write(String key, String name, byte[] value);
133         /** Sets the cluster identifier */
setCluster(String key, String cluster)134         void setCluster(String key, String cluster);
135         /** Requests removal of all entries matching the cluster */
removeCluster(String cluster)136         void removeCluster(String cluster);
137     }
138     /** Asynchronous response to a read request */
139     public interface BlobListener {
140         /** Provides the previously stored value, or null if none */
onBlobRetrieved(@ullable byte[] value)141         void onBlobRetrieved(@Nullable byte[] value);
142     }
143 
144     /**
145      * Installs a memory store.
146      *
147      * Normally this happens just once, shortly after we start. But wifi can
148      * come up before the disk is ready, and we might not yet have a valid wall
149      * clock when we start up, so we need to be prepared to begin recording data
150      * even if the MemoryStore is not yet available.
151      *
152      * When the store is installed for the first time, we want to merge any
153      * recently recorded data together with data already in the store. But if
154      * the store restarts and has to be reinstalled, we don't want to do
155      * this merge, because that would risk double-counting the old data.
156      *
157      */
installMemoryStore(@onNull MemoryStore memoryStore)158     public void installMemoryStore(@NonNull MemoryStore memoryStore) {
159         Preconditions.checkNotNull(memoryStore);
160         if (mMemoryStore == null) {
161             mMemoryStore = memoryStore;
162             Log.i(TAG, "Installing MemoryStore");
163             requestReadForAllChanged();
164         } else {
165             mMemoryStore = memoryStore;
166             Log.e(TAG, "Reinstalling MemoryStore");
167             // Our caller will call doWrites() eventually, so nothing more to do here.
168         }
169     }
170 
171     /**
172      * Enable/Disable verbose logging.
173      *
174      * @param verbose true to enable and false to disable.
175      */
enableVerboseLogging(boolean verbose)176     public void enableVerboseLogging(boolean verbose) {
177         mVerboseLoggingEnabled = verbose;
178     }
179 
180     /**
181      * Timestamp of the start of the most recent connection attempt.
182      *
183      * Based on mClock.getElapsedSinceBootMillis().
184      *
185      * This is for calculating the time to connect and the duration of the connection.
186      * Any negative value means we are not currently connected.
187      */
188     private long mTsConnectionAttemptStart = TS_NONE;
189     @VisibleForTesting
190     static final long TS_NONE = -1;
191 
192     /**
193      * Timestamp captured when we find out about a firmware roam
194      */
195     private long mTsRoam = TS_NONE;
196 
197     /**
198      * Becomes true the first time we see a poll with a valid RSSI in a connection
199      */
200     private boolean mPolled = false;
201 
202     /**
203      * Records validation success for the current connection.
204      *
205      * We want to gather statistics only on the first success.
206      */
207     private boolean mValidatedThisConnectionAtLeastOnce = false;
208 
209     /**
210      * A note to ourself that we are attempting a network switch
211      */
212     private boolean mAttemptingSwitch = false;
213 
214     /**
215      *  SSID of currently connected or connecting network. Used during disconnection
216      */
217     private String mSsidCurr = "";
218     /**
219      *  SSID of previously connected network. Used during disconnection when connection attempt
220      *  of current network is issued before the disconnection of previous network.
221      */
222     private String mSsidPrev = "";
223     /**
224      * A flag that notes that current disconnection is not generated by wpa_supplicant
225      * which may indicate abnormal disconnection.
226      */
227     private boolean mNonlocalDisconnection = false;
228     private int mDisconnectionReason;
229 
230     private long mFirmwareAlertTimeMs = TS_NONE;
231 
232     /**
233      * @param clock is the time source
234      * @param l2KeySeed is for making our L2Keys usable only on this device
235      */
WifiScoreCard(Clock clock, String l2KeySeed, DeviceConfigFacade deviceConfigFacade)236     public WifiScoreCard(Clock clock, String l2KeySeed, DeviceConfigFacade deviceConfigFacade) {
237         mClock = clock;
238         mL2KeySeed = l2KeySeed;
239         mDummyPerBssid = new PerBssid("", MacAddress.fromString(DEFAULT_MAC_ADDRESS));
240         mDummyPerNetwork = new PerNetwork("");
241         mDeviceConfigFacade = deviceConfigFacade;
242     }
243 
244     /**
245      * Gets the L2Key and GroupHint associated with the connection.
246      */
getL2KeyAndGroupHint(ExtendedWifiInfo wifiInfo)247     public @NonNull Pair<String, String> getL2KeyAndGroupHint(ExtendedWifiInfo wifiInfo) {
248         PerBssid perBssid = lookupBssid(wifiInfo.getSSID(), wifiInfo.getBSSID());
249         if (perBssid == mDummyPerBssid) {
250             return new Pair<>(null, null);
251         }
252         return new Pair<>(perBssid.getL2Key(), groupHintFromSsid(perBssid.ssid));
253     }
254 
255     /**
256      * Computes the GroupHint associated with the given ssid.
257      */
groupHintFromSsid(String ssid)258     public @NonNull String groupHintFromSsid(String ssid) {
259         final long groupIdHash = computeHashLong(ssid, mDummyPerBssid.bssid, mL2KeySeed);
260         return groupHintFromLong(groupIdHash);
261     }
262 
263     /**
264      * Handle network disconnection or shutdown event
265      */
resetConnectionState()266     public void resetConnectionState() {
267         String ssidDisconnected = mAttemptingSwitch ? mSsidPrev : mSsidCurr;
268         updatePerNetwork(Event.DISCONNECTION, ssidDisconnected, INVALID_RSSI, LINK_SPEED_UNKNOWN,
269                 UNKNOWN_REASON);
270         if (mVerboseLoggingEnabled && mTsConnectionAttemptStart > TS_NONE && !mAttemptingSwitch) {
271             Log.v(TAG, "handleNetworkDisconnect", new Exception());
272         }
273         resetConnectionStateInternal(true);
274     }
275 
276     /**
277      * @param calledFromResetConnectionState says the call is from outside the class,
278      *        indicating that we need to respect the value of mAttemptingSwitch.
279      */
resetConnectionStateInternal(boolean calledFromResetConnectionState)280     private void resetConnectionStateInternal(boolean calledFromResetConnectionState) {
281         if (!calledFromResetConnectionState) {
282             mAttemptingSwitch = false;
283         }
284         if (!mAttemptingSwitch) {
285             mTsConnectionAttemptStart = TS_NONE;
286         }
287         mTsRoam = TS_NONE;
288         mPolled = false;
289         mValidatedThisConnectionAtLeastOnce = false;
290         mNonlocalDisconnection = false;
291         mFirmwareAlertTimeMs = TS_NONE;
292     }
293 
294     /**
295      * Updates perBssid using relevant parts of WifiInfo
296      *
297      * @param wifiInfo object holding relevant values.
298      */
updatePerBssid(WifiScoreCardProto.Event event, ExtendedWifiInfo wifiInfo)299     private void updatePerBssid(WifiScoreCardProto.Event event, ExtendedWifiInfo wifiInfo) {
300         PerBssid perBssid = lookupBssid(wifiInfo.getSSID(), wifiInfo.getBSSID());
301         perBssid.updateEventStats(event,
302                 wifiInfo.getFrequency(),
303                 wifiInfo.getRssi(),
304                 wifiInfo.getLinkSpeed());
305         perBssid.setNetworkConfigId(wifiInfo.getNetworkId());
306         logd("BSSID update " + event + " ID: " + perBssid.id + " " + wifiInfo);
307     }
308 
309     /**
310      * Updates perNetwork with SSID, current RSSI and failureReason. failureReason is  meaningful
311      * only during connection failure.
312      */
updatePerNetwork(WifiScoreCardProto.Event event, String ssid, int rssi, int txSpeed, int failureReason)313     private void updatePerNetwork(WifiScoreCardProto.Event event, String ssid, int rssi,
314             int txSpeed, int failureReason) {
315         PerNetwork perNetwork = lookupNetwork(ssid);
316         logd("network update " + event + ((ssid == null) ? " " : " "
317                     + ssid) + " ID: " + perNetwork.id + " RSSI " + rssi + " txSpeed " + txSpeed);
318         perNetwork.updateEventStats(event, rssi, txSpeed, failureReason);
319     }
320 
321     /**
322      * Updates the score card after a signal poll
323      *
324      * @param wifiInfo object holding relevant values
325      */
noteSignalPoll(@onNull ExtendedWifiInfo wifiInfo)326     public void noteSignalPoll(@NonNull ExtendedWifiInfo wifiInfo) {
327         if (!mPolled && wifiInfo.getRssi() != INVALID_RSSI) {
328             updatePerBssid(Event.FIRST_POLL_AFTER_CONNECTION, wifiInfo);
329             mPolled = true;
330         }
331         updatePerBssid(Event.SIGNAL_POLL, wifiInfo);
332         int validTxSpeed = geTxLinkSpeedWithSufficientTxRate(wifiInfo);
333         updatePerNetwork(Event.SIGNAL_POLL, wifiInfo.getSSID(), wifiInfo.getRssi(),
334                 validTxSpeed, UNKNOWN_REASON);
335         if (mTsRoam > TS_NONE && wifiInfo.getRssi() != INVALID_RSSI) {
336             long duration = mClock.getElapsedSinceBootMillis() - mTsRoam;
337             if (duration >= SUCCESS_MILLIS_SINCE_ROAM) {
338                 updatePerBssid(Event.ROAM_SUCCESS, wifiInfo);
339                 mTsRoam = TS_NONE;
340                 doWritesBssid();
341             }
342         }
343     }
344 
geTxLinkSpeedWithSufficientTxRate(@onNull ExtendedWifiInfo wifiInfo)345     private int geTxLinkSpeedWithSufficientTxRate(@NonNull ExtendedWifiInfo wifiInfo) {
346         int txRate = (int) Math.ceil(wifiInfo.getSuccessfulTxPacketsPerSecond()
347                 + wifiInfo.getLostTxPacketsPerSecond()
348                 + wifiInfo.getRetriedTxPacketsPerSecond());
349         int txSpeed = wifiInfo.getTxLinkSpeedMbps();
350         logd("txRate: " + txRate + " txSpeed: " + txSpeed);
351         return (txRate >= HEALTH_MONITOR_MIN_TX_PACKET_PER_SEC) ? txSpeed : LINK_SPEED_UNKNOWN;
352     }
353 
354     /** Wait a few seconds before considering the roam successful */
355     private static final long SUCCESS_MILLIS_SINCE_ROAM = 4_000;
356 
357     /**
358      * Updates the score card after IP configuration
359      *
360      * @param wifiInfo object holding relevant values
361      */
noteIpConfiguration(@onNull ExtendedWifiInfo wifiInfo)362     public void noteIpConfiguration(@NonNull ExtendedWifiInfo wifiInfo) {
363         updatePerBssid(Event.IP_CONFIGURATION_SUCCESS, wifiInfo);
364         mAttemptingSwitch = false;
365         doWrites();
366     }
367 
368     /**
369      * Updates the score card after network validation success.
370      *
371      * @param wifiInfo object holding relevant values
372      */
noteValidationSuccess(@onNull ExtendedWifiInfo wifiInfo)373     public void noteValidationSuccess(@NonNull ExtendedWifiInfo wifiInfo) {
374         if (mValidatedThisConnectionAtLeastOnce) return; // Only once per connection
375         updatePerBssid(Event.VALIDATION_SUCCESS, wifiInfo);
376         mValidatedThisConnectionAtLeastOnce = true;
377         doWrites();
378     }
379 
380     /**
381      * Updates the score card after network validation failure
382      *
383      * @param wifiInfo object holding relevant values
384      */
noteValidationFailure(@onNull ExtendedWifiInfo wifiInfo)385     public void noteValidationFailure(@NonNull ExtendedWifiInfo wifiInfo) {
386         // VALIDATION_FAILURE is not currently recorded.
387     }
388 
389     /**
390      * Records the start of a connection attempt
391      *
392      * @param wifiInfo may have state about an existing connection
393      * @param scanRssi is the highest RSSI of recent scan found from scanDetailCache
394      * @param ssid is the network SSID of connection attempt
395      */
noteConnectionAttempt(@onNull ExtendedWifiInfo wifiInfo, int scanRssi, String ssid)396     public void noteConnectionAttempt(@NonNull ExtendedWifiInfo wifiInfo,
397             int scanRssi, String ssid) {
398         // We may or may not be currently connected. If not, simply record the start.
399         // But if we are connected, wrap up the old one first.
400         if (mTsConnectionAttemptStart > TS_NONE) {
401             if (mPolled) {
402                 updatePerBssid(Event.LAST_POLL_BEFORE_SWITCH, wifiInfo);
403             }
404             mAttemptingSwitch = true;
405         }
406         mTsConnectionAttemptStart = mClock.getElapsedSinceBootMillis();
407         mPolled = false;
408         mSsidPrev = mSsidCurr;
409         mSsidCurr = ssid;
410         mFirmwareAlertTimeMs = TS_NONE;
411 
412         updatePerNetwork(Event.CONNECTION_ATTEMPT, ssid, scanRssi, LINK_SPEED_UNKNOWN,
413                 UNKNOWN_REASON);
414         logd("CONNECTION_ATTEMPT" + (mAttemptingSwitch ? " X " : " ") + wifiInfo);
415     }
416 
417     /**
418      * Records a newly assigned NetworkAgent netId.
419      */
noteNetworkAgentCreated(@onNull ExtendedWifiInfo wifiInfo, int networkAgentId)420     public void noteNetworkAgentCreated(@NonNull ExtendedWifiInfo wifiInfo, int networkAgentId) {
421         PerBssid perBssid = lookupBssid(wifiInfo.getSSID(), wifiInfo.getBSSID());
422         logd("NETWORK_AGENT_ID: " + networkAgentId + " ID: " + perBssid.id);
423         perBssid.mNetworkAgentId = networkAgentId;
424     }
425 
426     /**
427      * Record disconnection not initiated by wpa_supplicant in connected mode
428      * @param reason is detailed disconnection reason code
429      */
noteNonlocalDisconnect(int reason)430     public void noteNonlocalDisconnect(int reason) {
431         mNonlocalDisconnection = true;
432         mDisconnectionReason = reason;
433         logd("nonlocal disconnection with reason: " + reason);
434     }
435 
436     /**
437      * Record firmware alert timestamp and error code
438      */
noteFirmwareAlert(int errorCode)439     public void noteFirmwareAlert(int errorCode) {
440         mFirmwareAlertTimeMs = mClock.getElapsedSinceBootMillis();
441         logd("firmware alert with error code: " + errorCode);
442     }
443 
444     /**
445      * Updates the score card after a failed connection attempt
446      *
447      * @param wifiInfo object holding relevant values.
448      * @param scanRssi is the highest RSSI of recent scan found from scanDetailCache
449      * @param ssid is the network SSID.
450      * @param failureReason is connection failure reason
451      */
noteConnectionFailure(@onNull ExtendedWifiInfo wifiInfo, int scanRssi, String ssid, @FailureReason int failureReason)452     public void noteConnectionFailure(@NonNull ExtendedWifiInfo wifiInfo,
453             int scanRssi, String ssid, @FailureReason int failureReason) {
454         // TODO: add the breakdown of level2FailureReason
455         updatePerBssid(Event.CONNECTION_FAILURE, wifiInfo);
456 
457         updatePerNetwork(Event.CONNECTION_FAILURE, ssid, scanRssi, LINK_SPEED_UNKNOWN,
458                 failureReason);
459         resetConnectionStateInternal(false);
460     }
461 
462     /**
463      * Updates the score card after network reachability failure
464      *
465      * @param wifiInfo object holding relevant values
466      */
noteIpReachabilityLost(@onNull ExtendedWifiInfo wifiInfo)467     public void noteIpReachabilityLost(@NonNull ExtendedWifiInfo wifiInfo) {
468         if (mTsRoam > TS_NONE) {
469             mTsConnectionAttemptStart = mTsRoam; // just to update elapsed
470             updatePerBssid(Event.ROAM_FAILURE, wifiInfo);
471         } else {
472             updatePerBssid(Event.IP_REACHABILITY_LOST, wifiInfo);
473         }
474         // No need to call resetConnectionStateInternal() because
475         // resetConnectionState() will be called after WifiNative.disconnect() in ClientModeImpl
476         doWrites();
477     }
478 
479     /**
480      * Updates the score card before a roam
481      *
482      * We may have already done a firmware roam, but wifiInfo has not yet
483      * been updated, so we still have the old state.
484      *
485      * @param wifiInfo object holding relevant values
486      */
noteRoam(@onNull ExtendedWifiInfo wifiInfo)487     private void noteRoam(@NonNull ExtendedWifiInfo wifiInfo) {
488         updatePerBssid(Event.LAST_POLL_BEFORE_ROAM, wifiInfo);
489         mTsRoam = mClock.getElapsedSinceBootMillis();
490     }
491 
492     /**
493      * Called when the supplicant state is about to change, before wifiInfo is updated
494      *
495      * @param wifiInfo object holding old values
496      * @param state the new supplicant state
497      */
noteSupplicantStateChanging(@onNull ExtendedWifiInfo wifiInfo, SupplicantState state)498     public void noteSupplicantStateChanging(@NonNull ExtendedWifiInfo wifiInfo,
499             SupplicantState state) {
500         if (state == SupplicantState.COMPLETED && wifiInfo.getSupplicantState() == state) {
501             // Our signal that a firmware roam has occurred
502             noteRoam(wifiInfo);
503         }
504         logd("Changing state to " + state + " " + wifiInfo);
505     }
506 
507     /**
508      * Called after the supplicant state changed
509      *
510      * @param wifiInfo object holding old values
511      */
noteSupplicantStateChanged(ExtendedWifiInfo wifiInfo)512     public void noteSupplicantStateChanged(ExtendedWifiInfo wifiInfo) {
513         logd("STATE " + wifiInfo);
514     }
515 
516     /**
517      * Updates the score card after wifi is disabled
518      *
519      * @param wifiInfo object holding relevant values
520      */
noteWifiDisabled(@onNull ExtendedWifiInfo wifiInfo)521     public void noteWifiDisabled(@NonNull ExtendedWifiInfo wifiInfo) {
522         updatePerBssid(Event.WIFI_DISABLED, wifiInfo);
523         resetConnectionStateInternal(false);
524         doWrites();
525     }
526 
527     /**
528      * Records the last successful L2 connection timestamp for a BSSID.
529      * @return the previous BSSID connection time.
530      */
setBssidConnectionTimestampMs(String ssid, String bssid, long timeMs)531     public long setBssidConnectionTimestampMs(String ssid, String bssid, long timeMs) {
532         PerBssid perBssid = lookupBssid(ssid, bssid);
533         long prev = perBssid.lastConnectionTimestampMs;
534         perBssid.lastConnectionTimestampMs = timeMs;
535         return prev;
536     }
537 
538     /**
539      * Returns the last successful L2 connection time for this BSSID.
540      */
getBssidConnectionTimestampMs(String ssid, String bssid)541     public long getBssidConnectionTimestampMs(String ssid, String bssid) {
542         return lookupBssid(ssid, bssid).lastConnectionTimestampMs;
543     }
544 
545     /**
546      * Increment the blocklist streak count for a failure reason on an AP.
547      * @return the updated count
548      */
incrementBssidBlocklistStreak(String ssid, String bssid, @BssidBlocklistMonitor.FailureReason int reason)549     public int incrementBssidBlocklistStreak(String ssid, String bssid,
550             @BssidBlocklistMonitor.FailureReason int reason) {
551         PerBssid perBssid = lookupBssid(ssid, bssid);
552         return ++perBssid.blocklistStreakCount[reason];
553     }
554 
555     /**
556      * Get the blocklist streak count for a failure reason on an AP.
557      * @return the blocklist streak count
558      */
getBssidBlocklistStreak(String ssid, String bssid, @BssidBlocklistMonitor.FailureReason int reason)559     public int getBssidBlocklistStreak(String ssid, String bssid,
560             @BssidBlocklistMonitor.FailureReason int reason) {
561         return lookupBssid(ssid, bssid).blocklistStreakCount[reason];
562     }
563 
564     /**
565      * Clear the blocklist streak count for a failure reason on an AP.
566      */
resetBssidBlocklistStreak(String ssid, String bssid, @BssidBlocklistMonitor.FailureReason int reason)567     public void resetBssidBlocklistStreak(String ssid, String bssid,
568             @BssidBlocklistMonitor.FailureReason int reason) {
569         lookupBssid(ssid, bssid).blocklistStreakCount[reason] = 0;
570     }
571 
572     /**
573      * Clear the blocklist streak count for all APs that belong to this SSID.
574      */
resetBssidBlocklistStreakForSsid(@onNull String ssid)575     public void resetBssidBlocklistStreakForSsid(@NonNull String ssid) {
576         Iterator<Map.Entry<MacAddress, PerBssid>> it = mApForBssid.entrySet().iterator();
577         while (it.hasNext()) {
578             PerBssid perBssid = it.next().getValue();
579             if (!ssid.equals(perBssid.ssid)) {
580                 continue;
581             }
582             for (int i = 0; i < perBssid.blocklistStreakCount.length; i++) {
583                 perBssid.blocklistStreakCount[i] = 0;
584             }
585         }
586     }
587 
588     /**
589      * Detect abnormal disconnection at high RSSI with a high rate
590      */
detectAbnormalDisconnection()591     public int detectAbnormalDisconnection() {
592         String ssid = mAttemptingSwitch ? mSsidPrev : mSsidCurr;
593         PerNetwork perNetwork = lookupNetwork(ssid);
594         NetworkConnectionStats recentStats = perNetwork.getRecentStats();
595         if (recentStats.getRecentCountCode() == CNT_SHORT_CONNECTION_NONLOCAL) {
596             return detectAbnormalFailureReason(recentStats, CNT_SHORT_CONNECTION_NONLOCAL,
597                     REASON_SHORT_CONNECTION_NONLOCAL,
598                     mDeviceConfigFacade.getShortConnectionNonlocalHighThrPercent(),
599                     mDeviceConfigFacade.getShortConnectionNonlocalCountMin(),
600                     CNT_DISCONNECTION);
601         } else if (recentStats.getRecentCountCode() == CNT_DISCONNECTION_NONLOCAL) {
602             return detectAbnormalFailureReason(recentStats, CNT_DISCONNECTION_NONLOCAL,
603                     REASON_DISCONNECTION_NONLOCAL,
604                     mDeviceConfigFacade.getDisconnectionNonlocalHighThrPercent(),
605                     mDeviceConfigFacade.getDisconnectionNonlocalCountMin(),
606                     CNT_DISCONNECTION);
607         } else {
608             return REASON_NO_FAILURE;
609         }
610     }
611 
612     /**
613      * Detect abnormal connection failure at high RSSI with a high rate
614      */
detectAbnormalConnectionFailure(String ssid)615     public int detectAbnormalConnectionFailure(String ssid) {
616         PerNetwork perNetwork = lookupNetwork(ssid);
617         NetworkConnectionStats recentStats = perNetwork.getRecentStats();
618         int recentCountCode = recentStats.getRecentCountCode();
619         if (recentCountCode == CNT_AUTHENTICATION_FAILURE) {
620             return detectAbnormalFailureReason(recentStats, CNT_AUTHENTICATION_FAILURE,
621                     REASON_AUTH_FAILURE,
622                     mDeviceConfigFacade.getAuthFailureHighThrPercent(),
623                     mDeviceConfigFacade.getAuthFailureCountMin(),
624                     CNT_CONNECTION_ATTEMPT);
625         } else if (recentCountCode == CNT_ASSOCIATION_REJECTION) {
626             return detectAbnormalFailureReason(recentStats, CNT_ASSOCIATION_REJECTION,
627                     REASON_ASSOC_REJECTION,
628                     mDeviceConfigFacade.getAssocRejectionHighThrPercent(),
629                     mDeviceConfigFacade.getAssocRejectionCountMin(),
630                     CNT_CONNECTION_ATTEMPT);
631         } else if (recentCountCode == CNT_ASSOCIATION_TIMEOUT) {
632             return detectAbnormalFailureReason(recentStats, CNT_ASSOCIATION_TIMEOUT,
633                     REASON_ASSOC_TIMEOUT,
634                     mDeviceConfigFacade.getAssocTimeoutHighThrPercent(),
635                     mDeviceConfigFacade.getAssocTimeoutCountMin(),
636                     CNT_CONNECTION_ATTEMPT);
637         } else if (recentCountCode == CNT_CONNECTION_FAILURE) {
638             return detectAbnormalFailureReason(recentStats, CNT_CONNECTION_FAILURE,
639                     REASON_CONNECTION_FAILURE,
640                     mDeviceConfigFacade.getConnectionFailureHighThrPercent(),
641                     mDeviceConfigFacade.getConnectionFailureCountMin(),
642                     CNT_CONNECTION_ATTEMPT);
643         } else {
644             return REASON_NO_FAILURE;
645         }
646     }
647 
detectAbnormalFailureReason(NetworkConnectionStats stats, int countCode, int reasonCode, int highThresholdPercent, int minCount, int refCountCode)648     private int detectAbnormalFailureReason(NetworkConnectionStats stats, int countCode,
649             int reasonCode, int highThresholdPercent, int minCount, int refCountCode) {
650         // To detect abnormal failure which may trigger bugReport,
651         // increase the detection threshold by thresholdRatio
652         int thresholdRatio =
653                 mDeviceConfigFacade.getBugReportThresholdExtraRatio();
654         if (isHighPercentageAndEnoughCount(stats, countCode, reasonCode,
655                 highThresholdPercent * thresholdRatio,
656                 minCount * thresholdRatio,
657                 refCountCode)) {
658             return reasonCode;
659         } else {
660             return REASON_NO_FAILURE;
661         }
662     }
663 
isHighPercentageAndEnoughCount(NetworkConnectionStats stats, int countCode, int reasonCode, int highThresholdPercent, int minCount, int refCountCode)664     private boolean isHighPercentageAndEnoughCount(NetworkConnectionStats stats, int countCode,
665             int reasonCode, int highThresholdPercent, int minCount, int refCountCode) {
666         highThresholdPercent = Math.min(highThresholdPercent, 100);
667         // Use Laplace's rule of succession, useful especially for a small
668         // connection attempt count
669         // R = (f+1)/(n+2) with a pseudo count of 2 (one for f and one for s)
670         return ((stats.getCount(countCode) >= minCount)
671                 && ((stats.getCount(countCode) + 1) * 100)
672                 >= (highThresholdPercent * (stats.getCount(refCountCode) + 2)));
673     }
674 
675     final class PerBssid extends MemoryStoreAccessBase {
676         public int id;
677         public final String ssid;
678         public final MacAddress bssid;
679         public final int[] blocklistStreakCount =
680                 new int[BssidBlocklistMonitor.NUMBER_REASON_CODES];
681         // The wall clock time in milliseconds for the last successful l2 connection.
682         public long lastConnectionTimestampMs;
683         public boolean changed;
684         public boolean referenced;
685 
686         private SecurityType mSecurityType = null;
687         private int mNetworkAgentId = Integer.MIN_VALUE;
688         private int mNetworkConfigId = Integer.MIN_VALUE;
689         private final Map<Pair<Event, Integer>, PerSignal>
690                 mSignalForEventAndFrequency = new ArrayMap<>();
PerBssid(String ssid, MacAddress bssid)691         PerBssid(String ssid, MacAddress bssid) {
692             super(computeHashLong(ssid, bssid, mL2KeySeed));
693             this.ssid = ssid;
694             this.bssid = bssid;
695             this.id = idFromLong();
696             this.changed = false;
697             this.referenced = false;
698         }
updateEventStats(Event event, int frequency, int rssi, int linkspeed)699         void updateEventStats(Event event, int frequency, int rssi, int linkspeed) {
700             PerSignal perSignal = lookupSignal(event, frequency);
701             if (rssi != INVALID_RSSI) {
702                 perSignal.rssi.update(rssi);
703                 changed = true;
704             }
705             if (linkspeed > 0) {
706                 perSignal.linkspeed.update(linkspeed);
707                 changed = true;
708             }
709             if (perSignal.elapsedMs != null && mTsConnectionAttemptStart > TS_NONE) {
710                 long millis = mClock.getElapsedSinceBootMillis() - mTsConnectionAttemptStart;
711                 if (millis >= 0) {
712                     perSignal.elapsedMs.update(millis);
713                     changed = true;
714                 }
715             }
716         }
lookupSignal(Event event, int frequency)717         PerSignal lookupSignal(Event event, int frequency) {
718             finishPendingRead();
719             Pair<Event, Integer> key = new Pair<>(event, frequency);
720             PerSignal ans = mSignalForEventAndFrequency.get(key);
721             if (ans == null) {
722                 ans = new PerSignal(event, frequency);
723                 mSignalForEventAndFrequency.put(key, ans);
724             }
725             return ans;
726         }
getSecurityType()727         SecurityType getSecurityType() {
728             finishPendingRead();
729             return mSecurityType;
730         }
setSecurityType(SecurityType securityType)731         void setSecurityType(SecurityType securityType) {
732             finishPendingRead();
733             if (!Objects.equals(securityType, mSecurityType)) {
734                 mSecurityType = securityType;
735                 changed = true;
736             }
737         }
setNetworkConfigId(int networkConfigId)738         void setNetworkConfigId(int networkConfigId) {
739             // Not serialized, so don't need to set changed, etc.
740             if (networkConfigId >= 0) {
741                 mNetworkConfigId = networkConfigId;
742             }
743         }
toAccessPoint()744         AccessPoint toAccessPoint() {
745             return toAccessPoint(false);
746         }
toAccessPoint(boolean obfuscate)747         AccessPoint toAccessPoint(boolean obfuscate) {
748             finishPendingRead();
749             AccessPoint.Builder builder = AccessPoint.newBuilder();
750             builder.setId(id);
751             if (!obfuscate) {
752                 builder.setBssid(ByteString.copyFrom(bssid.toByteArray()));
753             }
754             if (mSecurityType != null) {
755                 builder.setSecurityType(mSecurityType);
756             }
757             for (PerSignal sig: mSignalForEventAndFrequency.values()) {
758                 builder.addEventStats(sig.toSignal());
759             }
760             return builder.build();
761         }
merge(AccessPoint ap)762         PerBssid merge(AccessPoint ap) {
763             if (ap.hasId() && this.id != ap.getId()) {
764                 return this;
765             }
766             if (ap.hasSecurityType()) {
767                 SecurityType prev = ap.getSecurityType();
768                 if (mSecurityType == null) {
769                     mSecurityType = prev;
770                 } else if (!mSecurityType.equals(prev)) {
771                     if (mVerboseLoggingEnabled) {
772                         Log.i(TAG, "ID: " + id
773                                 + "SecurityType changed: " + prev + " to " + mSecurityType);
774                     }
775                     changed = true;
776                 }
777             }
778             for (Signal signal: ap.getEventStatsList()) {
779                 Pair<Event, Integer> key = new Pair<>(signal.getEvent(), signal.getFrequency());
780                 PerSignal perSignal = mSignalForEventAndFrequency.get(key);
781                 if (perSignal == null) {
782                     mSignalForEventAndFrequency.put(key,
783                             new PerSignal(key.first, key.second).merge(signal));
784                     // No need to set changed for this, since we are in sync with what's stored
785                 } else {
786                     perSignal.merge(signal);
787                     changed = true;
788                 }
789             }
790             return this;
791         }
792 
793         /**
794          * Handles (when convenient) the arrival of previously stored data.
795          *
796          * The response from IpMemoryStore arrives on a different thread, so we
797          * defer handling it until here, when we're on our favorite thread and
798          * in a good position to deal with it. We may have already collected some
799          * data before now, so we need to be prepared to merge the new and old together.
800          */
finishPendingRead()801         void finishPendingRead() {
802             final byte[] serialized = finishPendingReadBytes();
803             if (serialized == null) return;
804             AccessPoint ap;
805             try {
806                 ap = AccessPoint.parseFrom(serialized);
807             } catch (InvalidProtocolBufferException e) {
808                 Log.e(TAG, "Failed to deserialize", e);
809                 return;
810             }
811             merge(ap);
812         }
813 
814         /**
815          * Estimates the probability of getting internet access, based on the
816          * device experience.
817          *
818          * @return a probability, expressed as a percentage in the range 0 to 100
819          */
estimatePercentInternetAvailability()820         public int estimatePercentInternetAvailability() {
821             // Initialize counts accoring to Laplace's rule of succession
822             int trials = 2;
823             int successes = 1;
824             // Aggregate over all of the frequencies
825             for (PerSignal s : mSignalForEventAndFrequency.values()) {
826                 switch (s.event) {
827                     case IP_CONFIGURATION_SUCCESS:
828                         if (s.elapsedMs != null) {
829                             trials += s.elapsedMs.count;
830                         }
831                         break;
832                     case VALIDATION_SUCCESS:
833                         if (s.elapsedMs != null) {
834                             successes += s.elapsedMs.count;
835                         }
836                         break;
837                     default:
838                         break;
839                 }
840             }
841             // Note that because of roaming it is possible to count successes
842             // without corresponding trials.
843             return Math.min(Math.max(Math.round(successes * 100.0f / trials), 0), 100);
844         }
845     }
846 
847     /**
848      * A class collecting the connection stats of one network or SSID.
849      */
850     final class PerNetwork extends MemoryStoreAccessBase {
851         public int id;
852         public final String ssid;
853         public boolean changed;
854         private int mLastRssiPoll = INVALID_RSSI;
855         private int mLastTxSpeedPoll = LINK_SPEED_UNKNOWN;
856         private long mLastRssiPollTimeMs = TS_NONE;
857         private long mConnectionSessionStartTimeMs = TS_NONE;
858         private NetworkConnectionStats mRecentStats;
859         private NetworkConnectionStats mStatsCurrBuild;
860         private NetworkConnectionStats mStatsPrevBuild;
861         private LruList<Integer> mFrequencyList;
862         // In memory keep frequency with timestamp last time available, the elapsed time since boot.
863         private SparseLongArray mFreqTimestamp;
864 
PerNetwork(String ssid)865         PerNetwork(String ssid) {
866             super(computeHashLong(ssid, MacAddress.fromString(DEFAULT_MAC_ADDRESS), mL2KeySeed));
867             this.ssid = ssid;
868             this.id = idFromLong();
869             this.changed = false;
870             mRecentStats = new NetworkConnectionStats();
871             mStatsCurrBuild = new NetworkConnectionStats();
872             mStatsPrevBuild = new NetworkConnectionStats();
873             mFrequencyList = new LruList<>(MAX_FREQUENCIES_PER_SSID);
874             mFreqTimestamp = new SparseLongArray();
875         }
876 
updateEventStats(Event event, int rssi, int txSpeed, int failureReason)877         void updateEventStats(Event event, int rssi, int txSpeed, int failureReason) {
878             finishPendingRead();
879             long currTimeMs = mClock.getElapsedSinceBootMillis();
880             switch (event) {
881                 case SIGNAL_POLL:
882                     mLastRssiPoll = rssi;
883                     mLastRssiPollTimeMs = currTimeMs;
884                     mLastTxSpeedPoll = txSpeed;
885                     changed = true;
886                     break;
887                 case CONNECTION_ATTEMPT:
888                     logd(" scan rssi: " + rssi);
889                     if (rssi >= mDeviceConfigFacade.getHealthMonitorMinRssiThrDbm()) {
890                         mRecentStats.incrementCount(CNT_CONNECTION_ATTEMPT);
891                     }
892                     mConnectionSessionStartTimeMs = currTimeMs;
893                     changed = true;
894                     break;
895                 case CONNECTION_FAILURE:
896                     mConnectionSessionStartTimeMs = TS_NONE;
897                     if (rssi >= mDeviceConfigFacade.getHealthMonitorMinRssiThrDbm()) {
898                         if (failureReason != BssidBlocklistMonitor.REASON_WRONG_PASSWORD) {
899                             mRecentStats.incrementCount(CNT_CONNECTION_FAILURE);
900                             mRecentStats.incrementCount(CNT_CONSECUTIVE_CONNECTION_FAILURE);
901                         }
902                         switch (failureReason) {
903                             case BssidBlocklistMonitor.REASON_AP_UNABLE_TO_HANDLE_NEW_STA:
904                             case BssidBlocklistMonitor.REASON_ASSOCIATION_REJECTION:
905                                 mRecentStats.incrementCount(CNT_ASSOCIATION_REJECTION);
906                                 break;
907                             case BssidBlocklistMonitor.REASON_ASSOCIATION_TIMEOUT:
908                                 mRecentStats.incrementCount(CNT_ASSOCIATION_TIMEOUT);
909                                 break;
910                             case BssidBlocklistMonitor.REASON_AUTHENTICATION_FAILURE:
911                             case BssidBlocklistMonitor.REASON_EAP_FAILURE:
912                                 mRecentStats.incrementCount(CNT_AUTHENTICATION_FAILURE);
913                                 break;
914                             case BssidBlocklistMonitor.REASON_WRONG_PASSWORD:
915                             case BssidBlocklistMonitor.REASON_DHCP_FAILURE:
916                             default:
917                                 break;
918                         }
919                     }
920                     changed = true;
921                     break;
922                 case WIFI_DISABLED:
923                 case DISCONNECTION:
924                     handleDisconnection();
925                     changed = true;
926                     break;
927                 default:
928                     break;
929             }
930             logd(this.toString());
931         }
932         @Override
toString()933         public String toString() {
934             StringBuilder sb = new StringBuilder();
935             sb.append("SSID: ").append(ssid).append("\n");
936             if (mLastRssiPollTimeMs != TS_NONE) {
937                 sb.append(" LastRssiPollTime: ");
938                 sb.append(mLastRssiPollTimeMs);
939             }
940             sb.append(" LastRssiPoll: " + mLastRssiPoll);
941             sb.append(" LastTxSpeedPoll: " + mLastTxSpeedPoll);
942             sb.append("\n");
943             sb.append(" StatsRecent: ").append(mRecentStats).append("\n");
944             sb.append(" StatsCurr: ").append(mStatsCurrBuild).append("\n");
945             sb.append(" StatsPrev: ").append(mStatsPrevBuild);
946             return sb.toString();
947         }
handleDisconnection()948         private void handleDisconnection() {
949             if (mConnectionSessionStartTimeMs > TS_NONE) {
950                 long currTimeMs = mClock.getElapsedSinceBootMillis();
951                 int currSessionDurationMs = (int) (currTimeMs - mConnectionSessionStartTimeMs);
952                 int currSessionDurationSec = currSessionDurationMs / 1000;
953                 mRecentStats.accumulate(CNT_CONNECTION_DURATION_SEC, currSessionDurationSec);
954                 long timeSinceLastRssiPollMs = currTimeMs - mLastRssiPollTimeMs;
955                 boolean hasRecentRssiPoll = mLastRssiPollTimeMs > TS_NONE
956                         && timeSinceLastRssiPollMs <= mDeviceConfigFacade
957                         .getHealthMonitorRssiPollValidTimeMs();
958                 if (hasRecentRssiPoll) {
959                     mRecentStats.incrementCount(CNT_DISCONNECTION);
960                 }
961                 int fwAlertValidTimeMs = mDeviceConfigFacade.getHealthMonitorFwAlertValidTimeMs();
962                 long timeSinceLastFirmAlert = currTimeMs - mFirmwareAlertTimeMs;
963                 boolean isInvalidFwAlertTime = mFirmwareAlertTimeMs == TS_NONE;
964                 boolean disableFwAlertCheck = fwAlertValidTimeMs == -1;
965                 boolean passFirmwareAlertCheck = disableFwAlertCheck ? true : (isInvalidFwAlertTime
966                         ? false : timeSinceLastFirmAlert < fwAlertValidTimeMs);
967                 boolean hasHighRssiOrHighTxSpeed =
968                         mLastRssiPoll >= mDeviceConfigFacade.getHealthMonitorMinRssiThrDbm()
969                         || mLastTxSpeedPoll >= HEALTH_MONITOR_COUNT_TX_SPEED_MIN_MBPS;
970                 if (mNonlocalDisconnection && hasRecentRssiPoll
971                         && isAbnormalDisconnectionReason(mDisconnectionReason)
972                         && passFirmwareAlertCheck
973                         && hasHighRssiOrHighTxSpeed) {
974                     mRecentStats.incrementCount(CNT_DISCONNECTION_NONLOCAL);
975                     if (currSessionDurationMs <= mDeviceConfigFacade
976                             .getHealthMonitorShortConnectionDurationThrMs()) {
977                         mRecentStats.incrementCount(CNT_SHORT_CONNECTION_NONLOCAL);
978                     }
979                 }
980             }
981             // Reset CNT_CONSECUTIVE_CONNECTION_FAILURE here so that it can report the correct
982             // failure count after a connection success
983             mRecentStats.clearCount(CNT_CONSECUTIVE_CONNECTION_FAILURE);
984             mConnectionSessionStartTimeMs = TS_NONE;
985             mLastRssiPollTimeMs = TS_NONE;
986         }
987 
isAbnormalDisconnectionReason(int disconnectionReason)988         private boolean isAbnormalDisconnectionReason(int disconnectionReason) {
989             long mask = mDeviceConfigFacade.getAbnormalDisconnectionReasonCodeMask();
990             return disconnectionReason >= 0 && disconnectionReason <= 63
991                     && ((mask >> disconnectionReason) & 0x1) == 0x1;
992         }
993 
getRecentStats()994         @NonNull NetworkConnectionStats getRecentStats() {
995             return mRecentStats;
996         }
getStatsCurrBuild()997         @NonNull NetworkConnectionStats getStatsCurrBuild() {
998             return mStatsCurrBuild;
999         }
getStatsPrevBuild()1000         @NonNull NetworkConnectionStats getStatsPrevBuild() {
1001             return mStatsPrevBuild;
1002         }
1003 
1004         /**
1005          * Retrieve the list of frequencies seen for this network, with the most recent first.
1006          * @param ageInMills Max age to filter the channels.
1007          * @return a list of frequencies
1008          */
getFrequencies(Long ageInMills)1009         List<Integer> getFrequencies(Long ageInMills) {
1010             List<Integer> results = new ArrayList<>();
1011             Long nowInMills = mClock.getElapsedSinceBootMillis();
1012             for (Integer freq : mFrequencyList.getEntries()) {
1013                 if (nowInMills - mFreqTimestamp.get(freq, 0L) > ageInMills) {
1014                     continue;
1015                 }
1016                 results.add(freq);
1017             }
1018             return results;
1019         }
1020 
1021         /**
1022          * Add a frequency to the list of frequencies for this network.
1023          * Will evict the least recently added frequency if the cache is full.
1024          */
addFrequency(int frequency)1025         void addFrequency(int frequency) {
1026             mFrequencyList.add(frequency);
1027             mFreqTimestamp.put(frequency, mClock.getElapsedSinceBootMillis());
1028         }
1029 
1030         /**
1031         /* Detect a significant failure stats change with historical data
1032         /* or high failure stats without historical data.
1033         /* @return 0 if recentStats doesn't have sufficient data
1034          *         1 if recentStats has sufficient data while statsPrevBuild doesn't
1035          *         2 if recentStats and statsPrevBuild have sufficient data
1036          */
dailyDetection(FailureStats statsDec, FailureStats statsInc, FailureStats statsHigh)1037         int dailyDetection(FailureStats statsDec, FailureStats statsInc, FailureStats statsHigh) {
1038             finishPendingRead();
1039             dailyDetectionDisconnectionEvent(statsDec, statsInc, statsHigh);
1040             return dailyDetectionConnectionEvent(statsDec, statsInc, statsHigh);
1041         }
1042 
dailyDetectionConnectionEvent(FailureStats statsDec, FailureStats statsInc, FailureStats statsHigh)1043         private int dailyDetectionConnectionEvent(FailureStats statsDec, FailureStats statsInc,
1044                 FailureStats statsHigh) {
1045             // Skip daily detection if recentStats is not sufficient
1046             if (!isRecentConnectionStatsSufficient()) return INSUFFICIENT_RECENT_STATS;
1047             if (mStatsPrevBuild.getCount(CNT_CONNECTION_ATTEMPT)
1048                     < mDeviceConfigFacade.getHealthMonitorMinNumConnectionAttempt()) {
1049                 // don't have enough historical data,
1050                 // so only detect high failure stats without relying on mStatsPrevBuild.
1051                 recentStatsHighDetectionConnection(statsHigh);
1052                 return SUFFICIENT_RECENT_STATS_ONLY;
1053             } else {
1054                 // mStatsPrevBuild has enough updates,
1055                 // detect improvement or degradation
1056                 statsDeltaDetectionConnection(statsDec, statsInc);
1057                 return SUFFICIENT_RECENT_PREV_STATS;
1058             }
1059         }
1060 
dailyDetectionDisconnectionEvent(FailureStats statsDec, FailureStats statsInc, FailureStats statsHigh)1061         private void dailyDetectionDisconnectionEvent(FailureStats statsDec, FailureStats statsInc,
1062                 FailureStats statsHigh) {
1063             // Skip daily detection if recentStats is not sufficient
1064             int minConnectAttempt = mDeviceConfigFacade.getHealthMonitorMinNumConnectionAttempt();
1065             if (mRecentStats.getCount(CNT_CONNECTION_ATTEMPT) < minConnectAttempt) {
1066                 return;
1067             }
1068             if (mStatsPrevBuild.getCount(CNT_CONNECTION_ATTEMPT) < minConnectAttempt) {
1069                 recentStatsHighDetectionDisconnection(statsHigh);
1070             } else {
1071                 statsDeltaDetectionDisconnection(statsDec, statsInc);
1072             }
1073         }
1074 
statsDeltaDetectionConnection(FailureStats statsDec, FailureStats statsInc)1075         private void statsDeltaDetectionConnection(FailureStats statsDec,
1076                 FailureStats statsInc) {
1077             statsDeltaDetection(statsDec, statsInc, CNT_CONNECTION_FAILURE,
1078                     REASON_CONNECTION_FAILURE,
1079                     mDeviceConfigFacade.getConnectionFailureCountMin(),
1080                     CNT_CONNECTION_ATTEMPT);
1081             statsDeltaDetection(statsDec, statsInc, CNT_AUTHENTICATION_FAILURE,
1082                     REASON_AUTH_FAILURE,
1083                     mDeviceConfigFacade.getAuthFailureCountMin(),
1084                     CNT_CONNECTION_ATTEMPT);
1085             statsDeltaDetection(statsDec, statsInc, CNT_ASSOCIATION_REJECTION,
1086                     REASON_ASSOC_REJECTION,
1087                     mDeviceConfigFacade.getAssocRejectionCountMin(),
1088                     CNT_CONNECTION_ATTEMPT);
1089             statsDeltaDetection(statsDec, statsInc, CNT_ASSOCIATION_TIMEOUT,
1090                     REASON_ASSOC_TIMEOUT,
1091                     mDeviceConfigFacade.getAssocTimeoutCountMin(),
1092                     CNT_CONNECTION_ATTEMPT);
1093         }
1094 
recentStatsHighDetectionConnection(FailureStats statsHigh)1095         private void recentStatsHighDetectionConnection(FailureStats statsHigh) {
1096             recentStatsHighDetection(statsHigh, CNT_CONNECTION_FAILURE,
1097                     REASON_CONNECTION_FAILURE,
1098                     mDeviceConfigFacade.getConnectionFailureHighThrPercent(),
1099                     mDeviceConfigFacade.getConnectionFailureCountMin(),
1100                     CNT_CONNECTION_ATTEMPT);
1101             recentStatsHighDetection(statsHigh, CNT_AUTHENTICATION_FAILURE,
1102                     REASON_AUTH_FAILURE,
1103                     mDeviceConfigFacade.getAuthFailureHighThrPercent(),
1104                     mDeviceConfigFacade.getAuthFailureCountMin(),
1105                     CNT_CONNECTION_ATTEMPT);
1106             recentStatsHighDetection(statsHigh, CNT_ASSOCIATION_REJECTION,
1107                     REASON_ASSOC_REJECTION,
1108                     mDeviceConfigFacade.getAssocRejectionHighThrPercent(),
1109                     mDeviceConfigFacade.getAssocRejectionCountMin(),
1110                     CNT_CONNECTION_ATTEMPT);
1111             recentStatsHighDetection(statsHigh, CNT_ASSOCIATION_TIMEOUT,
1112                     REASON_ASSOC_TIMEOUT,
1113                     mDeviceConfigFacade.getAssocTimeoutHighThrPercent(),
1114                     mDeviceConfigFacade.getAssocTimeoutCountMin(),
1115                     CNT_CONNECTION_ATTEMPT);
1116         }
1117 
statsDeltaDetectionDisconnection(FailureStats statsDec, FailureStats statsInc)1118         private void statsDeltaDetectionDisconnection(FailureStats statsDec,
1119                 FailureStats statsInc) {
1120             statsDeltaDetection(statsDec, statsInc, CNT_SHORT_CONNECTION_NONLOCAL,
1121                     REASON_SHORT_CONNECTION_NONLOCAL,
1122                     mDeviceConfigFacade.getShortConnectionNonlocalCountMin(),
1123                     CNT_CONNECTION_ATTEMPT);
1124             statsDeltaDetection(statsDec, statsInc, CNT_DISCONNECTION_NONLOCAL,
1125                     REASON_DISCONNECTION_NONLOCAL,
1126                     mDeviceConfigFacade.getDisconnectionNonlocalCountMin(),
1127                     CNT_CONNECTION_ATTEMPT);
1128         }
1129 
recentStatsHighDetectionDisconnection(FailureStats statsHigh)1130         private void recentStatsHighDetectionDisconnection(FailureStats statsHigh) {
1131             recentStatsHighDetection(statsHigh, CNT_SHORT_CONNECTION_NONLOCAL,
1132                     REASON_SHORT_CONNECTION_NONLOCAL,
1133                     mDeviceConfigFacade.getShortConnectionNonlocalHighThrPercent(),
1134                     mDeviceConfigFacade.getShortConnectionNonlocalCountMin(),
1135                     CNT_DISCONNECTION);
1136             recentStatsHighDetection(statsHigh, CNT_DISCONNECTION_NONLOCAL,
1137                     REASON_DISCONNECTION_NONLOCAL,
1138                     mDeviceConfigFacade.getDisconnectionNonlocalHighThrPercent(),
1139                     mDeviceConfigFacade.getDisconnectionNonlocalCountMin(),
1140                     CNT_DISCONNECTION);
1141         }
1142 
statsDeltaDetection(FailureStats statsDec, FailureStats statsInc, int countCode, int reasonCode, int minCount, int refCountCode)1143         private boolean statsDeltaDetection(FailureStats statsDec,
1144                 FailureStats statsInc, int countCode, int reasonCode,
1145                 int minCount, int refCountCode) {
1146             if (isRatioAboveThreshold(mRecentStats, mStatsPrevBuild, countCode, refCountCode)
1147                     && mRecentStats.getCount(countCode) >= minCount) {
1148                 statsInc.incrementCount(reasonCode);
1149                 return true;
1150             }
1151 
1152             if (isRatioAboveThreshold(mStatsPrevBuild, mRecentStats, countCode, refCountCode)
1153                     && mStatsPrevBuild.getCount(countCode) >= minCount) {
1154                 statsDec.incrementCount(reasonCode);
1155                 return true;
1156             }
1157             return false;
1158         }
1159 
recentStatsHighDetection(FailureStats statsHigh, int countCode, int reasonCode, int highThresholdPercent, int minCount, int refCountCode)1160         private boolean recentStatsHighDetection(FailureStats statsHigh, int countCode,
1161                 int reasonCode, int highThresholdPercent, int minCount, int refCountCode) {
1162             if (isHighPercentageAndEnoughCount(mRecentStats, countCode, reasonCode,
1163                     highThresholdPercent, minCount, refCountCode)) {
1164                 statsHigh.incrementCount(reasonCode);
1165                 return true;
1166             }
1167             return false;
1168         }
1169 
isRatioAboveThreshold(NetworkConnectionStats stats1, NetworkConnectionStats stats2, @ConnectionCountCode int countCode, int refCountCode)1170         private boolean isRatioAboveThreshold(NetworkConnectionStats stats1,
1171                 NetworkConnectionStats stats2,
1172                 @ConnectionCountCode int countCode, int refCountCode) {
1173             // Also with Laplace's rule of succession discussed above
1174             // R1 = (stats1(countCode) + 1) / (stats1(refCountCode) + 2)
1175             // R2 = (stats2(countCode) + 1) / (stats2(refCountCode) + 2)
1176             // Check R1 / R2 >= ratioThr
1177             return ((stats1.getCount(countCode) + 1) * (stats2.getCount(refCountCode) + 2)
1178                     * mDeviceConfigFacade.HEALTH_MONITOR_RATIO_THR_DENOMINATOR)
1179                     >= ((stats1.getCount(refCountCode) + 2) * (stats2.getCount(countCode) + 1)
1180                     * mDeviceConfigFacade.getHealthMonitorRatioThrNumerator());
1181         }
1182 
isRecentConnectionStatsSufficient()1183         private boolean isRecentConnectionStatsSufficient() {
1184             return (mRecentStats.getCount(CNT_CONNECTION_ATTEMPT)
1185                 >= mDeviceConfigFacade.getHealthMonitorMinNumConnectionAttempt());
1186         }
1187 
1188         // Update StatsCurrBuild with recentStats and clear recentStats
updateAfterDailyDetection()1189         void updateAfterDailyDetection() {
1190             // Skip update if recentStats is not sufficient since daily detection is also skipped
1191             if (!isRecentConnectionStatsSufficient()) return;
1192             mStatsCurrBuild.accumulateAll(mRecentStats);
1193             mRecentStats.clear();
1194             changed = true;
1195         }
1196 
1197         // Refresh StatsPrevBuild with StatsCurrBuild which is cleared afterwards
updateAfterSwBuildChange()1198         void updateAfterSwBuildChange() {
1199             finishPendingRead();
1200             mStatsPrevBuild.copy(mStatsCurrBuild);
1201             mRecentStats.clear();
1202             mStatsCurrBuild.clear();
1203             changed = true;
1204         }
1205 
toNetworkStats()1206         NetworkStats toNetworkStats() {
1207             finishPendingRead();
1208             NetworkStats.Builder builder = NetworkStats.newBuilder();
1209             builder.setId(id);
1210             builder.setRecentStats(toConnectionStats(mRecentStats));
1211             builder.setStatsCurrBuild(toConnectionStats(mStatsCurrBuild));
1212             builder.setStatsPrevBuild(toConnectionStats(mStatsPrevBuild));
1213             if (mFrequencyList.size() > 0) {
1214                 builder.addAllFrequencies(mFrequencyList.getEntries());
1215             }
1216             return builder.build();
1217         }
1218 
toConnectionStats(NetworkConnectionStats stats)1219         private ConnectionStats toConnectionStats(NetworkConnectionStats stats) {
1220             ConnectionStats.Builder builder = ConnectionStats.newBuilder();
1221             builder.setNumConnectionAttempt(stats.getCount(CNT_CONNECTION_ATTEMPT));
1222             builder.setNumConnectionFailure(stats.getCount(CNT_CONNECTION_FAILURE));
1223             builder.setConnectionDurationSec(stats.getCount(CNT_CONNECTION_DURATION_SEC));
1224             builder.setNumDisconnectionNonlocal(stats.getCount(CNT_DISCONNECTION_NONLOCAL));
1225             builder.setNumDisconnection(stats.getCount(CNT_DISCONNECTION));
1226             builder.setNumShortConnectionNonlocal(stats.getCount(CNT_SHORT_CONNECTION_NONLOCAL));
1227             builder.setNumAssociationRejection(stats.getCount(CNT_ASSOCIATION_REJECTION));
1228             builder.setNumAssociationTimeout(stats.getCount(CNT_ASSOCIATION_TIMEOUT));
1229             builder.setNumAuthenticationFailure(stats.getCount(CNT_AUTHENTICATION_FAILURE));
1230             return builder.build();
1231         }
1232 
finishPendingRead()1233         void finishPendingRead() {
1234             final byte[] serialized = finishPendingReadBytes();
1235             if (serialized == null) return;
1236             NetworkStats ns;
1237             try {
1238                 ns = NetworkStats.parseFrom(serialized);
1239             } catch (InvalidProtocolBufferException e) {
1240                 Log.e(TAG, "Failed to deserialize", e);
1241                 return;
1242             }
1243             mergeNetworkStatsFromMemory(ns);
1244             changed = true;
1245         }
1246 
mergeNetworkStatsFromMemory(@onNull NetworkStats ns)1247         PerNetwork mergeNetworkStatsFromMemory(@NonNull NetworkStats ns) {
1248             if (ns.hasId() && this.id != ns.getId()) {
1249                 return this;
1250             }
1251             if (ns.hasRecentStats()) {
1252                 ConnectionStats recentStats = ns.getRecentStats();
1253                 mergeConnectionStats(recentStats, mRecentStats);
1254             }
1255             if (ns.hasStatsCurrBuild()) {
1256                 ConnectionStats statsCurr = ns.getStatsCurrBuild();
1257                 mStatsCurrBuild.clear();
1258                 mergeConnectionStats(statsCurr, mStatsCurrBuild);
1259             }
1260             if (ns.hasStatsPrevBuild()) {
1261                 ConnectionStats statsPrev = ns.getStatsPrevBuild();
1262                 mStatsPrevBuild.clear();
1263                 mergeConnectionStats(statsPrev, mStatsPrevBuild);
1264             }
1265             if (ns.getFrequenciesList().size() > 0) {
1266                 // This merge assumes that whatever data is in memory is more recent that what's
1267                 // in store
1268                 List<Integer> mergedFrequencyList = mFrequencyList.getEntries();
1269                 mergedFrequencyList.addAll(ns.getFrequenciesList());
1270                 mFrequencyList = new LruList<>(MAX_FREQUENCIES_PER_SSID);
1271                 for (int i = mergedFrequencyList.size() - 1; i >= 0; i--) {
1272                     mFrequencyList.add(mergedFrequencyList.get(i));
1273                 }
1274             }
1275             return this;
1276         }
1277 
mergeConnectionStats(ConnectionStats source, NetworkConnectionStats target)1278         private void mergeConnectionStats(ConnectionStats source, NetworkConnectionStats target) {
1279             if (source.hasNumConnectionAttempt()) {
1280                 target.accumulate(CNT_CONNECTION_ATTEMPT, source.getNumConnectionAttempt());
1281             }
1282             if (source.hasNumConnectionFailure()) {
1283                 target.accumulate(CNT_CONNECTION_ATTEMPT, source.getNumConnectionFailure());
1284             }
1285             if (source.hasConnectionDurationSec()) {
1286                 target.accumulate(CNT_CONNECTION_DURATION_SEC, source.getConnectionDurationSec());
1287             }
1288             if (source.hasNumDisconnectionNonlocal()) {
1289                 target.accumulate(CNT_DISCONNECTION_NONLOCAL, source.getNumDisconnectionNonlocal());
1290             }
1291             if (source.hasNumDisconnection()) {
1292                 target.accumulate(CNT_DISCONNECTION, source.getNumDisconnection());
1293             }
1294             if (source.hasNumShortConnectionNonlocal()) {
1295                 target.accumulate(CNT_SHORT_CONNECTION_NONLOCAL,
1296                         source.getNumShortConnectionNonlocal());
1297             }
1298             if (source.hasNumAssociationRejection()) {
1299                 target.accumulate(CNT_ASSOCIATION_REJECTION, source.getNumAssociationRejection());
1300             }
1301             if (source.hasNumAssociationTimeout()) {
1302                 target.accumulate(CNT_ASSOCIATION_TIMEOUT, source.getNumAssociationTimeout());
1303             }
1304             if (source.hasNumAuthenticationFailure()) {
1305                 target.accumulate(CNT_AUTHENTICATION_FAILURE, source.getNumAuthenticationFailure());
1306             }
1307         }
1308     }
1309 
1310     // Codes for various connection related counts
1311     public static final int CNT_INVALID = -1;
1312     public static final int CNT_CONNECTION_ATTEMPT = 0;
1313     public static final int CNT_CONNECTION_FAILURE = 1;
1314     public static final int CNT_CONNECTION_DURATION_SEC = 2;
1315     public static final int CNT_ASSOCIATION_REJECTION = 3;
1316     public static final int CNT_ASSOCIATION_TIMEOUT = 4;
1317     public static final int CNT_AUTHENTICATION_FAILURE = 5;
1318     public static final int CNT_SHORT_CONNECTION_NONLOCAL = 6;
1319     public static final int CNT_DISCONNECTION_NONLOCAL = 7;
1320     public static final int CNT_DISCONNECTION = 8;
1321     public static final int CNT_CONSECUTIVE_CONNECTION_FAILURE = 9;
1322     // Constant being used to keep track of how many counter there are.
1323     public static final int NUMBER_CONNECTION_CNT_CODE = 10;
1324     private static final String[] CONNECTION_CNT_NAME = {
1325         " ConnectAttempt: ",
1326         " ConnectFailure: ",
1327         " ConnectDurSec: ",
1328         " AssocRej: ",
1329         " AssocTimeout: ",
1330         " AuthFailure: ",
1331         " ShortDiscNonlocal: ",
1332         " DisconnectNonlocal: ",
1333         " Disconnect: ",
1334         " ConsecutiveConnectFailure: "
1335     };
1336 
1337     @IntDef(prefix = { "CNT_" }, value = {
1338         CNT_CONNECTION_ATTEMPT,
1339         CNT_CONNECTION_FAILURE,
1340         CNT_CONNECTION_DURATION_SEC,
1341         CNT_ASSOCIATION_REJECTION,
1342         CNT_ASSOCIATION_TIMEOUT,
1343         CNT_AUTHENTICATION_FAILURE,
1344         CNT_SHORT_CONNECTION_NONLOCAL,
1345         CNT_DISCONNECTION_NONLOCAL,
1346         CNT_DISCONNECTION,
1347         CNT_CONSECUTIVE_CONNECTION_FAILURE
1348     })
1349     @Retention(RetentionPolicy.SOURCE)
1350     public @interface ConnectionCountCode {}
1351 
1352     /**
1353      * A class maintaining the connection related statistics of a Wifi network.
1354      */
1355     public static class NetworkConnectionStats {
1356         private final int[] mCount = new int[NUMBER_CONNECTION_CNT_CODE];
1357         private int mRecentCountCode = CNT_INVALID;
1358         /**
1359          * Copy all values
1360          * @param src is the source of copy
1361          */
copy(NetworkConnectionStats src)1362         public void copy(NetworkConnectionStats src) {
1363             for (int i = 0; i < NUMBER_CONNECTION_CNT_CODE; i++) {
1364                 mCount[i] = src.getCount(i);
1365             }
1366             mRecentCountCode = src.mRecentCountCode;
1367         }
1368 
1369         /**
1370          * Clear all counters
1371          */
clear()1372         public void clear() {
1373             for (int i = 0; i < NUMBER_CONNECTION_CNT_CODE; i++) {
1374                 mCount[i] = 0;
1375             }
1376             mRecentCountCode = CNT_INVALID;
1377         }
1378 
1379         /**
1380          * Get counter value
1381          * @param countCode is the selected counter
1382          * @return the value of selected counter
1383          */
getCount(@onnectionCountCode int countCode)1384         public int getCount(@ConnectionCountCode int countCode) {
1385             return mCount[countCode];
1386         }
1387 
1388         /**
1389          * Clear counter value
1390          * @param countCode is the selected counter to be cleared
1391          */
clearCount(@onnectionCountCode int countCode)1392         public void clearCount(@ConnectionCountCode int countCode) {
1393             mCount[countCode] = 0;
1394         }
1395 
1396         /**
1397          * Increment count value by 1
1398          * @param countCode is the selected counter
1399          */
incrementCount(@onnectionCountCode int countCode)1400         public void incrementCount(@ConnectionCountCode int countCode) {
1401             mCount[countCode]++;
1402             mRecentCountCode = countCode;
1403         }
1404 
1405         /**
1406          * Got the recent incremented count code
1407          */
getRecentCountCode()1408         public int getRecentCountCode() {
1409             return mRecentCountCode;
1410         }
1411 
1412         /**
1413          * Decrement count value by 1
1414          * @param countCode is the selected counter
1415          */
decrementCount(@onnectionCountCode int countCode)1416         public void decrementCount(@ConnectionCountCode int countCode) {
1417             mCount[countCode]--;
1418         }
1419 
1420         /**
1421          * Add and accumulate the selected counter
1422          * @param countCode is the selected counter
1423          * @param cnt is the value to be added to the counter
1424          */
accumulate(@onnectionCountCode int countCode, int cnt)1425         public void accumulate(@ConnectionCountCode int countCode, int cnt) {
1426             mCount[countCode] += cnt;
1427         }
1428 
1429         /**
1430          * Accumulate daily stats to historical data
1431          * @param recentStats are the raw daily counts
1432          */
accumulateAll(NetworkConnectionStats recentStats)1433         public void accumulateAll(NetworkConnectionStats recentStats) {
1434             // 32-bit counter in second can support connection duration up to 68 years.
1435             // Similarly 32-bit counter can support up to continuous connection attempt
1436             // up to 68 years with one attempt per second.
1437             for (int i = 0; i < NUMBER_CONNECTION_CNT_CODE; i++) {
1438                 mCount[i] += recentStats.getCount(i);
1439             }
1440         }
1441 
1442         @Override
toString()1443         public String toString() {
1444             StringBuilder sb = new StringBuilder();
1445             for (int i = 0; i < NUMBER_CONNECTION_CNT_CODE; i++) {
1446                 sb.append(CONNECTION_CNT_NAME[i]);
1447                 sb.append(mCount[i]);
1448             }
1449             return sb.toString();
1450         }
1451     }
1452     /**
1453      * A base class dealing with common operations of MemoryStore.
1454      */
1455     public static class MemoryStoreAccessBase {
1456         private final String mL2Key;
1457         private final long mHash;
1458         private static final String TAG = "WifiMemoryStoreAccessBase";
1459         private final AtomicReference<byte[]> mPendingReadFromStore = new AtomicReference<>();
MemoryStoreAccessBase(long hash)1460         MemoryStoreAccessBase(long hash) {
1461             mHash = hash;
1462             mL2Key = l2KeyFromLong();
1463         }
getL2Key()1464         String getL2Key() {
1465             return mL2Key;
1466         }
1467 
l2KeyFromLong()1468         private String l2KeyFromLong() {
1469             return "W" + Long.toHexString(mHash);
1470         }
1471 
1472         /**
1473          * Callback function when MemoryStore read is done
1474          * @param serialized is the readback value
1475          */
readBackListener(byte[] serialized)1476         void readBackListener(byte[] serialized) {
1477             if (serialized == null) return;
1478             byte[] old = mPendingReadFromStore.getAndSet(serialized);
1479             if (old != null) {
1480                 Log.e(TAG, "More answers than we expected!");
1481             }
1482         }
1483 
1484         /**
1485          * Handles (when convenient) the arrival of previously stored data.
1486          *
1487          * The response from IpMemoryStore arrives on a different thread, so we
1488          * defer handling it until here, when we're on our favorite thread and
1489          * in a good position to deal with it. We may have already collected some
1490          * data before now, so we need to be prepared to merge the new and old together.
1491          */
finishPendingReadBytes()1492         byte[] finishPendingReadBytes() {
1493             return mPendingReadFromStore.getAndSet(null);
1494         }
1495 
idFromLong()1496         int idFromLong() {
1497             return (int) mHash & 0x7fffffff;
1498         }
1499     }
1500 
logd(String string)1501     private void logd(String string) {
1502         if (mVerboseLoggingEnabled) {
1503             Log.d(TAG, string);
1504         }
1505     }
1506     // Returned by lookupBssid when the BSSID is not available,
1507     // for instance when we are not associated.
1508     private final PerBssid mDummyPerBssid;
1509 
1510     private final Map<MacAddress, PerBssid> mApForBssid = new ArrayMap<>();
1511     private int mApForBssidTargetSize = TARGET_IN_MEMORY_ENTRIES;
1512     private int mApForBssidReferenced = 0;
1513 
1514     // TODO should be private, but WifiCandidates needs it
lookupBssid(String ssid, String bssid)1515     @NonNull PerBssid lookupBssid(String ssid, String bssid) {
1516         MacAddress mac;
1517         if (ssid == null || WifiManager.UNKNOWN_SSID.equals(ssid) || bssid == null) {
1518             return mDummyPerBssid;
1519         }
1520         try {
1521             mac = MacAddress.fromString(bssid);
1522         } catch (IllegalArgumentException e) {
1523             return mDummyPerBssid;
1524         }
1525         if (mac.equals(mDummyPerBssid.bssid)) {
1526             return mDummyPerBssid;
1527         }
1528         PerBssid ans = mApForBssid.get(mac);
1529         if (ans == null || !ans.ssid.equals(ssid)) {
1530             ans = new PerBssid(ssid, mac);
1531             PerBssid old = mApForBssid.put(mac, ans);
1532             if (old != null) {
1533                 Log.i(TAG, "Discarding stats for score card (ssid changed) ID: " + old.id);
1534                 if (old.referenced) mApForBssidReferenced--;
1535             }
1536             requestReadBssid(ans);
1537         }
1538         if (!ans.referenced) {
1539             ans.referenced = true;
1540             mApForBssidReferenced++;
1541             clean();
1542         }
1543         return ans;
1544     }
1545 
requestReadBssid(final PerBssid perBssid)1546     private void requestReadBssid(final PerBssid perBssid) {
1547         if (mMemoryStore != null) {
1548             mMemoryStore.read(perBssid.getL2Key(), PER_BSSID_DATA_NAME,
1549                     (value) -> perBssid.readBackListener(value));
1550         }
1551     }
1552 
requestReadForAllChanged()1553     private void requestReadForAllChanged() {
1554         for (PerBssid perBssid : mApForBssid.values()) {
1555             if (perBssid.changed) {
1556                 requestReadBssid(perBssid);
1557             }
1558         }
1559     }
1560 
1561     // Returned by lookupNetwork when the network is not available,
1562     // for instance when we are not associated.
1563     private final PerNetwork mDummyPerNetwork;
1564     private final Map<String, PerNetwork> mApForNetwork = new ArrayMap<>();
lookupNetwork(String ssid)1565     PerNetwork lookupNetwork(String ssid) {
1566         if (ssid == null || WifiManager.UNKNOWN_SSID.equals(ssid)) {
1567             return mDummyPerNetwork;
1568         }
1569 
1570         PerNetwork ans = mApForNetwork.get(ssid);
1571         if (ans == null) {
1572             ans = new PerNetwork(ssid);
1573             mApForNetwork.put(ssid, ans);
1574             requestReadNetwork(ans);
1575         }
1576         return ans;
1577     }
1578 
1579     /**
1580      * Remove network from cache and memory store
1581      * @param ssid is the network SSID
1582      */
removeNetwork(String ssid)1583     public void removeNetwork(String ssid) {
1584         if (ssid == null || WifiManager.UNKNOWN_SSID.equals(ssid)) {
1585             return;
1586         }
1587         mApForNetwork.remove(ssid);
1588         mApForBssid.entrySet().removeIf(entry -> ssid.equals(entry.getValue().ssid));
1589         if (mMemoryStore == null) return;
1590         mMemoryStore.removeCluster(groupHintFromSsid(ssid));
1591     }
1592 
requestReadNetwork(final PerNetwork perNetwork)1593     void requestReadNetwork(final PerNetwork perNetwork) {
1594         if (mMemoryStore != null) {
1595             mMemoryStore.read(perNetwork.getL2Key(), PER_NETWORK_DATA_NAME,
1596                     (value) -> perNetwork.readBackListener(value));
1597         }
1598     }
1599 
1600     /**
1601      * Issues write requests for all changed entries.
1602      *
1603      * This should be called from time to time to save the state to persistent
1604      * storage. Since we always check internal state first, this does not need
1605      * to be called very often, but it should be called before shutdown.
1606      *
1607      * @returns number of writes issued.
1608      */
doWrites()1609     public int doWrites() {
1610         return doWritesBssid() + doWritesNetwork();
1611     }
1612 
doWritesBssid()1613     private int doWritesBssid() {
1614         if (mMemoryStore == null) return 0;
1615         int count = 0;
1616         int bytes = 0;
1617         for (PerBssid perBssid : mApForBssid.values()) {
1618             if (perBssid.changed) {
1619                 perBssid.finishPendingRead();
1620                 byte[] serialized = perBssid.toAccessPoint(/* No BSSID */ true).toByteArray();
1621                 mMemoryStore.setCluster(perBssid.getL2Key(), groupHintFromSsid(perBssid.ssid));
1622                 mMemoryStore.write(perBssid.getL2Key(), PER_BSSID_DATA_NAME, serialized);
1623 
1624                 perBssid.changed = false;
1625                 count++;
1626                 bytes += serialized.length;
1627             }
1628         }
1629         if (mVerboseLoggingEnabled && count > 0) {
1630             Log.v(TAG, "Write count: " + count + ", bytes: " + bytes);
1631         }
1632         return count;
1633     }
1634 
doWritesNetwork()1635     private int doWritesNetwork() {
1636         if (mMemoryStore == null) return 0;
1637         int count = 0;
1638         int bytes = 0;
1639         for (PerNetwork perNetwork : mApForNetwork.values()) {
1640             if (perNetwork.changed) {
1641                 perNetwork.finishPendingRead();
1642                 byte[] serialized = perNetwork.toNetworkStats().toByteArray();
1643                 mMemoryStore.setCluster(perNetwork.getL2Key(), groupHintFromSsid(perNetwork.ssid));
1644                 mMemoryStore.write(perNetwork.getL2Key(), PER_NETWORK_DATA_NAME, serialized);
1645                 perNetwork.changed = false;
1646                 count++;
1647                 bytes += serialized.length;
1648             }
1649         }
1650         if (mVerboseLoggingEnabled && count > 0) {
1651             Log.v(TAG, "Write count: " + count + ", bytes: " + bytes);
1652         }
1653         return count;
1654     }
1655 
1656     /**
1657      * Evicts older entries from memory.
1658      *
1659      * This uses an approximate least-recently-used method. When the number of
1660      * referenced entries exceeds the target value, any items that have not been
1661      * referenced since the last round are evicted, and the remaining entries
1662      * are marked as unreferenced. The total count varies between the target
1663      * value and twice the target value.
1664      */
clean()1665     private void clean() {
1666         if (mMemoryStore == null) return;
1667         if (mApForBssidReferenced >= mApForBssidTargetSize) {
1668             doWritesBssid(); // Do not want to evict changed items
1669             // Evict the unreferenced ones, and clear all the referenced bits for the next round.
1670             Iterator<Map.Entry<MacAddress, PerBssid>> it = mApForBssid.entrySet().iterator();
1671             while (it.hasNext()) {
1672                 PerBssid perBssid = it.next().getValue();
1673                 if (perBssid.referenced) {
1674                     perBssid.referenced = false;
1675                 } else {
1676                     it.remove();
1677                     if (mVerboseLoggingEnabled) Log.v(TAG, "Evict " + perBssid.id);
1678                 }
1679             }
1680             mApForBssidReferenced = 0;
1681         }
1682     }
1683 
1684     /**
1685      * Compute a hash value with the given SSID and MAC address
1686      * @param ssid is the network SSID
1687      * @param mac is the network MAC address
1688      * @param l2KeySeed is the seed for hash generation
1689      * @return
1690      */
computeHashLong(String ssid, MacAddress mac, String l2KeySeed)1691     public static long computeHashLong(String ssid, MacAddress mac, String l2KeySeed) {
1692         byte[][] parts = {
1693                 // Our seed keeps the L2Keys specific to this device
1694                 l2KeySeed.getBytes(),
1695                 // ssid is either quoted utf8 or hex-encoded bytes; turn it into plain bytes.
1696                 NativeUtil.byteArrayFromArrayList(NativeUtil.decodeSsid(ssid)),
1697                 // And the BSSID
1698                 mac.toByteArray()
1699         };
1700         // Assemble the parts into one, with single-byte lengths before each.
1701         int n = 0;
1702         for (int i = 0; i < parts.length; i++) {
1703             n += 1 + parts[i].length;
1704         }
1705         byte[] mashed = new byte[n];
1706         int p = 0;
1707         for (int i = 0; i < parts.length; i++) {
1708             byte[] part = parts[i];
1709             mashed[p++] = (byte) part.length;
1710             for (int j = 0; j < part.length; j++) {
1711                 mashed[p++] = part[j];
1712             }
1713         }
1714         // Finally, turn that into a long
1715         MessageDigest md;
1716         try {
1717             md = MessageDigest.getInstance("SHA-256");
1718         } catch (NoSuchAlgorithmException e) {
1719             Log.e(TAG, "SHA-256 not supported.");
1720             return 0;
1721         }
1722         ByteBuffer buffer = ByteBuffer.wrap(md.digest(mashed));
1723         return buffer.getLong();
1724     }
1725 
groupHintFromLong(long hash)1726     private static String groupHintFromLong(long hash) {
1727         return "G" + Long.toHexString(hash);
1728     }
1729 
1730     @VisibleForTesting
fetchByBssid(MacAddress mac)1731     PerBssid fetchByBssid(MacAddress mac) {
1732         return mApForBssid.get(mac);
1733     }
1734 
1735     @VisibleForTesting
fetchByNetwork(String ssid)1736     PerNetwork fetchByNetwork(String ssid) {
1737         return mApForNetwork.get(ssid);
1738     }
1739 
1740     @VisibleForTesting
perBssidFromAccessPoint(String ssid, AccessPoint ap)1741     PerBssid perBssidFromAccessPoint(String ssid, AccessPoint ap) {
1742         MacAddress bssid = MacAddress.fromBytes(ap.getBssid().toByteArray());
1743         return new PerBssid(ssid, bssid).merge(ap);
1744     }
1745 
1746     @VisibleForTesting
perNetworkFromNetworkStats(String ssid, NetworkStats ns)1747     PerNetwork perNetworkFromNetworkStats(String ssid, NetworkStats ns) {
1748         return new PerNetwork(ssid).mergeNetworkStatsFromMemory(ns);
1749     }
1750 
1751     final class PerSignal {
1752         public final Event event;
1753         public final int frequency;
1754         public final PerUnivariateStatistic rssi;
1755         public final PerUnivariateStatistic linkspeed;
1756         @Nullable public final PerUnivariateStatistic elapsedMs;
PerSignal(Event event, int frequency)1757         PerSignal(Event event, int frequency) {
1758             this.event = event;
1759             this.frequency = frequency;
1760             switch (event) {
1761                 case SIGNAL_POLL:
1762                 case IP_CONFIGURATION_SUCCESS:
1763                 case IP_REACHABILITY_LOST:
1764                     this.rssi = new PerUnivariateStatistic(RSSI_BUCKETS);
1765                     break;
1766                 default:
1767                     this.rssi = new PerUnivariateStatistic();
1768                     break;
1769             }
1770             this.linkspeed = new PerUnivariateStatistic();
1771             switch (event) {
1772                 case FIRST_POLL_AFTER_CONNECTION:
1773                 case IP_CONFIGURATION_SUCCESS:
1774                 case VALIDATION_SUCCESS:
1775                 case CONNECTION_FAILURE:
1776                 case DISCONNECTION:
1777                 case WIFI_DISABLED:
1778                 case ROAM_FAILURE:
1779                     this.elapsedMs = new PerUnivariateStatistic();
1780                     break;
1781                 default:
1782                     this.elapsedMs = null;
1783                     break;
1784             }
1785         }
merge(Signal signal)1786         PerSignal merge(Signal signal) {
1787             Preconditions.checkArgument(event == signal.getEvent());
1788             Preconditions.checkArgument(frequency == signal.getFrequency());
1789             rssi.merge(signal.getRssi());
1790             linkspeed.merge(signal.getLinkspeed());
1791             if (elapsedMs != null && signal.hasElapsedMs()) {
1792                 elapsedMs.merge(signal.getElapsedMs());
1793             }
1794             return this;
1795         }
toSignal()1796         Signal toSignal() {
1797             Signal.Builder builder = Signal.newBuilder();
1798             builder.setEvent(event)
1799                     .setFrequency(frequency)
1800                     .setRssi(rssi.toUnivariateStatistic())
1801                     .setLinkspeed(linkspeed.toUnivariateStatistic());
1802             if (elapsedMs != null) {
1803                 builder.setElapsedMs(elapsedMs.toUnivariateStatistic());
1804             }
1805             if (rssi.intHistogram != null
1806                     && rssi.intHistogram.numNonEmptyBuckets() > 0) {
1807                 logd("Histogram " + event + " RSSI" + rssi.intHistogram);
1808             }
1809             return builder.build();
1810         }
1811     }
1812 
1813     final class PerUnivariateStatistic {
1814         public long count = 0;
1815         public double sum = 0.0;
1816         public double sumOfSquares = 0.0;
1817         public double minValue = Double.POSITIVE_INFINITY;
1818         public double maxValue = Double.NEGATIVE_INFINITY;
1819         public double historicalMean = 0.0;
1820         public double historicalVariance = Double.POSITIVE_INFINITY;
1821         public IntHistogram intHistogram = null;
PerUnivariateStatistic()1822         PerUnivariateStatistic() {}
PerUnivariateStatistic(int[] bucketBoundaries)1823         PerUnivariateStatistic(int[] bucketBoundaries) {
1824             intHistogram = new IntHistogram(bucketBoundaries);
1825         }
update(double value)1826         void update(double value) {
1827             count++;
1828             sum += value;
1829             sumOfSquares += value * value;
1830             minValue = Math.min(minValue, value);
1831             maxValue = Math.max(maxValue, value);
1832             if (intHistogram != null) {
1833                 intHistogram.add(Math.round((float) value), 1);
1834             }
1835         }
age()1836         void age() {
1837             //TODO  Fold the current stats into the historical stats
1838         }
merge(UnivariateStatistic stats)1839         void merge(UnivariateStatistic stats) {
1840             if (stats.hasCount()) {
1841                 count += stats.getCount();
1842                 sum += stats.getSum();
1843                 sumOfSquares += stats.getSumOfSquares();
1844             }
1845             if (stats.hasMinValue()) {
1846                 minValue = Math.min(minValue, stats.getMinValue());
1847             }
1848             if (stats.hasMaxValue()) {
1849                 maxValue = Math.max(maxValue, stats.getMaxValue());
1850             }
1851             if (stats.hasHistoricalVariance()) {
1852                 if (historicalVariance < Double.POSITIVE_INFINITY) {
1853                     // Combine the estimates; c.f.
1854                     // Maybeck, Stochasic Models, Estimation, and Control, Vol. 1
1855                     // equations (1-3) and (1-4)
1856                     double numer1 = stats.getHistoricalVariance();
1857                     double numer2 = historicalVariance;
1858                     double denom = numer1 + numer2;
1859                     historicalMean = (numer1 * historicalMean
1860                                     + numer2 * stats.getHistoricalMean())
1861                                     / denom;
1862                     historicalVariance = numer1 * numer2 / denom;
1863                 } else {
1864                     historicalMean = stats.getHistoricalMean();
1865                     historicalVariance = stats.getHistoricalVariance();
1866                 }
1867             }
1868             if (intHistogram != null) {
1869                 for (HistogramBucket bucket : stats.getBucketsList()) {
1870                     long low = bucket.getLow();
1871                     long count = bucket.getNumber();
1872                     if (low != (int) low || count != (int) count || count < 0) {
1873                         Log.e(TAG, "Found corrupted histogram! Clearing.");
1874                         intHistogram.clear();
1875                         break;
1876                     }
1877                     intHistogram.add((int) low, (int) count);
1878                 }
1879             }
1880         }
toUnivariateStatistic()1881         UnivariateStatistic toUnivariateStatistic() {
1882             UnivariateStatistic.Builder builder = UnivariateStatistic.newBuilder();
1883             if (count != 0) {
1884                 builder.setCount(count)
1885                         .setSum(sum)
1886                         .setSumOfSquares(sumOfSquares)
1887                         .setMinValue(minValue)
1888                         .setMaxValue(maxValue);
1889             }
1890             if (historicalVariance < Double.POSITIVE_INFINITY) {
1891                 builder.setHistoricalMean(historicalMean)
1892                         .setHistoricalVariance(historicalVariance);
1893             }
1894             if (mPersistentHistograms
1895                     && intHistogram != null && intHistogram.numNonEmptyBuckets() > 0) {
1896                 for (IntHistogram.Bucket b : intHistogram) {
1897                     if (b.count == 0) continue;
1898                     builder.addBuckets(
1899                             HistogramBucket.newBuilder().setLow(b.start).setNumber(b.count));
1900                 }
1901             }
1902             return builder.build();
1903         }
1904     }
1905 
1906     /**
1907      * Returns the current scorecard in the form of a protobuf com_android_server_wifi.NetworkList
1908      *
1909      * Synchronization is the caller's responsibility.
1910      *
1911      * @param obfuscate - if true, ssids and bssids are omitted (short id only)
1912      */
getNetworkListByteArray(boolean obfuscate)1913     public byte[] getNetworkListByteArray(boolean obfuscate) {
1914         // These are really grouped by ssid, ignoring the security type.
1915         Map<String, Network.Builder> networks = new ArrayMap<>();
1916         for (PerBssid perBssid: mApForBssid.values()) {
1917             String key = perBssid.ssid;
1918             Network.Builder network = networks.get(key);
1919             if (network == null) {
1920                 network = Network.newBuilder();
1921                 networks.put(key, network);
1922                 if (!obfuscate) {
1923                     network.setSsid(perBssid.ssid);
1924                 }
1925             }
1926             if (perBssid.mNetworkAgentId >= network.getNetworkAgentId()) {
1927                 network.setNetworkAgentId(perBssid.mNetworkAgentId);
1928             }
1929             if (perBssid.mNetworkConfigId >= network.getNetworkConfigId()) {
1930                 network.setNetworkConfigId(perBssid.mNetworkConfigId);
1931             }
1932             network.addAccessPoints(perBssid.toAccessPoint(obfuscate));
1933         }
1934         for (PerNetwork perNetwork: mApForNetwork.values()) {
1935             String key = perNetwork.ssid;
1936             Network.Builder network = networks.get(key);
1937             if (network != null) {
1938                 network.setNetworkStats(perNetwork.toNetworkStats());
1939             }
1940         }
1941         NetworkList.Builder builder = NetworkList.newBuilder();
1942         for (Network.Builder network: networks.values()) {
1943             builder.addNetworks(network);
1944         }
1945         return builder.build().toByteArray();
1946     }
1947 
1948     /**
1949      * Returns the current scorecard as a base64-encoded protobuf
1950      *
1951      * Synchronization is the caller's responsibility.
1952      *
1953      * @param obfuscate - if true, bssids are omitted (short id only)
1954      */
getNetworkListBase64(boolean obfuscate)1955     public String getNetworkListBase64(boolean obfuscate) {
1956         byte[] raw = getNetworkListByteArray(obfuscate);
1957         return Base64.encodeToString(raw, Base64.DEFAULT);
1958     }
1959 
1960     /**
1961      * Clears the internal state.
1962      *
1963      * This is called in response to a factoryReset call from Settings.
1964      * The memory store will be called after we are called, to wipe the stable
1965      * storage as well. Since we will have just removed all of our networks,
1966      * it is very unlikely that we're connected, or will connect immediately.
1967      * Any in-flight reads will land in the objects we are dropping here, and
1968      * the memory store should drop the in-flight writes. Ideally we would
1969      * avoid issuing reads until we were sure that the memory store had
1970      * received the factoryReset.
1971      */
clear()1972     public void clear() {
1973         mApForBssid.clear();
1974         mApForNetwork.clear();
1975         resetConnectionStateInternal(false);
1976     }
1977 }
1978