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