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 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.net.MacAddress;
25 import android.net.wifi.SupplicantState;
26 import android.net.wifi.WifiSsid;
27 import android.util.ArrayMap;
28 import android.util.Base64;
29 import android.util.Log;
30 import android.util.Pair;
31 
32 import com.android.internal.annotations.VisibleForTesting;
33 import com.android.internal.util.Preconditions;
34 import com.android.server.wifi.WifiScoreCardProto.AccessPoint;
35 import com.android.server.wifi.WifiScoreCardProto.Event;
36 import com.android.server.wifi.WifiScoreCardProto.Network;
37 import com.android.server.wifi.WifiScoreCardProto.NetworkList;
38 import com.android.server.wifi.WifiScoreCardProto.SecurityType;
39 import com.android.server.wifi.WifiScoreCardProto.Signal;
40 import com.android.server.wifi.WifiScoreCardProto.UnivariateStatistic;
41 import com.android.server.wifi.util.NativeUtil;
42 
43 import com.google.protobuf.ByteString;
44 import com.google.protobuf.InvalidProtocolBufferException;
45 
46 import java.nio.ByteBuffer;
47 import java.security.MessageDigest;
48 import java.security.NoSuchAlgorithmException;
49 import java.util.Map;
50 import java.util.Objects;
51 import java.util.concurrent.atomic.AtomicReference;
52 
53 import javax.annotation.concurrent.NotThreadSafe;
54 
55 /**
56  * Retains statistical information about the performance of various
57  * access points, as experienced by this device.
58  *
59  * The purpose is to better inform future network selection and switching
60  * by this device.
61  */
62 @NotThreadSafe
63 public class WifiScoreCard {
64 
65     public static final String DUMP_ARG = "WifiScoreCard";
66 
67     private static final String TAG = "WifiScoreCard";
68     private static final boolean DBG = false;
69 
70     private final Clock mClock;
71     private final String mL2KeySeed;
72     private MemoryStore mMemoryStore;
73 
74     /** Our view of the memory store */
75     public interface MemoryStore {
76         /** Requests a read, with asynchronous reply */
read(String key, BlobListener blobListener)77         void read(String key, BlobListener blobListener);
78         /** Requests a write, does not wait for completion */
write(String key, byte[] value)79         void write(String key, byte[] value);
80     }
81     /** Asynchronous response to a read request */
82     public interface BlobListener {
83         /** Provides the previously stored value, or null if none */
onBlobRetrieved(@ullable byte[] value)84         void onBlobRetrieved(@Nullable byte[] value);
85     }
86 
87     /**
88      * Installs a memory store.
89      *
90      * Normally this happens just once, shortly after we start. But wifi can
91      * come up before the disk is ready, and we might not yet have a valid wall
92      * clock when we start up, so we need to be prepared to begin recording data
93      * even if the MemoryStore is not yet available.
94      *
95      * When the store is installed for the first time, we want to merge any
96      * recently recorded data together with data already in the store. But if
97      * the store restarts and has to be reinstalled, we don't want to do
98      * this merge, because that would risk double-counting the old data.
99      *
100      */
installMemoryStore(@onNull MemoryStore memoryStore)101     public void installMemoryStore(@NonNull MemoryStore memoryStore) {
102         Preconditions.checkNotNull(memoryStore);
103         if (mMemoryStore == null) {
104             mMemoryStore = memoryStore;
105             Log.i(TAG, "Installing MemoryStore");
106             requestReadForAllChanged();
107         } else {
108             mMemoryStore = memoryStore;
109             Log.e(TAG, "Reinstalling MemoryStore");
110             // Our caller will call doWrites() eventually, so nothing more to do here.
111         }
112     }
113 
114     /**
115      * Timestamp of the start of the most recent connection attempt.
116      *
117      * Based on mClock.getElapsedSinceBootMillis().
118      *
119      * This is for calculating the time to connect and the duration of the connection.
120      * Any negative value means we are not currently connected.
121      */
122     private long mTsConnectionAttemptStart = TS_NONE;
123     private static final long TS_NONE = -1;
124 
125     /**
126      * Timestamp captured when we find out about a firmware roam
127      */
128     private long mTsRoam = TS_NONE;
129 
130     /**
131      * Becomes true the first time we see a poll with a valid RSSI in a connection
132      */
133     private boolean mPolled = false;
134 
135     /**
136      * Records validation success for the current connection.
137      *
138      * We want to gather statistics only on the first success.
139      */
140     private boolean mValidated = false;
141 
142     /**
143      * A note to ourself that we are attempting a network switch
144      */
145     private boolean mAttemptingSwitch = false;
146 
147     /**
148      * @param clock is the time source
149      * @param l2KeySeed is for making our L2Keys usable only on this device
150      */
WifiScoreCard(Clock clock, String l2KeySeed)151     public WifiScoreCard(Clock clock, String l2KeySeed) {
152         mClock = clock;
153         mL2KeySeed = l2KeySeed;
154         mDummyPerBssid = new PerBssid("", MacAddress.fromString(DEFAULT_MAC_ADDRESS));
155     }
156 
157     /**
158      * Gets the L2Key and GroupHint associated with the connection.
159      */
getL2KeyAndGroupHint(ExtendedWifiInfo wifiInfo)160     public @NonNull Pair<String, String> getL2KeyAndGroupHint(ExtendedWifiInfo wifiInfo) {
161         PerBssid perBssid = lookupBssid(wifiInfo.getSSID(), wifiInfo.getBSSID());
162         if (perBssid == mDummyPerBssid) {
163             return new Pair<>(null, null);
164         }
165         final long groupIdHash = computeHashLong(perBssid.ssid, mDummyPerBssid.bssid);
166         return new Pair<>(perBssid.l2Key, groupHintFromLong(groupIdHash));
167     }
168 
169     /**
170      * Resets the connection state
171      */
resetConnectionState()172     public void resetConnectionState() {
173         if (DBG && mTsConnectionAttemptStart > TS_NONE && !mAttemptingSwitch) {
174             Log.v(TAG, "resetConnectionState", new Exception());
175         }
176         resetConnectionStateInternal(true);
177     }
178 
179     /**
180      * @param calledFromResetConnectionState says the call is from outside the class,
181      *        indicating that we need to resepect the value of mAttemptingSwitch.
182      */
resetConnectionStateInternal(boolean calledFromResetConnectionState)183     private void resetConnectionStateInternal(boolean calledFromResetConnectionState) {
184         if (!calledFromResetConnectionState) {
185             mAttemptingSwitch = false;
186         }
187         if (!mAttemptingSwitch) {
188             mTsConnectionAttemptStart = TS_NONE;
189         }
190         mTsRoam = TS_NONE;
191         mPolled = false;
192         mValidated = false;
193     }
194 
195     /**
196      * Updates the score card using relevant parts of WifiInfo
197      *
198      * @param wifiInfo object holding relevant values.
199      */
update(WifiScoreCardProto.Event event, ExtendedWifiInfo wifiInfo)200     private void update(WifiScoreCardProto.Event event, ExtendedWifiInfo wifiInfo) {
201         PerBssid perBssid = lookupBssid(wifiInfo.getSSID(), wifiInfo.getBSSID());
202         perBssid.updateEventStats(event,
203                 wifiInfo.getFrequency(),
204                 wifiInfo.getRssi(),
205                 wifiInfo.getLinkSpeed());
206         perBssid.setNetworkConfigId(wifiInfo.getNetworkId());
207 
208         if (DBG) Log.d(TAG, event.toString() + " ID: " + perBssid.id + " " + wifiInfo);
209     }
210 
211     /**
212      * Updates the score card after a signal poll
213      *
214      * @param wifiInfo object holding relevant values
215      */
noteSignalPoll(ExtendedWifiInfo wifiInfo)216     public void noteSignalPoll(ExtendedWifiInfo wifiInfo) {
217         if (!mPolled && wifiInfo.getRssi() != INVALID_RSSI) {
218             update(Event.FIRST_POLL_AFTER_CONNECTION, wifiInfo);
219             mPolled = true;
220         }
221         update(Event.SIGNAL_POLL, wifiInfo);
222         if (mTsRoam > TS_NONE && wifiInfo.getRssi() != INVALID_RSSI) {
223             long duration = mClock.getElapsedSinceBootMillis() - mTsRoam;
224             if (duration >= SUCCESS_MILLIS_SINCE_ROAM) {
225                 update(Event.ROAM_SUCCESS, wifiInfo);
226                 mTsRoam = TS_NONE;
227                 doWrites();
228             }
229         }
230     }
231     /** Wait a few seconds before considering the roam successful */
232     private static final long SUCCESS_MILLIS_SINCE_ROAM = 4_000;
233 
234     /**
235      * Updates the score card after IP configuration
236      *
237      * @param wifiInfo object holding relevant values
238      */
noteIpConfiguration(ExtendedWifiInfo wifiInfo)239     public void noteIpConfiguration(ExtendedWifiInfo wifiInfo) {
240         update(Event.IP_CONFIGURATION_SUCCESS, wifiInfo);
241         mAttemptingSwitch = false;
242         doWrites();
243     }
244 
245     /**
246      * Updates the score card after network validation success.
247      *
248      * @param wifiInfo object holding relevant values
249      */
noteValidationSuccess(ExtendedWifiInfo wifiInfo)250     public void noteValidationSuccess(ExtendedWifiInfo wifiInfo) {
251         if (mValidated) return; // Only once per connection
252         update(Event.VALIDATION_SUCCESS, wifiInfo);
253         mValidated = true;
254     }
255 
256     /**
257      * Records the start of a connection attempt
258      *
259      * @param wifiInfo may have state about an existing connection
260      */
noteConnectionAttempt(ExtendedWifiInfo wifiInfo)261     public void noteConnectionAttempt(ExtendedWifiInfo wifiInfo) {
262         // We may or may not be currently connected. If not, simply record the start.
263         // But if we are connected, wrap up the old one first.
264         if (mTsConnectionAttemptStart > TS_NONE) {
265             if (mPolled) {
266                 update(Event.LAST_POLL_BEFORE_SWITCH, wifiInfo);
267             }
268             mAttemptingSwitch = true;
269         }
270         mTsConnectionAttemptStart = mClock.getElapsedSinceBootMillis();
271         mPolled = false;
272 
273         if (DBG) Log.d(TAG, "CONNECTION_ATTEMPT" + (mAttemptingSwitch ? " X " : " ") + wifiInfo);
274     }
275 
276     /**
277      * Records a newly assigned NetworkAgent netId.
278      */
noteNetworkAgentCreated(ExtendedWifiInfo wifiInfo, int networkAgentId)279     public void noteNetworkAgentCreated(ExtendedWifiInfo wifiInfo, int networkAgentId) {
280         PerBssid perBssid = lookupBssid(wifiInfo.getSSID(), wifiInfo.getBSSID());
281         if (DBG) {
282             Log.d(TAG, "NETWORK_AGENT_ID: " + networkAgentId + " ID: " + perBssid.id);
283         }
284         perBssid.mNetworkAgentId = networkAgentId;
285     }
286 
287     /**
288      * Updates the score card after a failed connection attempt
289      *
290      * @param wifiInfo object holding relevant values
291      */
noteConnectionFailure(ExtendedWifiInfo wifiInfo, int codeMetrics, int codeMetricsProto)292     public void noteConnectionFailure(ExtendedWifiInfo wifiInfo,
293                 int codeMetrics, int codeMetricsProto) {
294         if (DBG) {
295             Log.d(TAG, "noteConnectionFailure(..., " + codeMetrics + ", " + codeMetricsProto + ")");
296         }
297         // TODO(b/112196799) Need to sort out the reasons better. Also, we get here
298         // when we disconnect from below, so it should sometimes get counted as a
299         // disconnection rather than a connection failure.
300         update(Event.CONNECTION_FAILURE, wifiInfo);
301         resetConnectionStateInternal(false);
302     }
303 
304     /**
305      * Updates the score card after network reachability failure
306      *
307      * @param wifiInfo object holding relevant values
308      */
noteIpReachabilityLost(ExtendedWifiInfo wifiInfo)309     public void noteIpReachabilityLost(ExtendedWifiInfo wifiInfo) {
310         update(Event.IP_REACHABILITY_LOST, wifiInfo);
311         if (mTsRoam > TS_NONE) {
312             mTsConnectionAttemptStart = mTsRoam; // just to update elapsed
313             update(Event.ROAM_FAILURE, wifiInfo);
314         }
315         resetConnectionStateInternal(false);
316         doWrites();
317     }
318 
319     /**
320      * Updates the score card before a roam
321      *
322      * We may have already done a firmware roam, but wifiInfo has not yet
323      * been updated, so we still have the old state.
324      *
325      * @param wifiInfo object holding relevant values
326      */
noteRoam(ExtendedWifiInfo wifiInfo)327     public void noteRoam(ExtendedWifiInfo wifiInfo) {
328         update(Event.LAST_POLL_BEFORE_ROAM, wifiInfo);
329         mTsRoam = mClock.getElapsedSinceBootMillis();
330     }
331 
332     /**
333      * Called when the supplicant state is about to change, before wifiInfo is updated
334      *
335      * @param wifiInfo object holding old values
336      * @param state the new supplicant state
337      */
noteSupplicantStateChanging(ExtendedWifiInfo wifiInfo, SupplicantState state)338     public void noteSupplicantStateChanging(ExtendedWifiInfo wifiInfo, SupplicantState state) {
339         if (DBG) {
340             Log.d(TAG, "Changing state to " + state + " " + wifiInfo);
341         }
342     }
343 
344     /**
345      * Called after the supplicant state changed
346      *
347      * @param wifiInfo object holding old values
348      */
noteSupplicantStateChanged(ExtendedWifiInfo wifiInfo)349     public void noteSupplicantStateChanged(ExtendedWifiInfo wifiInfo) {
350         if (DBG) {
351             Log.d(TAG, "STATE " + wifiInfo);
352         }
353     }
354 
355     /**
356      * Updates the score card after wifi is disabled
357      *
358      * @param wifiInfo object holding relevant values
359      */
noteWifiDisabled(ExtendedWifiInfo wifiInfo)360     public void noteWifiDisabled(ExtendedWifiInfo wifiInfo) {
361         update(Event.WIFI_DISABLED, wifiInfo);
362         resetConnectionStateInternal(false);
363         doWrites();
364     }
365 
366     final class PerBssid {
367         public int id;
368         public final String l2Key;
369         public final String ssid;
370         public final MacAddress bssid;
371         public boolean changed;
372         private SecurityType mSecurityType = null;
373         private int mNetworkAgentId = Integer.MIN_VALUE;
374         private int mNetworkConfigId = Integer.MIN_VALUE;
375         private final Map<Pair<Event, Integer>, PerSignal>
376                 mSignalForEventAndFrequency = new ArrayMap<>();
PerBssid(String ssid, MacAddress bssid)377         PerBssid(String ssid, MacAddress bssid) {
378             this.ssid = ssid;
379             this.bssid = bssid;
380             final long hash = computeHashLong(ssid, bssid);
381             this.l2Key = l2KeyFromLong(hash);
382             this.id = idFromLong(hash);
383             this.changed = false;
384         }
updateEventStats(Event event, int frequency, int rssi, int linkspeed)385         void updateEventStats(Event event, int frequency, int rssi, int linkspeed) {
386             PerSignal perSignal = lookupSignal(event, frequency);
387             if (rssi != INVALID_RSSI) {
388                 perSignal.rssi.update(rssi);
389             }
390             if (linkspeed > 0) {
391                 perSignal.linkspeed.update(linkspeed);
392             }
393             if (perSignal.elapsedMs != null && mTsConnectionAttemptStart > TS_NONE) {
394                 long millis = mClock.getElapsedSinceBootMillis() - mTsConnectionAttemptStart;
395                 if (millis >= 0) {
396                     perSignal.elapsedMs.update(millis);
397                 }
398             }
399             changed = true;
400         }
lookupSignal(Event event, int frequency)401         PerSignal lookupSignal(Event event, int frequency) {
402             finishPendingRead();
403             Pair<Event, Integer> key = new Pair<>(event, frequency);
404             PerSignal ans = mSignalForEventAndFrequency.get(key);
405             if (ans == null) {
406                 ans = new PerSignal(event, frequency);
407                 mSignalForEventAndFrequency.put(key, ans);
408             }
409             return ans;
410         }
getSecurityType()411         SecurityType getSecurityType() {
412             finishPendingRead();
413             return mSecurityType;
414         }
setSecurityType(SecurityType securityType)415         void setSecurityType(SecurityType securityType) {
416             finishPendingRead();
417             if (!Objects.equals(securityType, mSecurityType)) {
418                 mSecurityType = securityType;
419                 changed = true;
420             }
421         }
setNetworkConfigId(int networkConfigId)422         void setNetworkConfigId(int networkConfigId) {
423             // Not serialized, so don't need to set changed, etc.
424             if (networkConfigId >= 0) {
425                 mNetworkConfigId = networkConfigId;
426             }
427         }
toAccessPoint()428         AccessPoint toAccessPoint() {
429             return toAccessPoint(false);
430         }
toAccessPoint(boolean obfuscate)431         AccessPoint toAccessPoint(boolean obfuscate) {
432             finishPendingRead();
433             AccessPoint.Builder builder = AccessPoint.newBuilder();
434             builder.setId(id);
435             if (!obfuscate) {
436                 builder.setBssid(ByteString.copyFrom(bssid.toByteArray()));
437             }
438             if (mSecurityType != null) {
439                 builder.setSecurityType(mSecurityType);
440             }
441             for (PerSignal sig: mSignalForEventAndFrequency.values()) {
442                 builder.addEventStats(sig.toSignal());
443             }
444             return builder.build();
445         }
merge(AccessPoint ap)446         PerBssid merge(AccessPoint ap) {
447             if (ap.hasId() && this.id != ap.getId()) {
448                 return this;
449             }
450             if (ap.hasSecurityType()) {
451                 SecurityType prev = ap.getSecurityType();
452                 if (mSecurityType == null) {
453                     mSecurityType = prev;
454                 } else if (!mSecurityType.equals(prev)) {
455                     if (DBG) {
456                         Log.i(TAG, "ID: " + id
457                                 + "SecurityType changed: " + prev + " to " + mSecurityType);
458                     }
459                     changed = true;
460                 }
461             }
462             for (Signal signal: ap.getEventStatsList()) {
463                 Pair<Event, Integer> key = new Pair<>(signal.getEvent(), signal.getFrequency());
464                 PerSignal perSignal = mSignalForEventAndFrequency.get(key);
465                 if (perSignal == null) {
466                     mSignalForEventAndFrequency.put(key, new PerSignal(signal));
467                     // No need to set changed for this, since we are in sync with what's stored
468                 } else {
469                     perSignal.merge(signal);
470                     changed = true;
471                 }
472             }
473             return this;
474         }
getL2Key()475         String getL2Key() {
476             return l2Key.toString();
477         }
478         /**
479          * Called when the (asynchronous) answer to a read request comes back.
480          */
lazyMerge(byte[] serialized)481         void lazyMerge(byte[] serialized) {
482             if (serialized == null) return;
483             byte[] old = mPendingReadFromStore.getAndSet(serialized);
484             if (old != null) {
485                 Log.e(TAG, "More answers than we expected!");
486             }
487         }
488         /**
489          * Handles (when convenient) the arrival of previously stored data.
490          *
491          * The response from IpMemoryStore arrives on a different thread, so we
492          * defer handling it until here, when we're on our favorite thread and
493          * in a good position to deal with it. We may have already collected some
494          * data before now, so we need to be prepared to merge the new and old together.
495          */
finishPendingRead()496         void finishPendingRead() {
497             final byte[] serialized = mPendingReadFromStore.getAndSet(null);
498             if (serialized == null) return;
499             AccessPoint ap;
500             try {
501                 ap = AccessPoint.parseFrom(serialized);
502             } catch (InvalidProtocolBufferException e) {
503                 Log.e(TAG, "Failed to deserialize", e);
504                 return;
505             }
506             merge(ap);
507         }
508         private final AtomicReference<byte[]> mPendingReadFromStore = new AtomicReference<>();
509     }
510 
511     // Returned by lookupBssid when the BSSID is not available,
512     // for instance when we are not associated.
513     private final PerBssid mDummyPerBssid;
514 
515     private final Map<MacAddress, PerBssid> mApForBssid = new ArrayMap<>();
516 
517     // TODO should be private, but WifiCandidates needs it
lookupBssid(String ssid, String bssid)518     @NonNull PerBssid lookupBssid(String ssid, String bssid) {
519         MacAddress mac;
520         if (ssid == null || WifiSsid.NONE.equals(ssid) || bssid == null) {
521             return mDummyPerBssid;
522         }
523         try {
524             mac = MacAddress.fromString(bssid);
525         } catch (IllegalArgumentException e) {
526             return mDummyPerBssid;
527         }
528         PerBssid ans = mApForBssid.get(mac);
529         if (ans == null || !ans.ssid.equals(ssid)) {
530             ans = new PerBssid(ssid, mac);
531             PerBssid old = mApForBssid.put(mac, ans);
532             if (old != null) {
533                 Log.i(TAG, "Discarding stats for score card (ssid changed) ID: " + old.id);
534             }
535             requestReadForPerBssid(ans);
536         }
537         return ans;
538     }
539 
requestReadForPerBssid(final PerBssid perBssid)540     private void requestReadForPerBssid(final PerBssid perBssid) {
541         if (mMemoryStore != null) {
542             mMemoryStore.read(perBssid.getL2Key(), (value) -> perBssid.lazyMerge(value));
543         }
544     }
545 
requestReadForAllChanged()546     private void requestReadForAllChanged() {
547         for (PerBssid perBssid : mApForBssid.values()) {
548             if (perBssid.changed) {
549                 requestReadForPerBssid(perBssid);
550             }
551         }
552     }
553 
554     /**
555      * Issues write requests for all changed entries.
556      *
557      * This should be called from time to time to save the state to persistent
558      * storage. Since we always check internal state first, this does not need
559      * to be called very often, but it should be called before shutdown.
560      *
561      * @returns number of writes issued.
562      */
doWrites()563     public int doWrites() {
564         if (mMemoryStore == null) return 0;
565         int count = 0;
566         int bytes = 0;
567         for (PerBssid perBssid : mApForBssid.values()) {
568             if (perBssid.changed) {
569                 perBssid.finishPendingRead();
570                 byte[] serialized = perBssid.toAccessPoint(/* No BSSID */ true).toByteArray();
571                 mMemoryStore.write(perBssid.getL2Key(), serialized);
572                 perBssid.changed = false;
573                 count++;
574                 bytes += serialized.length;
575             }
576         }
577         if (DBG && count > 0) {
578             Log.v(TAG, "Write count: " + count + ", bytes: " + bytes);
579         }
580         return count;
581     }
582 
computeHashLong(String ssid, MacAddress mac)583     private long computeHashLong(String ssid, MacAddress mac) {
584         byte[][] parts = {
585                 // Our seed keeps the L2Keys specific to this device
586                 mL2KeySeed.getBytes(),
587                 // ssid is either quoted utf8 or hex-encoded bytes; turn it into plain bytes.
588                 NativeUtil.byteArrayFromArrayList(NativeUtil.decodeSsid(ssid)),
589                 // And the BSSID
590                 mac.toByteArray()
591         };
592         // Assemble the parts into one, with single-byte lengths before each.
593         int n = 0;
594         for (int i = 0; i < parts.length; i++) {
595             n += 1 + parts[i].length;
596         }
597         byte[] mashed = new byte[n];
598         int p = 0;
599         for (int i = 0; i < parts.length; i++) {
600             byte[] part = parts[i];
601             mashed[p++] = (byte) part.length;
602             for (int j = 0; j < part.length; j++) {
603                 mashed[p++] = part[j];
604             }
605         }
606         // Finally, turn that into a long
607         MessageDigest md;
608         try {
609             md = MessageDigest.getInstance("SHA-256");
610         } catch (NoSuchAlgorithmException e) {
611             Log.e(TAG, "SHA-256 not supported.");
612             return 0;
613         }
614         ByteBuffer buffer = ByteBuffer.wrap(md.digest(mashed));
615         return buffer.getLong();
616     }
617 
idFromLong(long hash)618     private static int idFromLong(long hash) {
619         return (int) hash & 0x7fffffff;
620     }
621 
l2KeyFromLong(long hash)622     private static String l2KeyFromLong(long hash) {
623         return "W" + Long.toHexString(hash);
624     }
625 
groupHintFromLong(long hash)626     private static String groupHintFromLong(long hash) {
627         return "G" + Long.toHexString(hash);
628     }
629 
630     @VisibleForTesting
fetchByBssid(MacAddress mac)631     PerBssid fetchByBssid(MacAddress mac) {
632         return mApForBssid.get(mac);
633     }
634 
635     @VisibleForTesting
perBssidFromAccessPoint(String ssid, AccessPoint ap)636     PerBssid perBssidFromAccessPoint(String ssid, AccessPoint ap) {
637         MacAddress bssid = MacAddress.fromBytes(ap.getBssid().toByteArray());
638         return new PerBssid(ssid, bssid).merge(ap);
639     }
640 
641     final class PerSignal {
642         public final Event event;
643         public final int frequency;
644         public final PerUnivariateStatistic rssi;
645         public final PerUnivariateStatistic linkspeed;
646         @Nullable public final PerUnivariateStatistic elapsedMs;
PerSignal(Event event, int frequency)647         PerSignal(Event event, int frequency) {
648             this.event = event;
649             this.frequency = frequency;
650             this.rssi = new PerUnivariateStatistic();
651             this.linkspeed = new PerUnivariateStatistic();
652             switch (event) {
653                 case FIRST_POLL_AFTER_CONNECTION:
654                 case IP_CONFIGURATION_SUCCESS:
655                 case VALIDATION_SUCCESS:
656                 case CONNECTION_FAILURE:
657                 case WIFI_DISABLED:
658                 case ROAM_FAILURE:
659                     this.elapsedMs = new PerUnivariateStatistic();
660                     break;
661                 default:
662                     this.elapsedMs = null;
663                     break;
664             }
665         }
PerSignal(Signal signal)666         PerSignal(Signal signal) {
667             this.event = signal.getEvent();
668             this.frequency = signal.getFrequency();
669             this.rssi = new PerUnivariateStatistic(signal.getRssi());
670             this.linkspeed = new PerUnivariateStatistic(signal.getLinkspeed());
671             if (signal.hasElapsedMs()) {
672                 this.elapsedMs = new PerUnivariateStatistic(signal.getElapsedMs());
673             } else {
674                 this.elapsedMs = null;
675             }
676         }
merge(Signal signal)677         void merge(Signal signal) {
678             Preconditions.checkArgument(event == signal.getEvent());
679             Preconditions.checkArgument(frequency == signal.getFrequency());
680             rssi.merge(signal.getRssi());
681             linkspeed.merge(signal.getLinkspeed());
682             if (signal.hasElapsedMs()) {
683                 elapsedMs.merge(signal.getElapsedMs());
684             }
685         }
toSignal()686         Signal toSignal() {
687             Signal.Builder builder = Signal.newBuilder();
688             builder.setEvent(event)
689                     .setFrequency(frequency)
690                     .setRssi(rssi.toUnivariateStatistic())
691                     .setLinkspeed(linkspeed.toUnivariateStatistic());
692             if (elapsedMs != null) {
693                 builder.setElapsedMs(elapsedMs.toUnivariateStatistic());
694             }
695             return builder.build();
696         }
697     }
698 
699     final class PerUnivariateStatistic {
700         public long count = 0;
701         public double sum = 0.0;
702         public double sumOfSquares = 0.0;
703         public double minValue = Double.POSITIVE_INFINITY;
704         public double maxValue = Double.NEGATIVE_INFINITY;
705         public double historicalMean = 0.0;
706         public double historicalVariance = Double.POSITIVE_INFINITY;
PerUnivariateStatistic()707         PerUnivariateStatistic() {}
PerUnivariateStatistic(UnivariateStatistic stats)708         PerUnivariateStatistic(UnivariateStatistic stats) {
709             if (stats.hasCount()) {
710                 this.count = stats.getCount();
711                 this.sum = stats.getSum();
712                 this.sumOfSquares = stats.getSumOfSquares();
713             }
714             if (stats.hasMinValue()) {
715                 this.minValue = stats.getMinValue();
716             }
717             if (stats.hasMaxValue()) {
718                 this.maxValue = stats.getMaxValue();
719             }
720             if (stats.hasHistoricalMean()) {
721                 this.historicalMean = stats.getHistoricalMean();
722             }
723             if (stats.hasHistoricalVariance()) {
724                 this.historicalVariance = stats.getHistoricalVariance();
725             }
726         }
update(double value)727         void update(double value) {
728             count++;
729             sum += value;
730             sumOfSquares += value * value;
731             minValue = Math.min(minValue, value);
732             maxValue = Math.max(maxValue, value);
733         }
age()734         void age() {
735             //TODO  Fold the current stats into the historical stats
736         }
merge(UnivariateStatistic stats)737         void merge(UnivariateStatistic stats) {
738             if (stats.hasCount()) {
739                 count += stats.getCount();
740                 sum += stats.getSum();
741                 sumOfSquares += stats.getSumOfSquares();
742             }
743             if (stats.hasMinValue()) {
744                 minValue = Math.min(minValue, stats.getMinValue());
745             }
746             if (stats.hasMaxValue()) {
747                 maxValue = Math.max(maxValue, stats.getMaxValue());
748             }
749             if (stats.hasHistoricalVariance()) {
750                 if (historicalVariance < Double.POSITIVE_INFINITY) {
751                     // Combine the estimates; c.f.
752                     // Maybeck, Stochasic Models, Estimation, and Control, Vol. 1
753                     // equations (1-3) and (1-4)
754                     double numer1 = stats.getHistoricalVariance();
755                     double numer2 = historicalVariance;
756                     double denom = numer1 + numer2;
757                     historicalMean = (numer1 * historicalMean
758                                     + numer2 * stats.getHistoricalMean())
759                                     / denom;
760                     historicalVariance = numer1 * numer2 / denom;
761                 } else {
762                     historicalMean = stats.getHistoricalMean();
763                     historicalVariance = stats.getHistoricalVariance();
764                 }
765             }
766         }
toUnivariateStatistic()767         UnivariateStatistic toUnivariateStatistic() {
768             UnivariateStatistic.Builder builder = UnivariateStatistic.newBuilder();
769             if (count != 0) {
770                 builder.setCount(count)
771                         .setSum(sum)
772                         .setSumOfSquares(sumOfSquares)
773                         .setMinValue(minValue)
774                         .setMaxValue(maxValue);
775             }
776             if (historicalVariance < Double.POSITIVE_INFINITY) {
777                 builder.setHistoricalMean(historicalMean)
778                         .setHistoricalVariance(historicalVariance);
779             }
780             return builder.build();
781         }
782     }
783 
784     /**
785      * Returns the current scorecard in the form of a protobuf com_android_server_wifi.NetworkList
786      *
787      * Synchronization is the caller's responsibility.
788      *
789      * @param obfuscate - if true, ssids and bssids are omitted (short id only)
790      */
getNetworkListByteArray(boolean obfuscate)791     public byte[] getNetworkListByteArray(boolean obfuscate) {
792         Map<String, Network.Builder> networks = new ArrayMap<>();
793         for (PerBssid perBssid: mApForBssid.values()) {
794             String key = perBssid.ssid;
795             Network.Builder network = networks.get(key);
796             if (network == null) {
797                 network = Network.newBuilder();
798                 networks.put(key, network);
799                 if (!obfuscate) {
800                     network.setSsid(perBssid.ssid);
801                 }
802                 if (perBssid.mSecurityType != null) {
803                     network.setSecurityType(perBssid.mSecurityType);
804                 }
805                 if (perBssid.mNetworkAgentId >= network.getNetworkAgentId()) {
806                     network.setNetworkAgentId(perBssid.mNetworkAgentId);
807                 }
808                 if (perBssid.mNetworkConfigId >= network.getNetworkConfigId()) {
809                     network.setNetworkConfigId(perBssid.mNetworkConfigId);
810                 }
811             }
812             network.addAccessPoints(perBssid.toAccessPoint(obfuscate));
813         }
814         NetworkList.Builder builder = NetworkList.newBuilder();
815         for (Network.Builder network: networks.values()) {
816             builder.addNetworks(network);
817         }
818         return builder.build().toByteArray();
819     }
820 
821     /**
822      * Returns the current scorecard as a base64-encoded protobuf
823      *
824      * Synchronization is the caller's responsibility.
825      *
826      * @param obfuscate - if true, bssids are omitted (short id only)
827      */
getNetworkListBase64(boolean obfuscate)828     public String getNetworkListBase64(boolean obfuscate) {
829         byte[] raw = getNetworkListByteArray(obfuscate);
830         return Base64.encodeToString(raw, Base64.DEFAULT);
831     }
832 
833     /**
834      * Clears the internal state.
835      *
836      * This is called in response to a factoryReset call from Settings.
837      * The memory store will be called after we are called, to wipe the stable
838      * storage as well. Since we will have just removed all of our networks,
839      * it is very unlikely that we're connected, or will connect immediately.
840      * Any in-flight reads will land in the objects we are dropping here, and
841      * the memory store should drop the in-flight writes. Ideally we would
842      * avoid issuing reads until we were sure that the memory store had
843      * received the factoryReset.
844      */
clear()845     public void clear() {
846         mApForBssid.clear();
847         resetConnectionStateInternal(false);
848     }
849 
850 }
851