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