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 android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.net.MacAddress; 22 import android.net.wifi.ScanResult; 23 import android.net.wifi.WifiConfiguration; 24 import android.util.ArrayMap; 25 26 import com.android.internal.util.Preconditions; 27 28 import java.util.ArrayList; 29 import java.util.Collection; 30 import java.util.Map; 31 import java.util.Objects; 32 import java.util.StringJoiner; 33 34 /** 35 * Candidates for network selection 36 */ 37 public class WifiCandidates { 38 private static final String TAG = "WifiCandidates"; 39 WifiCandidates(@onNull WifiScoreCard wifiScoreCard)40 WifiCandidates(@NonNull WifiScoreCard wifiScoreCard) { 41 mWifiScoreCard = Preconditions.checkNotNull(wifiScoreCard); 42 } 43 private final WifiScoreCard mWifiScoreCard; 44 45 /** 46 * Represents a connectable candidate. 47 */ 48 public interface Candidate { 49 /** 50 * Gets the Key, which contains the SSID, BSSID, security type, and config id. 51 * 52 * Generally, a CandidateScorer should not need to use this. 53 */ getKey()54 @Nullable Key getKey(); 55 /** 56 * Gets the ScanDetail associate with the candidate. 57 */ getScanDetail()58 @Nullable ScanDetail getScanDetail(); 59 /** 60 * Gets the config id. 61 */ getNetworkConfigId()62 int getNetworkConfigId(); 63 /** 64 * Returns true for an open network. 65 */ isOpenNetwork()66 boolean isOpenNetwork(); 67 /** 68 * Returns true for a passpoint network. 69 */ isPasspoint()70 boolean isPasspoint(); 71 /** 72 * Returns true for an ephemeral network. 73 */ isEphemeral()74 boolean isEphemeral(); 75 /** 76 * Returns true for a trusted network. 77 */ isTrusted()78 boolean isTrusted(); 79 /** 80 * Returns the ID of the evaluator that provided the candidate. 81 */ getEvaluatorId()82 @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int getEvaluatorId(); 83 /** 84 * Gets the score that was provided by the evaluator. 85 * 86 * Not all evaluators provide a useful score. Scores from different evaluators 87 * are not directly comparable. 88 */ getEvaluatorScore()89 int getEvaluatorScore(); 90 /** 91 * Returns true if the candidate is in the same network as the 92 * current connection. 93 */ isCurrentNetwork()94 boolean isCurrentNetwork(); 95 /** 96 * Return true if the candidate is currently connected. 97 */ isCurrentBssid()98 boolean isCurrentBssid(); 99 /** 100 * Returns a value between 0 and 1. 101 * 102 * 1.0 means the network was recently selected by the user or an app. 103 * 0.0 means not recently selected by user or app. 104 */ getLastSelectionWeight()105 double getLastSelectionWeight(); 106 /** 107 * Gets the scan RSSI. 108 */ getScanRssi()109 int getScanRssi(); 110 /** 111 * Gets the scan frequency. 112 */ getFrequency()113 int getFrequency(); 114 /** 115 * Gets statistics from the scorecard. 116 */ getEventStatistics(WifiScoreCardProto.Event event)117 @Nullable WifiScoreCardProto.Signal getEventStatistics(WifiScoreCardProto.Event event); 118 } 119 120 /** 121 * Represents a connectable candidate 122 */ 123 static class CandidateImpl implements Candidate { 124 public final Key key; // SSID/sectype/BSSID/configId 125 public final ScanDetail scanDetail; 126 public final WifiConfiguration config; 127 // First evaluator to nominate this config 128 public final @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId; 129 public final int evaluatorScore; // Score provided by first nominating evaluator 130 public final double lastSelectionWeight; // Value between 0 and 1 131 132 private WifiScoreCard.PerBssid mPerBssid; // For accessing the scorecard entry 133 private final boolean mIsCurrentNetwork; 134 private final boolean mIsCurrentBssid; 135 CandidateImpl(Key key, ScanDetail scanDetail, WifiConfiguration config, @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId, int evaluatorScore, WifiScoreCard.PerBssid perBssid, double lastSelectionWeight, boolean isCurrentNetwork, boolean isCurrentBssid)136 CandidateImpl(Key key, 137 ScanDetail scanDetail, 138 WifiConfiguration config, 139 @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId, 140 int evaluatorScore, 141 WifiScoreCard.PerBssid perBssid, 142 double lastSelectionWeight, 143 boolean isCurrentNetwork, 144 boolean isCurrentBssid) { 145 this.key = key; 146 this.scanDetail = scanDetail; 147 this.config = config; 148 this.evaluatorId = evaluatorId; 149 this.evaluatorScore = evaluatorScore; 150 this.mPerBssid = perBssid; 151 this.lastSelectionWeight = lastSelectionWeight; 152 this.mIsCurrentNetwork = isCurrentNetwork; 153 this.mIsCurrentBssid = isCurrentBssid; 154 } 155 156 @Override getKey()157 public Key getKey() { 158 return key; 159 } 160 161 @Override getNetworkConfigId()162 public int getNetworkConfigId() { 163 return key.networkId; 164 } 165 166 @Override getScanDetail()167 public ScanDetail getScanDetail() { 168 return scanDetail; 169 } 170 171 @Override isOpenNetwork()172 public boolean isOpenNetwork() { 173 // TODO - should be able to base this on key.matchInfo.securityType 174 return WifiConfigurationUtil.isConfigForOpenNetwork(config); 175 } 176 177 @Override isPasspoint()178 public boolean isPasspoint() { 179 return config.isPasspoint(); 180 } 181 182 @Override isEphemeral()183 public boolean isEphemeral() { 184 return config.ephemeral; 185 } 186 187 @Override isTrusted()188 public boolean isTrusted() { 189 return config.trusted; 190 } 191 192 @Override getEvaluatorId()193 public @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int getEvaluatorId() { 194 return evaluatorId; 195 } 196 197 @Override getEvaluatorScore()198 public int getEvaluatorScore() { 199 return evaluatorScore; 200 } 201 202 @Override getLastSelectionWeight()203 public double getLastSelectionWeight() { 204 return lastSelectionWeight; 205 } 206 207 @Override isCurrentNetwork()208 public boolean isCurrentNetwork() { 209 return mIsCurrentNetwork; 210 } 211 212 @Override isCurrentBssid()213 public boolean isCurrentBssid() { 214 return mIsCurrentBssid; 215 } 216 217 @Override getScanRssi()218 public int getScanRssi() { 219 return scanDetail.getScanResult().level; 220 } 221 222 @Override getFrequency()223 public int getFrequency() { 224 return scanDetail.getScanResult().frequency; 225 } 226 227 /** 228 * Accesses statistical information from the score card 229 */ 230 @Override 231 public WifiScoreCardProto.Signal getEventStatistics(WifiScoreCardProto.Event event)232 getEventStatistics(WifiScoreCardProto.Event event) { 233 if (mPerBssid == null) return null; 234 WifiScoreCard.PerSignal perSignal = mPerBssid.lookupSignal(event, getFrequency()); 235 if (perSignal == null) return null; 236 return perSignal.toSignal(); 237 } 238 239 } 240 241 /** 242 * Represents a scoring function 243 */ 244 public interface CandidateScorer { 245 /** 246 * The scorer's name, and perhaps important parameterization/version. 247 */ getIdentifier()248 String getIdentifier(); 249 250 /** 251 * Calculates the score for a group of candidates that belong 252 * to the same network. 253 */ scoreCandidates(@onNull Collection<Candidate> group)254 @Nullable ScoredCandidate scoreCandidates(@NonNull Collection<Candidate> group); 255 256 /** 257 * Returns true if the legacy user connect choice logic should be used. 258 * 259 * @returns false to disable the legacy logic 260 */ userConnectChoiceOverrideWanted()261 boolean userConnectChoiceOverrideWanted(); 262 } 263 264 /** 265 * Represents a candidate with a real-valued score, along with an error estimate. 266 * 267 * Larger values reflect more desirable candidates. The range is arbitrary, 268 * because scores generated by different sources are not compared with each 269 * other. 270 * 271 * The error estimate is on the same scale as the value, and should 272 * always be strictly positive. For instance, it might be the standard deviation. 273 */ 274 public static class ScoredCandidate { 275 public final double value; 276 public final double err; 277 public final Key candidateKey; ScoredCandidate(double value, double err, Candidate candidate)278 public ScoredCandidate(double value, double err, Candidate candidate) { 279 this.value = value; 280 this.err = err; 281 this.candidateKey = (candidate == null) ? null : candidate.getKey(); 282 } 283 /** 284 * Represents no score 285 */ 286 public static final ScoredCandidate NONE = 287 new ScoredCandidate(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, null); 288 } 289 290 /** 291 * The key used for tracking candidates, consisting of SSID, security type, BSSID, and network 292 * configuration id. 293 */ 294 // TODO (b/123014687) unify with similar classes in the framework 295 public static class Key { 296 public final ScanResultMatchInfo matchInfo; // Contains the SSID and security type 297 public final MacAddress bssid; 298 public final int networkId; // network configuration id 299 Key(ScanResultMatchInfo matchInfo, MacAddress bssid, int networkId)300 public Key(ScanResultMatchInfo matchInfo, 301 MacAddress bssid, 302 int networkId) { 303 this.matchInfo = matchInfo; 304 this.bssid = bssid; 305 this.networkId = networkId; 306 } 307 308 @Override equals(Object other)309 public boolean equals(Object other) { 310 if (!(other instanceof Key)) return false; 311 Key that = (Key) other; 312 return (this.matchInfo.equals(that.matchInfo) 313 && this.bssid.equals(that.bssid) 314 && this.networkId == that.networkId); 315 } 316 317 @Override hashCode()318 public int hashCode() { 319 return Objects.hash(matchInfo, bssid, networkId); 320 } 321 } 322 323 private final Map<Key, CandidateImpl> mCandidates = new ArrayMap<>(); 324 325 private int mCurrentNetworkId = -1; 326 @Nullable private MacAddress mCurrentBssid = null; 327 328 /** 329 * Sets up information about the currently-connected network. 330 */ setCurrent(int currentNetworkId, String currentBssid)331 public void setCurrent(int currentNetworkId, String currentBssid) { 332 mCurrentNetworkId = currentNetworkId; 333 mCurrentBssid = null; 334 if (currentBssid == null) return; 335 try { 336 mCurrentBssid = MacAddress.fromString(currentBssid); 337 } catch (RuntimeException e) { 338 failWithException(e); 339 } 340 } 341 342 /** 343 * Adds a new candidate 344 * 345 * @returns true if added or replaced, false otherwise 346 */ add(ScanDetail scanDetail, WifiConfiguration config, @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId, int evaluatorScore, double lastSelectionWeightBetweenZeroAndOne)347 public boolean add(ScanDetail scanDetail, 348 WifiConfiguration config, 349 @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId, 350 int evaluatorScore, 351 double lastSelectionWeightBetweenZeroAndOne) { 352 if (config == null) return failure(); 353 if (scanDetail == null) return failure(); 354 ScanResult scanResult = scanDetail.getScanResult(); 355 if (scanResult == null) return failure(); 356 MacAddress bssid; 357 try { 358 bssid = MacAddress.fromString(scanResult.BSSID); 359 } catch (RuntimeException e) { 360 return failWithException(e); 361 } 362 ScanResultMatchInfo key1 = ScanResultMatchInfo.fromWifiConfiguration(config); 363 ScanResultMatchInfo key2 = ScanResultMatchInfo.fromScanResult(scanResult); 364 if (!key1.equals(key2)) return failure(key1, key2); 365 Key key = new Key(key1, bssid, config.networkId); 366 CandidateImpl old = mCandidates.get(key); 367 if (old != null) { 368 // check if we want to replace this old candidate 369 if (evaluatorId < old.evaluatorId) return failure(); 370 if (evaluatorId > old.evaluatorId) return false; 371 if (evaluatorScore <= old.evaluatorScore) return false; 372 remove(old); 373 } 374 WifiScoreCard.PerBssid perBssid = mWifiScoreCard.lookupBssid( 375 key.matchInfo.networkSsid, 376 key.bssid.toString()); 377 perBssid.setSecurityType( 378 WifiScoreCardProto.SecurityType.forNumber(key.matchInfo.networkType)); 379 perBssid.setNetworkConfigId(config.networkId); 380 CandidateImpl candidate = new CandidateImpl(key, 381 scanDetail, config, evaluatorId, evaluatorScore, perBssid, 382 Math.min(Math.max(lastSelectionWeightBetweenZeroAndOne, 0.0), 1.0), 383 config.networkId == mCurrentNetworkId, 384 bssid.equals(mCurrentBssid)); 385 mCandidates.put(key, candidate); 386 return true; 387 } 388 /** Adds a new candidate with no user selection weight. */ add(ScanDetail scanDetail, WifiConfiguration config, @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId, int evaluatorScore)389 public boolean add(ScanDetail scanDetail, 390 WifiConfiguration config, 391 @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId, 392 int evaluatorScore) { 393 return add(scanDetail, config, evaluatorId, evaluatorScore, 0.0); 394 } 395 396 /** 397 * Removes a candidate 398 * @returns true if the candidate was successfully removed 399 */ remove(Candidate candidate)400 public boolean remove(Candidate candidate) { 401 if (!(candidate instanceof CandidateImpl)) return failure(); 402 return mCandidates.remove(((CandidateImpl) candidate).key, (CandidateImpl) candidate); 403 } 404 405 /** 406 * Returns the number of candidates (at the BSSID level) 407 */ size()408 public int size() { 409 return mCandidates.size(); 410 } 411 412 /** 413 * Returns the candidates, grouped by network. 414 */ getGroupedCandidates()415 public Collection<Collection<Candidate>> getGroupedCandidates() { 416 Map<Integer, Collection<Candidate>> candidatesForNetworkId = new ArrayMap<>(); 417 for (CandidateImpl candidate : mCandidates.values()) { 418 Collection<Candidate> cc = candidatesForNetworkId.get(candidate.key.networkId); 419 if (cc == null) { 420 cc = new ArrayList<>(2); // Guess 2 bssids per network 421 candidatesForNetworkId.put(candidate.key.networkId, cc); 422 } 423 cc.add(candidate); 424 } 425 return candidatesForNetworkId.values(); 426 } 427 428 /** 429 * Make a choice from among the candidates, using the provided scorer. 430 * 431 * @returns the chosen scored candidate, or ScoredCandidate.NONE. 432 */ choose(@onNull CandidateScorer candidateScorer)433 public @NonNull ScoredCandidate choose(@NonNull CandidateScorer candidateScorer) { 434 Preconditions.checkNotNull(candidateScorer); 435 ScoredCandidate choice = ScoredCandidate.NONE; 436 for (Collection<Candidate> group : getGroupedCandidates()) { 437 ScoredCandidate scoredCandidate = candidateScorer.scoreCandidates(group); 438 if (scoredCandidate != null && scoredCandidate.value > choice.value) { 439 choice = scoredCandidate; 440 } 441 } 442 return choice; 443 } 444 445 /** 446 * After a failure indication is returned, this may be used to get details. 447 */ getLastFault()448 public RuntimeException getLastFault() { 449 return mLastFault; 450 } 451 452 /** 453 * Returns the number of faults we have seen 454 */ getFaultCount()455 public int getFaultCount() { 456 return mFaultCount; 457 } 458 459 /** 460 * Clears any recorded faults 461 */ clearFaults()462 public void clearFaults() { 463 mLastFault = null; 464 mFaultCount = 0; 465 } 466 467 /** 468 * Controls whether to immediately raise an exception on a failure 469 */ setPicky(boolean picky)470 public WifiCandidates setPicky(boolean picky) { 471 mPicky = picky; 472 return this; 473 } 474 475 /** 476 * Records details about a failure 477 * 478 * This captures a stack trace, so don't bother to construct a string message, just 479 * supply any culprits (convertible to strings) that might aid diagnosis. 480 * 481 * @returns false 482 * @throws RuntimeException (if in picky mode) 483 */ failure(Object... culprits)484 private boolean failure(Object... culprits) { 485 StringJoiner joiner = new StringJoiner(","); 486 for (Object c : culprits) { 487 joiner.add("" + c); 488 } 489 return failWithException(new IllegalArgumentException(joiner.toString())); 490 } 491 492 /** 493 * As above, if we already have an exception. 494 */ failWithException(RuntimeException e)495 private boolean failWithException(RuntimeException e) { 496 mLastFault = e; 497 mFaultCount++; 498 if (mPicky) { 499 throw e; 500 } 501 return false; 502 } 503 504 private boolean mPicky = false; 505 private RuntimeException mLastFault = null; 506 private int mFaultCount = 0; 507 508 } 509