/* * Copyright 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.wifi; import android.annotation.NonNull; import android.annotation.Nullable; import android.net.MacAddress; import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; import android.util.ArrayMap; import com.android.internal.util.Preconditions; import java.util.ArrayList; import java.util.Collection; import java.util.Map; import java.util.Objects; import java.util.StringJoiner; /** * Candidates for network selection */ public class WifiCandidates { private static final String TAG = "WifiCandidates"; WifiCandidates(@NonNull WifiScoreCard wifiScoreCard) { mWifiScoreCard = Preconditions.checkNotNull(wifiScoreCard); } private final WifiScoreCard mWifiScoreCard; /** * Represents a connectable candidate. */ public interface Candidate { /** * Gets the Key, which contains the SSID, BSSID, security type, and config id. * * Generally, a CandidateScorer should not need to use this. */ @Nullable Key getKey(); /** * Gets the ScanDetail associate with the candidate. */ @Nullable ScanDetail getScanDetail(); /** * Gets the config id. */ int getNetworkConfigId(); /** * Returns true for an open network. */ boolean isOpenNetwork(); /** * Returns true for a passpoint network. */ boolean isPasspoint(); /** * Returns true for an ephemeral network. */ boolean isEphemeral(); /** * Returns true for a trusted network. */ boolean isTrusted(); /** * Returns the ID of the evaluator that provided the candidate. */ @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int getEvaluatorId(); /** * Gets the score that was provided by the evaluator. * * Not all evaluators provide a useful score. Scores from different evaluators * are not directly comparable. */ int getEvaluatorScore(); /** * Returns true if the candidate is in the same network as the * current connection. */ boolean isCurrentNetwork(); /** * Return true if the candidate is currently connected. */ boolean isCurrentBssid(); /** * Returns a value between 0 and 1. * * 1.0 means the network was recently selected by the user or an app. * 0.0 means not recently selected by user or app. */ double getLastSelectionWeight(); /** * Gets the scan RSSI. */ int getScanRssi(); /** * Gets the scan frequency. */ int getFrequency(); /** * Gets statistics from the scorecard. */ @Nullable WifiScoreCardProto.Signal getEventStatistics(WifiScoreCardProto.Event event); } /** * Represents a connectable candidate */ static class CandidateImpl implements Candidate { public final Key key; // SSID/sectype/BSSID/configId public final ScanDetail scanDetail; public final WifiConfiguration config; // First evaluator to nominate this config public final @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId; public final int evaluatorScore; // Score provided by first nominating evaluator public final double lastSelectionWeight; // Value between 0 and 1 private WifiScoreCard.PerBssid mPerBssid; // For accessing the scorecard entry private final boolean mIsCurrentNetwork; private final boolean mIsCurrentBssid; CandidateImpl(Key key, ScanDetail scanDetail, WifiConfiguration config, @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId, int evaluatorScore, WifiScoreCard.PerBssid perBssid, double lastSelectionWeight, boolean isCurrentNetwork, boolean isCurrentBssid) { this.key = key; this.scanDetail = scanDetail; this.config = config; this.evaluatorId = evaluatorId; this.evaluatorScore = evaluatorScore; this.mPerBssid = perBssid; this.lastSelectionWeight = lastSelectionWeight; this.mIsCurrentNetwork = isCurrentNetwork; this.mIsCurrentBssid = isCurrentBssid; } @Override public Key getKey() { return key; } @Override public int getNetworkConfigId() { return key.networkId; } @Override public ScanDetail getScanDetail() { return scanDetail; } @Override public boolean isOpenNetwork() { // TODO - should be able to base this on key.matchInfo.securityType return WifiConfigurationUtil.isConfigForOpenNetwork(config); } @Override public boolean isPasspoint() { return config.isPasspoint(); } @Override public boolean isEphemeral() { return config.ephemeral; } @Override public boolean isTrusted() { return config.trusted; } @Override public @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int getEvaluatorId() { return evaluatorId; } @Override public int getEvaluatorScore() { return evaluatorScore; } @Override public double getLastSelectionWeight() { return lastSelectionWeight; } @Override public boolean isCurrentNetwork() { return mIsCurrentNetwork; } @Override public boolean isCurrentBssid() { return mIsCurrentBssid; } @Override public int getScanRssi() { return scanDetail.getScanResult().level; } @Override public int getFrequency() { return scanDetail.getScanResult().frequency; } /** * Accesses statistical information from the score card */ @Override public WifiScoreCardProto.Signal getEventStatistics(WifiScoreCardProto.Event event) { if (mPerBssid == null) return null; WifiScoreCard.PerSignal perSignal = mPerBssid.lookupSignal(event, getFrequency()); if (perSignal == null) return null; return perSignal.toSignal(); } } /** * Represents a scoring function */ public interface CandidateScorer { /** * The scorer's name, and perhaps important parameterization/version. */ String getIdentifier(); /** * Calculates the score for a group of candidates that belong * to the same network. */ @Nullable ScoredCandidate scoreCandidates(@NonNull Collection group); /** * Returns true if the legacy user connect choice logic should be used. * * @returns false to disable the legacy logic */ boolean userConnectChoiceOverrideWanted(); } /** * Represents a candidate with a real-valued score, along with an error estimate. * * Larger values reflect more desirable candidates. The range is arbitrary, * because scores generated by different sources are not compared with each * other. * * The error estimate is on the same scale as the value, and should * always be strictly positive. For instance, it might be the standard deviation. */ public static class ScoredCandidate { public final double value; public final double err; public final Key candidateKey; public ScoredCandidate(double value, double err, Candidate candidate) { this.value = value; this.err = err; this.candidateKey = (candidate == null) ? null : candidate.getKey(); } /** * Represents no score */ public static final ScoredCandidate NONE = new ScoredCandidate(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, null); } /** * The key used for tracking candidates, consisting of SSID, security type, BSSID, and network * configuration id. */ // TODO (b/123014687) unify with similar classes in the framework public static class Key { public final ScanResultMatchInfo matchInfo; // Contains the SSID and security type public final MacAddress bssid; public final int networkId; // network configuration id public Key(ScanResultMatchInfo matchInfo, MacAddress bssid, int networkId) { this.matchInfo = matchInfo; this.bssid = bssid; this.networkId = networkId; } @Override public boolean equals(Object other) { if (!(other instanceof Key)) return false; Key that = (Key) other; return (this.matchInfo.equals(that.matchInfo) && this.bssid.equals(that.bssid) && this.networkId == that.networkId); } @Override public int hashCode() { return Objects.hash(matchInfo, bssid, networkId); } } private final Map mCandidates = new ArrayMap<>(); private int mCurrentNetworkId = -1; @Nullable private MacAddress mCurrentBssid = null; /** * Sets up information about the currently-connected network. */ public void setCurrent(int currentNetworkId, String currentBssid) { mCurrentNetworkId = currentNetworkId; mCurrentBssid = null; if (currentBssid == null) return; try { mCurrentBssid = MacAddress.fromString(currentBssid); } catch (RuntimeException e) { failWithException(e); } } /** * Adds a new candidate * * @returns true if added or replaced, false otherwise */ public boolean add(ScanDetail scanDetail, WifiConfiguration config, @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId, int evaluatorScore, double lastSelectionWeightBetweenZeroAndOne) { if (config == null) return failure(); if (scanDetail == null) return failure(); ScanResult scanResult = scanDetail.getScanResult(); if (scanResult == null) return failure(); MacAddress bssid; try { bssid = MacAddress.fromString(scanResult.BSSID); } catch (RuntimeException e) { return failWithException(e); } ScanResultMatchInfo key1 = ScanResultMatchInfo.fromWifiConfiguration(config); ScanResultMatchInfo key2 = ScanResultMatchInfo.fromScanResult(scanResult); if (!key1.equals(key2)) return failure(key1, key2); Key key = new Key(key1, bssid, config.networkId); CandidateImpl old = mCandidates.get(key); if (old != null) { // check if we want to replace this old candidate if (evaluatorId < old.evaluatorId) return failure(); if (evaluatorId > old.evaluatorId) return false; if (evaluatorScore <= old.evaluatorScore) return false; remove(old); } WifiScoreCard.PerBssid perBssid = mWifiScoreCard.lookupBssid( key.matchInfo.networkSsid, key.bssid.toString()); perBssid.setSecurityType( WifiScoreCardProto.SecurityType.forNumber(key.matchInfo.networkType)); perBssid.setNetworkConfigId(config.networkId); CandidateImpl candidate = new CandidateImpl(key, scanDetail, config, evaluatorId, evaluatorScore, perBssid, Math.min(Math.max(lastSelectionWeightBetweenZeroAndOne, 0.0), 1.0), config.networkId == mCurrentNetworkId, bssid.equals(mCurrentBssid)); mCandidates.put(key, candidate); return true; } /** Adds a new candidate with no user selection weight. */ public boolean add(ScanDetail scanDetail, WifiConfiguration config, @WifiNetworkSelector.NetworkEvaluator.EvaluatorId int evaluatorId, int evaluatorScore) { return add(scanDetail, config, evaluatorId, evaluatorScore, 0.0); } /** * Removes a candidate * @returns true if the candidate was successfully removed */ public boolean remove(Candidate candidate) { if (!(candidate instanceof CandidateImpl)) return failure(); return mCandidates.remove(((CandidateImpl) candidate).key, (CandidateImpl) candidate); } /** * Returns the number of candidates (at the BSSID level) */ public int size() { return mCandidates.size(); } /** * Returns the candidates, grouped by network. */ public Collection> getGroupedCandidates() { Map> candidatesForNetworkId = new ArrayMap<>(); for (CandidateImpl candidate : mCandidates.values()) { Collection cc = candidatesForNetworkId.get(candidate.key.networkId); if (cc == null) { cc = new ArrayList<>(2); // Guess 2 bssids per network candidatesForNetworkId.put(candidate.key.networkId, cc); } cc.add(candidate); } return candidatesForNetworkId.values(); } /** * Make a choice from among the candidates, using the provided scorer. * * @returns the chosen scored candidate, or ScoredCandidate.NONE. */ public @NonNull ScoredCandidate choose(@NonNull CandidateScorer candidateScorer) { Preconditions.checkNotNull(candidateScorer); ScoredCandidate choice = ScoredCandidate.NONE; for (Collection group : getGroupedCandidates()) { ScoredCandidate scoredCandidate = candidateScorer.scoreCandidates(group); if (scoredCandidate != null && scoredCandidate.value > choice.value) { choice = scoredCandidate; } } return choice; } /** * After a failure indication is returned, this may be used to get details. */ public RuntimeException getLastFault() { return mLastFault; } /** * Returns the number of faults we have seen */ public int getFaultCount() { return mFaultCount; } /** * Clears any recorded faults */ public void clearFaults() { mLastFault = null; mFaultCount = 0; } /** * Controls whether to immediately raise an exception on a failure */ public WifiCandidates setPicky(boolean picky) { mPicky = picky; return this; } /** * Records details about a failure * * This captures a stack trace, so don't bother to construct a string message, just * supply any culprits (convertible to strings) that might aid diagnosis. * * @returns false * @throws RuntimeException (if in picky mode) */ private boolean failure(Object... culprits) { StringJoiner joiner = new StringJoiner(","); for (Object c : culprits) { joiner.add("" + c); } return failWithException(new IllegalArgumentException(joiner.toString())); } /** * As above, if we already have an exception. */ private boolean failWithException(RuntimeException e) { mLastFault = e; mFaultCount++; if (mPicky) { throw e; } return false; } private boolean mPicky = false; private RuntimeException mLastFault = null; private int mFaultCount = 0; }