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