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