/*
|
* 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<Candidate> 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<Key, CandidateImpl> 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<Collection<Candidate>> getGroupedCandidates() {
|
Map<Integer, Collection<Candidate>> candidatesForNetworkId = new ArrayMap<>();
|
for (CandidateImpl candidate : mCandidates.values()) {
|
Collection<Candidate> 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<Candidate> 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;
|
|
}
|