1 /*
2  * Copyright (C) 2014 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 android.net;
18 
19 import android.annotation.Nullable;
20 import android.annotation.SystemApi;
21 import android.os.Bundle;
22 import android.os.Parcel;
23 import android.os.Parcelable;
24 
25 import java.util.Objects;
26 import java.util.Set;
27 
28 /**
29  * A network identifier along with a score for the quality of that network.
30  *
31  * @hide
32  */
33 @SystemApi
34 public class ScoredNetwork implements Parcelable {
35 
36   /**
37      * Key used with the {@link #attributes} bundle to define the badging curve.
38      *
39      * <p>The badging curve is a {@link RssiCurve} used to map different RSSI values to {@link
40      * NetworkBadging.Badging} enums.
41      */
42     public static final String ATTRIBUTES_KEY_BADGING_CURVE =
43             "android.net.attributes.key.BADGING_CURVE";
44     /**
45      * Extra used with {@link #attributes} to specify whether the
46      * network is believed to have a captive portal.
47      * <p>
48      * This data may be used, for example, to display a visual indicator
49      * in a network selection list.
50      * <p>
51      * Note that the this extra conveys the possible presence of a
52      * captive portal, not its state or the user's ability to open
53      * the portal.
54      * <p>
55      * If no value is associated with this key then it's unknown.
56      */
57     public static final String ATTRIBUTES_KEY_HAS_CAPTIVE_PORTAL =
58             "android.net.attributes.key.HAS_CAPTIVE_PORTAL";
59 
60     /**
61      * Key used with the {@link #attributes} bundle to define the rankingScoreOffset int value.
62      *
63      * <p>The rankingScoreOffset is used when calculating the ranking score used to rank networks
64      * against one another. See {@link #calculateRankingScore} for more information.
65      */
66     public static final String ATTRIBUTES_KEY_RANKING_SCORE_OFFSET =
67             "android.net.attributes.key.RANKING_SCORE_OFFSET";
68 
69     /** A {@link NetworkKey} uniquely identifying this network. */
70     public final NetworkKey networkKey;
71 
72     /**
73      * The {@link RssiCurve} representing the scores for this network based on the RSSI.
74      *
75      * <p>This field is optional and may be set to null to indicate that no score is available for
76      * this network at this time. Such networks, along with networks for which the scorer has not
77      * responded, are always prioritized below scored networks, regardless of the score.
78      */
79     public final RssiCurve rssiCurve;
80 
81     /**
82      * A boolean value that indicates whether or not the network is believed to be metered.
83      *
84      * <p>A network can be classified as metered if the user would be
85      * sensitive to heavy data usage on that connection due to monetary costs,
86      * data limitations or battery/performance issues. A typical example would
87      * be a wifi connection where the user would be charged for usage.
88      */
89     public final boolean meteredHint;
90 
91     /**
92      * An additional collection of optional attributes set by
93      * the Network Recommendation Provider.
94      *
95      * @see #ATTRIBUTES_KEY_HAS_CAPTIVE_PORTAL
96      * @see #ATTRIBUTES_KEY_RANKING_SCORE_OFFSET
97      */
98     @Nullable
99     public final Bundle attributes;
100 
101     /**
102      * Construct a new {@link ScoredNetwork}.
103      *
104      * @param networkKey the {@link NetworkKey} uniquely identifying this network.
105      * @param rssiCurve the {@link RssiCurve} representing the scores for this network based on the
106      *     RSSI. This field is optional, and may be skipped to represent a network which the scorer
107      *     has opted not to score at this time. Passing a null value here is strongly preferred to
108      *     not returning any {@link ScoredNetwork} for a given {@link NetworkKey} because it
109      *     indicates to the system not to request scores for this network in the future, although
110      *     the scorer may choose to issue an out-of-band update at any time.
111      */
ScoredNetwork(NetworkKey networkKey, RssiCurve rssiCurve)112     public ScoredNetwork(NetworkKey networkKey, RssiCurve rssiCurve) {
113         this(networkKey, rssiCurve, false /* meteredHint */);
114     }
115 
116     /**
117      * Construct a new {@link ScoredNetwork}.
118      *
119      * @param networkKey the {@link NetworkKey} uniquely identifying this network.
120      * @param rssiCurve the {@link RssiCurve} representing the scores for this network based on the
121      *     RSSI. This field is optional, and may be skipped to represent a network which the scorer
122      *     has opted not to score at this time. Passing a null value here is strongly preferred to
123      *     not returning any {@link ScoredNetwork} for a given {@link NetworkKey} because it
124      *     indicates to the system not to request scores for this network in the future, although
125      *     the scorer may choose to issue an out-of-band update at any time.
126      * @param meteredHint A boolean value indicating whether or not the network is believed to be
127      *     metered.
128      */
ScoredNetwork(NetworkKey networkKey, RssiCurve rssiCurve, boolean meteredHint)129     public ScoredNetwork(NetworkKey networkKey, RssiCurve rssiCurve, boolean meteredHint) {
130         this(networkKey, rssiCurve, meteredHint, null /* attributes */);
131     }
132 
133     /**
134      * Construct a new {@link ScoredNetwork}.
135      *
136      * @param networkKey the {@link NetworkKey} uniquely identifying this network
137      * @param rssiCurve the {@link RssiCurve} representing the scores for this network based on the
138      *     RSSI. This field is optional, and may be skipped to represent a network which the scorer
139      *     has opted not to score at this time. Passing a null value here is strongly preferred to
140      *     not returning any {@link ScoredNetwork} for a given {@link NetworkKey} because it
141      *     indicates to the system not to request scores for this network in the future, although
142      *     the scorer may choose to issue an out-of-band update at any time.
143      * @param meteredHint a boolean value indicating whether or not the network is believed to be
144      *                    metered
145      * @param attributes optional provider specific attributes
146      */
ScoredNetwork(NetworkKey networkKey, RssiCurve rssiCurve, boolean meteredHint, @Nullable Bundle attributes)147     public ScoredNetwork(NetworkKey networkKey, RssiCurve rssiCurve, boolean meteredHint,
148             @Nullable Bundle attributes) {
149         this.networkKey = networkKey;
150         this.rssiCurve = rssiCurve;
151         this.meteredHint = meteredHint;
152         this.attributes = attributes;
153     }
154 
ScoredNetwork(Parcel in)155     private ScoredNetwork(Parcel in) {
156         networkKey = NetworkKey.CREATOR.createFromParcel(in);
157         if (in.readByte() == 1) {
158             rssiCurve = RssiCurve.CREATOR.createFromParcel(in);
159         } else {
160             rssiCurve = null;
161         }
162         meteredHint = (in.readByte() == 1);
163         attributes = in.readBundle();
164     }
165 
166     @Override
describeContents()167     public int describeContents() {
168         return 0;
169     }
170 
171     @Override
writeToParcel(Parcel out, int flags)172     public void writeToParcel(Parcel out, int flags) {
173         networkKey.writeToParcel(out, flags);
174         if (rssiCurve != null) {
175             out.writeByte((byte) 1);
176             rssiCurve.writeToParcel(out, flags);
177         } else {
178             out.writeByte((byte) 0);
179         }
180         out.writeByte((byte) (meteredHint ? 1 : 0));
181         out.writeBundle(attributes);
182     }
183 
184     @Override
equals(Object o)185     public boolean equals(Object o) {
186         if (this == o) return true;
187         if (o == null || getClass() != o.getClass()) return false;
188 
189         ScoredNetwork that = (ScoredNetwork) o;
190 
191         return Objects.equals(networkKey, that.networkKey)
192                 && Objects.equals(rssiCurve, that.rssiCurve)
193                 && Objects.equals(meteredHint, that.meteredHint)
194                 && bundleEquals(attributes, that.attributes);
195     }
196 
bundleEquals(Bundle bundle1, Bundle bundle2)197     private boolean bundleEquals(Bundle bundle1, Bundle bundle2) {
198         if (bundle1 == bundle2) {
199             return true;
200         }
201         if (bundle1 == null || bundle2 == null) {
202             return false;
203         }
204         if (bundle1.size() != bundle2.size()) {
205             return false;
206         }
207         Set<String> keys = bundle1.keySet();
208         for (String key : keys) {
209             Object value1 = bundle1.get(key);
210             Object value2 = bundle2.get(key);
211             if (!Objects.equals(value1, value2)) {
212                 return false;
213             }
214         }
215         return true;
216     }
217 
218     @Override
hashCode()219     public int hashCode() {
220         return Objects.hash(networkKey, rssiCurve, meteredHint, attributes);
221     }
222 
223     @Override
toString()224     public String toString() {
225         StringBuilder out = new StringBuilder(
226                 "ScoredNetwork{" +
227                 "networkKey=" + networkKey +
228                 ", rssiCurve=" + rssiCurve +
229                 ", meteredHint=" + meteredHint);
230         // calling isEmpty will unparcel the bundle so its contents can be converted to a string
231         if (attributes != null && !attributes.isEmpty()) {
232             out.append(", attributes=" + attributes);
233         }
234         out.append('}');
235         return out.toString();
236     }
237 
238     /**
239      * Returns true if a ranking score can be calculated for this network.
240      *
241      * @hide
242      */
hasRankingScore()243     public boolean hasRankingScore() {
244         return (rssiCurve != null)
245                 || (attributes != null
246                         && attributes.containsKey(ATTRIBUTES_KEY_RANKING_SCORE_OFFSET));
247     }
248 
249     /**
250      * Returns a ranking score for a given RSSI which can be used to comparatively
251      * rank networks.
252      *
253      * <p>The score obtained by the rssiCurve is bitshifted left by 8 bits to expand it to an
254      * integer and then the offset is added. If the addition operation overflows or underflows,
255      * Integer.MAX_VALUE and Integer.MIN_VALUE will be returned respectively.
256      *
257      * <p>{@link #hasRankingScore} should be called first to ensure this network is capable
258      * of returning a ranking score.
259      *
260      * @throws UnsupportedOperationException if there is no RssiCurve and no rankingScoreOffset
261      * for this network (hasRankingScore returns false).
262      *
263      * @hide
264      */
calculateRankingScore(int rssi)265     public int calculateRankingScore(int rssi) throws UnsupportedOperationException {
266         if (!hasRankingScore()) {
267             throw new UnsupportedOperationException(
268                     "Either rssiCurve or rankingScoreOffset is required to calculate the "
269                             + "ranking score");
270         }
271 
272         int offset = 0;
273         if (attributes != null) {
274              offset += attributes.getInt(ATTRIBUTES_KEY_RANKING_SCORE_OFFSET, 0 /* default */);
275         }
276 
277         int score = (rssiCurve == null) ? 0 : rssiCurve.lookupScore(rssi) << Byte.SIZE;
278 
279         try {
280             return Math.addExact(score, offset);
281         } catch (ArithmeticException e) {
282             return (score < 0) ? Integer.MIN_VALUE : Integer.MAX_VALUE;
283         }
284     }
285 
286     /**
287      * Return the {@link NetworkBadging.Badging} enum for this network for the given RSSI, derived from the
288      * badging curve.
289      *
290      * <p>If no badging curve is present, {@link #BADGE_NONE} will be returned.
291      *
292      * @param rssi The rssi level for which the badge should be calculated
293      */
294     @NetworkBadging.Badging
calculateBadge(int rssi)295     public int calculateBadge(int rssi) {
296         if (attributes != null && attributes.containsKey(ATTRIBUTES_KEY_BADGING_CURVE)) {
297             RssiCurve badgingCurve =
298                     attributes.getParcelable(ATTRIBUTES_KEY_BADGING_CURVE);
299             return badgingCurve.lookupScore(rssi);
300         }
301 
302         return NetworkBadging.BADGING_NONE;
303     }
304 
305     public static final @android.annotation.NonNull Parcelable.Creator<ScoredNetwork> CREATOR =
306             new Parcelable.Creator<ScoredNetwork>() {
307                 @Override
308                 public ScoredNetwork createFromParcel(Parcel in) {
309                     return new ScoredNetwork(in);
310                 }
311 
312                 @Override
313                 public ScoredNetwork[] newArray(int size) {
314                     return new ScoredNetwork[size];
315                 }
316             };
317 }
318