1 /*
2  * Copyright (C) 2019 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 com.android.server.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.NetworkKey;
24 import android.net.NetworkScoreManager;
25 import android.net.ScoredNetwork;
26 import android.net.wifi.ScanResult;
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.ArrayList;
38 import java.util.Collection;
39 import java.util.List;
40 
41 /**
42  * {@link NetworkScoreManager.NetworkScoreCallback} implementation for Wifi Networks.
43  *
44  * Note: This is a copy of WifiNetworkScoreCache for internal usage by the wifi stack. This extends
45  * the formal API: {@link NetworkScoreManager.NetworkScoreCallback}
46  */
47 public class WifiNetworkScoreCache extends NetworkScoreManager.NetworkScoreCallback {
48     private static final String TAG = "WifiNetworkScoreCache";
49     private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
50 
51     // A Network scorer returns a score in the range [-128, +127]
52     // We treat the lowest possible score as though there were no score, effectively allowing the
53     // scorer to provide an RSSI threshold below which a network should not be used.
54     public static final int INVALID_NETWORK_SCORE = Byte.MIN_VALUE;
55 
56     /** Default number entries to be stored in the {@link LruCache}. */
57     private static final int DEFAULT_MAX_CACHE_SIZE = 100;
58 
59     // See {@link #CacheListener}.
60     @Nullable
61     @GuardedBy("mLock")
62     private CacheListener mListener;
63 
64     private final Context mContext;
65     private final Object mLock = new Object();
66 
67     // The key is of the form "<ssid>"<bssid>
68     // TODO: What about SSIDs that can't be encoded as UTF-8?
69     @GuardedBy("mLock")
70     private final LruCache<String, ScoredNetwork> mCache;
71 
WifiNetworkScoreCache(Context context)72     public WifiNetworkScoreCache(Context context) {
73         this(context, null /* listener */);
74     }
75 
76     /**
77      * Instantiates a WifiNetworkScoreCache.
78      *
79      * @param context Application context
80      * @param listener CacheListener for cache updates
81      */
WifiNetworkScoreCache(Context context, @Nullable CacheListener listener)82     public WifiNetworkScoreCache(Context context, @Nullable CacheListener listener) {
83         this(context, listener, DEFAULT_MAX_CACHE_SIZE);
84     }
85 
WifiNetworkScoreCache( Context context, @Nullable CacheListener listener, int maxCacheSize)86     public WifiNetworkScoreCache(
87             Context context, @Nullable CacheListener listener, int maxCacheSize) {
88         mContext = context.getApplicationContext();
89         mListener = listener;
90         mCache = new LruCache<>(maxCacheSize);
91     }
92 
93     @Override
onScoresUpdated(Collection<ScoredNetwork> networks)94     public final void onScoresUpdated(Collection<ScoredNetwork> networks) {
95         if (networks == null || networks.isEmpty()) {
96             return;
97         }
98         if (DBG) {
99             Log.d(TAG, "updateScores list size=" + networks.size());
100         }
101 
102         boolean changed = false;
103 
104         synchronized (mLock) {
105             for (ScoredNetwork network : networks) {
106                 String networkKey = buildNetworkKey(network);
107                 if (networkKey == null) {
108                     if (DBG) {
109                         Log.d(TAG, "Failed to build network key for ScoredNetwork" + network);
110                     }
111                     continue;
112                 }
113                 mCache.put(networkKey, network);
114                 changed = true;
115             }
116 
117             if (mListener != null && changed) {
118                 mListener.post(new ArrayList<>(networks));
119             }
120         }
121     }
122 
123     @Override
onScoresInvalidated()124     public final void onScoresInvalidated() {
125         synchronized (mLock) {
126             mCache.evictAll();
127         }
128     }
129 
130     /**
131      * Returns whether there is any score info for the given ScanResult.
132      *
133      * This includes null-score info, so it should only be used when determining whether to request
134      * scores from the network scorer.
135      */
isScoredNetwork(ScanResult result)136     public boolean isScoredNetwork(ScanResult result) {
137         return getScoredNetwork(result) != null;
138     }
139 
140     /**
141      * Returns whether there is a non-null score curve for the given ScanResult.
142      *
143      * A null score curve has special meaning - we should never connect to an ephemeral network if
144      * the score curve is null.
145      */
hasScoreCurve(ScanResult result)146     public boolean hasScoreCurve(ScanResult result) {
147         ScoredNetwork network = getScoredNetwork(result);
148         return network != null && network.rssiCurve != null;
149     }
150 
getNetworkScore(ScanResult result)151     private int getNetworkScore(ScanResult result) {
152         int score = INVALID_NETWORK_SCORE;
153 
154         ScoredNetwork network = getScoredNetwork(result);
155         if (network != null && network.rssiCurve != null) {
156             score = network.rssiCurve.lookupScore(result.level);
157             if (DBG) {
158                 Log.d(TAG, "getNetworkScore found scored network " + network.networkKey
159                         + " score " + Integer.toString(score)
160                         + " RSSI " + result.level);
161             }
162         }
163         return score;
164     }
165 
166     /**
167      * Returns the ScoredNetwork metered hint for a given ScanResult.
168      *
169      * If there is no ScoredNetwork associated with the ScanResult then false will be returned.
170      */
getMeteredHint(ScanResult result)171     public boolean getMeteredHint(ScanResult result) {
172         ScoredNetwork network = getScoredNetwork(result);
173         return network != null && network.meteredHint;
174     }
175 
176     /**
177      * Get network score for the provided ScanResult.
178      */
getNetworkScore(ScanResult result, boolean isActiveNetwork)179     public int getNetworkScore(ScanResult result, boolean isActiveNetwork) {
180         int score = INVALID_NETWORK_SCORE;
181 
182         ScoredNetwork network = getScoredNetwork(result);
183         if (network != null && network.rssiCurve != null) {
184             score = network.rssiCurve.lookupScore(result.level, isActiveNetwork);
185             if (DBG) {
186                 Log.d(TAG, "getNetworkScore found scored network " + network.networkKey
187                         + " score " + Integer.toString(score)
188                         + " RSSI " + result.level
189                         + " isActiveNetwork " + isActiveNetwork);
190             }
191         }
192         return score;
193     }
194 
195     @Nullable
getScoredNetwork(ScanResult result)196     private ScoredNetwork getScoredNetwork(ScanResult result) {
197         String key = buildNetworkKey(result);
198         if (key == null) return null;
199 
200         synchronized (mLock) {
201             ScoredNetwork network = mCache.get(key);
202             return network;
203         }
204     }
205 
206     /** Returns the ScoredNetwork for the given key. */
207     @Nullable
getScoredNetwork(NetworkKey networkKey)208     public ScoredNetwork getScoredNetwork(NetworkKey networkKey) {
209         String key = buildNetworkKey(networkKey);
210         if (key == null) {
211             if (DBG) {
212                 Log.d(TAG, "Could not build key string for Network Key: " + networkKey);
213             }
214             return null;
215         }
216         synchronized (mLock) {
217             return mCache.get(key);
218         }
219     }
220 
buildNetworkKey(ScoredNetwork network)221     private String buildNetworkKey(ScoredNetwork network) {
222         if (network == null) {
223             return null;
224         }
225         return buildNetworkKey(network.networkKey);
226     }
227 
buildNetworkKey(NetworkKey networkKey)228     private String buildNetworkKey(NetworkKey networkKey) {
229         if (networkKey == null) {
230             return null;
231         }
232         if (networkKey.wifiKey == null) return null;
233         if (networkKey.type == NetworkKey.TYPE_WIFI) {
234             String key = networkKey.wifiKey.ssid;
235             if (key == null) return null;
236             if (networkKey.wifiKey.bssid != null) {
237                 key = key + networkKey.wifiKey.bssid;
238             }
239             return key;
240         }
241         return null;
242     }
243 
buildNetworkKey(ScanResult result)244     private String buildNetworkKey(ScanResult result) {
245         if (result == null || result.SSID == null) {
246             return null;
247         }
248         StringBuilder key = new StringBuilder("\"");
249         key.append(result.SSID);
250         key.append("\"");
251         if (result.BSSID != null) {
252             key.append(result.BSSID);
253         }
254         return key.toString();
255     }
256 
257     /**
258      * This is directly invoked from within Wifi-Service (on it's instance of this class), hence
259      * avoid making the WifiManager.getScanResults() call to avoid a deadlock.
260      */
dumpWithLatestScanResults( FileDescriptor fd, PrintWriter writer, String[] args, List<ScanResult> latestScanResults)261     public final void dumpWithLatestScanResults(
262             FileDescriptor fd, PrintWriter writer, String[] args,
263             List<ScanResult> latestScanResults) {
264         mContext.enforceCallingOrSelfPermission(permission.DUMP, TAG);
265         String header = String.format("WifiNetworkScoreCache (%s/%d)",
266                 mContext.getPackageName(), Process.myUid());
267         writer.println(header);
268         writer.println("  All score curves:");
269         synchronized (mLock) {
270             for (ScoredNetwork score : mCache.snapshot().values()) {
271                 writer.println("    " + score);
272             }
273             writer.println("  Network scores for latest ScanResults:");
274             for (ScanResult scanResult : latestScanResults) {
275                 writer.println(
276                         "    " + buildNetworkKey(scanResult) + ": " + getNetworkScore(scanResult));
277             }
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