1 /*
2  * Copyright 2019 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.net.wifi.ScanResult;
21 
22 import com.android.server.wifi.WifiCandidates.Candidate;
23 import com.android.server.wifi.WifiCandidates.ScoredCandidate;
24 
25 import java.util.Collection;
26 
27 /**
28  * A CandidateScorer that weights the RSSIs for more compactly-shaped
29  * regions of selection around access points.
30  */
31 final class BubbleFunScorer implements WifiCandidates.CandidateScorer {
32 
33     /**
34      * This should match WifiNetworkSelector.experimentIdFromIdentifier(getIdentifier())
35      * when using the default ScoringParams.
36      */
37     public static final int BUBBLE_FUN_SCORER_DEFAULT_EXPID = 42598152;
38 
39     private static final double SECURITY_AWARD = 44.0;
40     private static final double CURRENT_NETWORK_BOOST = 22.0;
41     private static final double LAST_SELECTION_BOOST = 250.0;
42     private static final double LOW_BAND_FACTOR = 0.25;
43     private static final double TYPICAL_SCAN_RSSI_STD = 4.0;
44     private static final boolean USE_USER_CONNECT_CHOICE = true;
45 
46     private final ScoringParams mScoringParams;
47 
BubbleFunScorer(ScoringParams scoringParams)48     BubbleFunScorer(ScoringParams scoringParams) {
49         mScoringParams = scoringParams;
50     }
51 
52     @Override
getIdentifier()53     public String getIdentifier() {
54         return "BubbleFunScorer_v2";
55     }
56 
57     /**
58      * Calculates an individual candidate's score.
59      *
60      * Ideally, this is a pure function of the candidate, and side-effect free.
61      */
scoreCandidate(Candidate candidate)62     private ScoredCandidate scoreCandidate(Candidate candidate) {
63         final int rssi = candidate.getScanRssi();
64         final int rssiEntryThreshold = mScoringParams.getEntryRssi(candidate.getFrequency());
65 
66         double score = shapeFunction(rssi) - shapeFunction(rssiEntryThreshold);
67 
68         // If we are below the entry threshold, make the score more negative
69         if (score < 0.0) score *= 2.0;
70 
71         // The gain is approximately the derivative of shapeFunction at the given rssi
72         // This is used to estimate the error
73         double gain = shapeFunction(rssi + 0.5)
74                     - shapeFunction(rssi - 0.5);
75 
76         // Prefer 5GHz/6GHz when all are strong, but at the fringes, 2.4 might be better
77         // Typically the entry rssi is lower for the 2.4 band, which provides the fringe boost
78         if (ScanResult.is24GHz(candidate.getFrequency())) {
79             score *= LOW_BAND_FACTOR;
80             gain *= LOW_BAND_FACTOR;
81         }
82 
83         // A recently selected network gets a large boost
84         score += candidate.getLastSelectionWeight() * LAST_SELECTION_BOOST;
85 
86         // Hysteresis to prefer staying on the current network.
87         if (candidate.isCurrentNetwork()) {
88             score += CURRENT_NETWORK_BOOST;
89         }
90 
91         if (!candidate.isOpenNetwork()) {
92             score += SECURITY_AWARD;
93         }
94 
95         return new ScoredCandidate(score, TYPICAL_SCAN_RSSI_STD * gain,
96                                    USE_USER_CONNECT_CHOICE, candidate);
97     }
98 
99     /**
100      * Reshapes raw RSSI into a value that varies more usefully for scoring purposes.
101      *
102      * The most important aspect of this function is that it is monotone (has
103      * positive slope). The offset and scale are not important, because the
104      * calculation above uses differences that cancel out the offset, and
105      * a rescaling here effects all the candidates' scores in the same way.
106      * However, we choose to scale things for an overall range of about 100 for
107      * useful values of RSSI.
108      */
unscaledShapeFunction(double rssi)109     private static double unscaledShapeFunction(double rssi) {
110         return -Math.exp(-rssi * BELS_PER_DECIBEL);
111     }
112     private static final double BELS_PER_DECIBEL = 0.1;
113 
114     private static final double RESCALE_FACTOR = 100.0 / (
115             unscaledShapeFunction(0.0) - unscaledShapeFunction(-85.0));
shapeFunction(double rssi)116     private static double shapeFunction(double rssi) {
117         return unscaledShapeFunction(rssi) * RESCALE_FACTOR;
118     }
119 
120     @Override
scoreCandidates(@onNull Collection<Candidate> candidates)121     public ScoredCandidate scoreCandidates(@NonNull Collection<Candidate> candidates) {
122         ScoredCandidate choice = ScoredCandidate.NONE;
123         for (Candidate candidate : candidates) {
124             ScoredCandidate scoredCandidate = scoreCandidate(candidate);
125             if (scoredCandidate.value > choice.value) {
126                 choice = scoredCandidate;
127             }
128         }
129         // Here we just return the highest scored candidate; we could
130         // compute a new score, if desired.
131         return choice;
132     }
133 
134 }
135