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.wifi;
18 
19 import android.Manifest.permission;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.content.Context;
23 import android.net.INetworkScoreCache;
24 import android.net.NetworkKey;
25 import android.net.ScoredNetwork;
26 import android.os.Handler;
27 import android.os.Process;
28 import android.util.Log;
29 import android.util.LruCache;
30 
31 import com.android.internal.annotations.GuardedBy;
32 import com.android.internal.util.Preconditions;
33 
34 import java.io.FileDescriptor;
35 import java.io.PrintWriter;
36 import java.util.List;
37 
38 /**
39  * {@link INetworkScoreCache} implementation for Wifi Networks.
40  *
41  * @hide
42  */
43 public 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 
WifiNetworkScoreCache(Context context)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      */
WifiNetworkScoreCache(Context context, @Nullable CacheListener listener)78     public WifiNetworkScoreCache(Context context, @Nullable CacheListener listener) {
79         this(context, listener, DEFAULT_MAX_CACHE_SIZE);
80     }
81 
WifiNetworkScoreCache( Context context, @Nullable CacheListener listener, int maxCacheSize)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 
updateScores(List<ScoredNetwork> networks)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 
clearScores()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      */
isScoredNetwork(ScanResult result)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      */
hasScoreCurve(ScanResult result)140     public boolean hasScoreCurve(ScanResult result) {
141         ScoredNetwork network = getScoredNetwork(result);
142         return network != null && network.rssiCurve != null;
143     }
144 
getNetworkScore(ScanResult result)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      */
getMeteredHint(ScanResult result)165     public boolean getMeteredHint(ScanResult result) {
166         ScoredNetwork network = getScoredNetwork(result);
167         return network != null && network.meteredHint;
168     }
169 
getNetworkScore(ScanResult result, boolean isActiveNetwork)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
getScoredNetwork(ScanResult result)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
getScoredNetwork(NetworkKey networkKey)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 
buildNetworkKey(ScoredNetwork network)212     private String buildNetworkKey(ScoredNetwork network) {
213         if (network == null) {
214             return null;
215         }
216         return buildNetworkKey(network.networkKey);
217     }
218 
buildNetworkKey(NetworkKey networkKey)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 
buildNetworkKey(ScanResult result)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 
dump(FileDescriptor fd, PrintWriter writer, String[] args)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. */
registerListener(CacheListener listener)268     public void registerListener(CacheListener listener) {
269         synchronized (mLock) {
270             mListener = listener;
271         }
272     }
273 
274     /** Removes the registered CacheListener. */
unregisterListener()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          */
CacheListener(@onNull Handler handler)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. */
post(List<ScoredNetwork> updatedNetworks)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          */
networkCacheUpdated(List<ScoredNetwork> updatedNetworks)313         public abstract void networkCacheUpdated(List<ScoredNetwork> updatedNetworks);
314     }
315 }
316