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
17package android.net;
18
19import android.annotation.Nullable;
20import android.annotation.SystemApi;
21import android.os.Bundle;
22import android.os.Parcel;
23import android.os.Parcelable;
24
25import java.util.Objects;
26import 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
34public 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     */
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     */
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     */
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
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
167    public int describeContents() {
168        return 0;
169    }
170
171    @Override
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
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
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
219    public int hashCode() {
220        return Objects.hash(networkKey, rssiCurve, meteredHint, attributes);
221    }
222
223    @Override
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     */
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     */
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
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 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