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.wifi;
18
19import android.Manifest.permission;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.content.Context;
23import android.net.INetworkScoreCache;
24import android.net.NetworkKey;
25import android.net.ScoredNetwork;
26import android.os.Handler;
27import android.os.Process;
28import android.util.Log;
29import android.util.LruCache;
30
31import com.android.internal.annotations.GuardedBy;
32import com.android.internal.util.Preconditions;
33
34import java.io.FileDescriptor;
35import java.io.PrintWriter;
36import java.util.List;
37
38/**
39 * {@link INetworkScoreCache} implementation for Wifi Networks.
40 *
41 * @hide
42 */
43public class WifiNetworkScoreCache extends INetworkScoreCache.Stub {
44    private static final String TAG = "WifiNetworkScoreCache";
45    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
46
47    // A Network scorer returns a score in the range [-128, +127]
48    // We treat the lowest possible score as though there were no score, effectively allowing the
49    // scorer to provide an RSSI threshold below which a network should not be used.
50    public static final int INVALID_NETWORK_SCORE = Byte.MIN_VALUE;
51
52    /** Default number entries to be stored in the {@link LruCache}. */
53    private static final int DEFAULT_MAX_CACHE_SIZE = 100;
54
55    // See {@link #CacheListener}.
56    @Nullable
57    @GuardedBy("mLock")
58    private CacheListener mListener;
59
60    private final Context mContext;
61    private final Object mLock = new Object();
62
63    // The key is of the form "<ssid>"<bssid>
64    // TODO: What about SSIDs that can't be encoded as UTF-8?
65    @GuardedBy("mLock")
66    private final LruCache<String, ScoredNetwork> mCache;
67
68    public WifiNetworkScoreCache(Context context) {
69        this(context, null /* listener */);
70    }
71
72    /**
73     * Instantiates a WifiNetworkScoreCache.
74     *
75     * @param context Application context
76     * @param listener CacheListener for cache updates
77     */
78    public WifiNetworkScoreCache(Context context, @Nullable CacheListener listener) {
79        this(context, listener, DEFAULT_MAX_CACHE_SIZE);
80    }
81
82    public WifiNetworkScoreCache(
83            Context context, @Nullable CacheListener listener, int maxCacheSize) {
84        mContext = context.getApplicationContext();
85        mListener = listener;
86        mCache = new LruCache<>(maxCacheSize);
87    }
88
89    @Override public final void updateScores(List<ScoredNetwork> networks) {
90        if (networks == null || networks.isEmpty()) {
91           return;
92        }
93        if (DBG) {
94            Log.d(TAG, "updateScores list size=" + networks.size());
95        }
96
97        boolean changed = false;
98
99        synchronized(mLock) {
100            for (ScoredNetwork network : networks) {
101                String networkKey = buildNetworkKey(network);
102                if (networkKey == null) {
103                    if (DBG) {
104                        Log.d(TAG, "Failed to build network key for ScoredNetwork" + network);
105                    }
106                    continue;
107                }
108                mCache.put(networkKey, network);
109                changed = true;
110            }
111
112            if (mListener != null && changed) {
113                mListener.post(networks);
114            }
115        }
116    }
117
118    @Override public final void clearScores() {
119        synchronized (mLock) {
120            mCache.evictAll();
121        }
122    }
123
124    /**
125     * Returns whether there is any score info for the given ScanResult.
126     *
127     * This includes null-score info, so it should only be used when determining whether to request
128     * scores from the network scorer.
129     */
130    public boolean isScoredNetwork(ScanResult result) {
131        return getScoredNetwork(result) != null;
132    }
133
134    /**
135     * Returns whether there is a non-null score curve for the given ScanResult.
136     *
137     * A null score curve has special meaning - we should never connect to an ephemeral network if
138     * the score curve is null.
139     */
140    public boolean hasScoreCurve(ScanResult result) {
141        ScoredNetwork network = getScoredNetwork(result);
142        return network != null && network.rssiCurve != null;
143    }
144
145    public int getNetworkScore(ScanResult result) {
146        int score = INVALID_NETWORK_SCORE;
147
148        ScoredNetwork network = getScoredNetwork(result);
149        if (network != null && network.rssiCurve != null) {
150            score = network.rssiCurve.lookupScore(result.level);
151            if (DBG) {
152                Log.d(TAG, "getNetworkScore found scored network " + network.networkKey
153                        + " score " + Integer.toString(score)
154                        + " RSSI " + result.level);
155            }
156        }
157        return score;
158    }
159
160    /**
161     * Returns the ScoredNetwork metered hint for a given ScanResult.
162     *
163     * If there is no ScoredNetwork associated with the ScanResult then false will be returned.
164     */
165    public boolean getMeteredHint(ScanResult result) {
166        ScoredNetwork network = getScoredNetwork(result);
167        return network != null && network.meteredHint;
168    }
169
170    public int getNetworkScore(ScanResult result, boolean isActiveNetwork) {
171        int score = INVALID_NETWORK_SCORE;
172
173        ScoredNetwork network = getScoredNetwork(result);
174        if (network != null && network.rssiCurve != null) {
175            score = network.rssiCurve.lookupScore(result.level, isActiveNetwork);
176            if (DBG) {
177                Log.d(TAG, "getNetworkScore found scored network " + network.networkKey
178                        + " score " + Integer.toString(score)
179                        + " RSSI " + result.level
180                        + " isActiveNetwork " + isActiveNetwork);
181            }
182        }
183        return score;
184    }
185
186    @Nullable
187    public ScoredNetwork getScoredNetwork(ScanResult result) {
188        String key = buildNetworkKey(result);
189        if (key == null) return null;
190
191        synchronized(mLock) {
192            ScoredNetwork network = mCache.get(key);
193            return network;
194        }
195    }
196
197    /** Returns the ScoredNetwork for the given key. */
198    @Nullable
199    public ScoredNetwork getScoredNetwork(NetworkKey networkKey) {
200        String key = buildNetworkKey(networkKey);
201        if (key == null) {
202            if (DBG) {
203                Log.d(TAG, "Could not build key string for Network Key: " + networkKey);
204            }
205            return null;
206        }
207        synchronized (mLock) {
208            return mCache.get(key);
209        }
210    }
211
212    private String buildNetworkKey(ScoredNetwork network) {
213        if (network == null) {
214            return null;
215        }
216        return buildNetworkKey(network.networkKey);
217    }
218
219    private String buildNetworkKey(NetworkKey networkKey) {
220        if (networkKey == null) {
221            return null;
222        }
223        if (networkKey.wifiKey == null) return null;
224        if (networkKey.type == NetworkKey.TYPE_WIFI) {
225            String key = networkKey.wifiKey.ssid;
226            if (key == null) return null;
227            if (networkKey.wifiKey.bssid != null) {
228                key = key + networkKey.wifiKey.bssid;
229            }
230            return key;
231        }
232        return null;
233    }
234
235    private String buildNetworkKey(ScanResult result) {
236        if (result == null || result.SSID == null) {
237            return null;
238        }
239        StringBuilder key = new StringBuilder("\"");
240        key.append(result.SSID);
241        key.append("\"");
242        if (result.BSSID != null) {
243            key.append(result.BSSID);
244        }
245        return key.toString();
246    }
247
248    @Override protected final void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
249        mContext.enforceCallingOrSelfPermission(permission.DUMP, TAG);
250        String header = String.format("WifiNetworkScoreCache (%s/%d)",
251                mContext.getPackageName(), Process.myUid());
252        writer.println(header);
253        writer.println("  All score curves:");
254        synchronized (mLock) {
255            for (ScoredNetwork score : mCache.snapshot().values()) {
256                writer.println("    " + score);
257            }
258            writer.println("  Network scores for latest ScanResults:");
259            WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
260            for (ScanResult scanResult : wifiManager.getScanResults()) {
261                writer.println(
262                        "    " + buildNetworkKey(scanResult) + ": " + getNetworkScore(scanResult));
263            }
264        }
265    }
266
267    /** Registers a CacheListener instance, replacing the previous listener if it existed. */
268    public void registerListener(CacheListener listener) {
269        synchronized (mLock) {
270            mListener = listener;
271        }
272    }
273
274    /** Removes the registered CacheListener. */
275    public void unregisterListener() {
276        synchronized (mLock) {
277            mListener = null;
278        }
279    }
280
281    /** Listener for updates to the cache inside WifiNetworkScoreCache. */
282    public abstract static class CacheListener {
283        private Handler mHandler;
284
285        /**
286         * Constructor for CacheListener.
287         *
288         * @param handler the Handler on which to invoke the {@link #networkCacheUpdated} method.
289         *          This cannot be null.
290         */
291        public CacheListener(@NonNull Handler handler) {
292            Preconditions.checkNotNull(handler);
293            mHandler = handler;
294        }
295
296        /** Invokes the {@link #networkCacheUpdated(List<ScoredNetwork>)} method on the handler. */
297        void post(List<ScoredNetwork> updatedNetworks) {
298            mHandler.post(new Runnable() {
299                @Override
300                public void run() {
301                    networkCacheUpdated(updatedNetworks);
302                }
303            });
304        }
305
306        /**
307         * Invoked whenever the cache is updated.
308         *
309         * <p>Clearing the cache does not invoke this method.
310         *
311         * @param updatedNetworks the networks that were updated
312         */
313        public abstract void networkCacheUpdated(List<ScoredNetwork> updatedNetworks);
314    }
315}
316