1 /*
2  * Copyright (C) 2015 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.net.wifi.ScanResult;
20 import android.net.wifi.WifiConfiguration;
21 import android.os.SystemClock;
22 import android.util.Log;
23 
24 import com.android.server.wifi.hotspot2.PasspointMatch;
25 import com.android.server.wifi.hotspot2.PasspointMatchInfo;
26 import com.android.server.wifi.hotspot2.pps.HomeSP;
27 
28 import java.util.ArrayList;
29 import java.util.Collection;
30 import java.util.Collections;
31 import java.util.Comparator;
32 import java.util.Iterator;
33 import java.util.concurrent.ConcurrentHashMap;
34 
35 /**
36  * Maps BSSIDs to their individual ScanDetails for a given WifiConfiguration.
37  */
38 public class ScanDetailCache {
39 
40     private static final String TAG = "ScanDetailCache";
41     private static final boolean DBG = false;
42 
43     private WifiConfiguration mConfig;
44     private ConcurrentHashMap<String, ScanDetail> mMap;
45     private ConcurrentHashMap<String, PasspointMatchInfo> mPasspointMatches;
46 
ScanDetailCache(WifiConfiguration config)47     ScanDetailCache(WifiConfiguration config) {
48         mConfig = config;
49         mMap = new ConcurrentHashMap(16, 0.75f, 2);
50         mPasspointMatches = new ConcurrentHashMap(16, 0.75f, 2);
51     }
52 
put(ScanDetail scanDetail)53     void put(ScanDetail scanDetail) {
54         put(scanDetail, null, null);
55     }
56 
put(ScanDetail scanDetail, PasspointMatch match, HomeSP homeSp)57     void put(ScanDetail scanDetail, PasspointMatch match, HomeSP homeSp) {
58 
59         mMap.put(scanDetail.getBSSIDString(), scanDetail);
60 
61         if (match != null && homeSp != null) {
62             mPasspointMatches.put(scanDetail.getBSSIDString(),
63                     new PasspointMatchInfo(match, scanDetail, homeSp));
64         }
65     }
66 
get(String bssid)67     ScanResult get(String bssid) {
68         ScanDetail scanDetail = getScanDetail(bssid);
69         return scanDetail == null ? null : scanDetail.getScanResult();
70     }
71 
getScanDetail(String bssid)72     ScanDetail getScanDetail(String bssid) {
73         return mMap.get(bssid);
74     }
75 
remove(String bssid)76     void remove(String bssid) {
77         mMap.remove(bssid);
78     }
79 
size()80     int size() {
81         return mMap.size();
82     }
83 
isEmpty()84     boolean isEmpty() {
85         return size() == 0;
86     }
87 
getFirst()88     ScanDetail getFirst() {
89         Iterator<ScanDetail> it = mMap.values().iterator();
90         return it.hasNext() ? it.next() : null;
91     }
92 
keySet()93     Collection<String> keySet() {
94         return mMap.keySet();
95     }
96 
values()97     Collection<ScanDetail> values() {
98         return mMap.values();
99     }
100 
101     /**
102      * Method to reduce the cache to the given size by removing the oldest entries.
103      *
104      * @param num int target cache size
105      */
trim(int num)106     public void trim(int num) {
107         int currentSize = mMap.size();
108         if (currentSize <= num) {
109             return; // Nothing to trim
110         }
111         ArrayList<ScanDetail> list = new ArrayList<ScanDetail>(mMap.values());
112         if (list.size() != 0) {
113             // Sort by descending timestamp
114             Collections.sort(list, new Comparator() {
115                 public int compare(Object o1, Object o2) {
116                     ScanDetail a = (ScanDetail) o1;
117                     ScanDetail b = (ScanDetail) o2;
118                     if (a.getSeen() > b.getSeen()) {
119                         return 1;
120                     }
121                     if (a.getSeen() < b.getSeen()) {
122                         return -1;
123                     }
124                     return a.getBSSIDString().compareTo(b.getBSSIDString());
125                 }
126             });
127         }
128         for (int i = 0; i < currentSize - num; i++) {
129             // Remove oldest results from scan cache
130             ScanDetail result = list.get(i);
131             mMap.remove(result.getBSSIDString());
132             mPasspointMatches.remove(result.getBSSIDString());
133         }
134     }
135 
136     /* @hide */
sort()137     private ArrayList<ScanDetail> sort() {
138         ArrayList<ScanDetail> list = new ArrayList<ScanDetail>(mMap.values());
139         if (list.size() != 0) {
140             Collections.sort(list, new Comparator() {
141                 public int compare(Object o1, Object o2) {
142                     ScanResult a = ((ScanDetail) o1).getScanResult();
143                     ScanResult b = ((ScanDetail) o2).getScanResult();
144                     if (a.numIpConfigFailures > b.numIpConfigFailures) {
145                         return 1;
146                     }
147                     if (a.numIpConfigFailures < b.numIpConfigFailures) {
148                         return -1;
149                     }
150                     if (a.seen > b.seen) {
151                         return -1;
152                     }
153                     if (a.seen < b.seen) {
154                         return 1;
155                     }
156                     if (a.level > b.level) {
157                         return -1;
158                     }
159                     if (a.level < b.level) {
160                         return 1;
161                     }
162                     return a.BSSID.compareTo(b.BSSID);
163                 }
164             });
165         }
166         return list;
167     }
168 
169     /**
170      * Method to get cached scan results that are less than 'age' old.
171      *
172      * @param age long Time window of desired results.
173      * @return WifiConfiguration.Visibility matches in the given visibility
174      */
getVisibilityByRssi(long age)175     public WifiConfiguration.Visibility getVisibilityByRssi(long age) {
176         WifiConfiguration.Visibility status = new WifiConfiguration.Visibility();
177 
178         long now_ms = System.currentTimeMillis();
179         long now_elapsed_ms = SystemClock.elapsedRealtime();
180         for (ScanDetail scanDetail : values()) {
181             ScanResult result = scanDetail.getScanResult();
182             if (scanDetail.getSeen() == 0) {
183                 continue;
184             }
185 
186             if (result.is5GHz()) {
187                 //strictly speaking: [4915, 5825]
188                 //number of known BSSID on 5GHz band
189                 status.num5 = status.num5 + 1;
190             } else if (result.is24GHz()) {
191                 //strictly speaking: [2412, 2482]
192                 //number of known BSSID on 2.4Ghz band
193                 status.num24 = status.num24 + 1;
194             }
195 
196             if (result.timestamp != 0) {
197                 if (DBG) {
198                     Log.e("getVisibilityByRssi", " considering " + result.SSID + " " + result.BSSID
199                             + " elapsed=" + now_elapsed_ms + " timestamp=" + result.timestamp
200                             + " age = " + age);
201                 }
202                 if ((now_elapsed_ms - (result.timestamp / 1000)) > age) continue;
203             } else {
204                 // This checks the time at which we have received the scan result from supplicant
205                 if ((now_ms - result.seen) > age) continue;
206             }
207 
208             if (result.is5GHz()) {
209                 if (result.level > status.rssi5) {
210                     status.rssi5 = result.level;
211                     status.age5 = result.seen;
212                     status.BSSID5 = result.BSSID;
213                 }
214             } else if (result.is24GHz()) {
215                 if (result.level > status.rssi24) {
216                     status.rssi24 = result.level;
217                     status.age24 = result.seen;
218                     status.BSSID24 = result.BSSID;
219                 }
220             }
221         }
222 
223         return status;
224     }
225 
226     /**
227      * Method returning the Visibility based on passpoint match time.
228      *
229      * @param age long Desired time window for matches.
230      * @return WifiConfiguration.Visibility matches in the given visibility
231      */
getVisibilityByPasspointMatch(long age)232     public WifiConfiguration.Visibility getVisibilityByPasspointMatch(long age) {
233 
234         long now_ms = System.currentTimeMillis();
235         PasspointMatchInfo pmiBest24 = null, pmiBest5 = null;
236 
237         for (PasspointMatchInfo pmi : mPasspointMatches.values()) {
238             ScanDetail scanDetail = pmi.getScanDetail();
239             if (scanDetail == null) continue;
240             ScanResult result = scanDetail.getScanResult();
241             if (result == null) continue;
242 
243             if (scanDetail.getSeen() == 0) continue;
244 
245             if ((now_ms - result.seen) > age) continue;
246 
247             if (result.is5GHz()) {
248                 if (pmiBest5 == null || pmiBest5.compareTo(pmi) < 0) {
249                     pmiBest5 = pmi;
250                 }
251             } else if (result.is24GHz()) {
252                 if (pmiBest24 == null || pmiBest24.compareTo(pmi) < 0) {
253                     pmiBest24 = pmi;
254                 }
255             }
256         }
257 
258         WifiConfiguration.Visibility status = new WifiConfiguration.Visibility();
259         String logMsg = "Visiblity by passpoint match returned ";
260         if (pmiBest5 != null) {
261             ScanResult result = pmiBest5.getScanDetail().getScanResult();
262             status.rssi5 = result.level;
263             status.age5 = result.seen;
264             status.BSSID5 = result.BSSID;
265             logMsg += "5 GHz BSSID of " + result.BSSID;
266         }
267         if (pmiBest24 != null) {
268             ScanResult result = pmiBest24.getScanDetail().getScanResult();
269             status.rssi24 = result.level;
270             status.age24 = result.seen;
271             status.BSSID24 = result.BSSID;
272             logMsg += "2.4 GHz BSSID of " + result.BSSID;
273         }
274 
275         Log.d(TAG, logMsg);
276 
277         return status;
278     }
279 
280     /**
281      * Method to get scan matches for the desired time window.  Returns matches by passpoint time if
282      * the WifiConfiguration is passpoint.
283      *
284      * @param age long desired time for matches.
285      * @return WifiConfiguration.Visibility matches in the given visibility
286      */
getVisibility(long age)287     public WifiConfiguration.Visibility getVisibility(long age) {
288         if (mConfig.isPasspoint()) {
289             return getVisibilityByPasspointMatch(age);
290         } else {
291             return getVisibilityByRssi(age);
292         }
293     }
294 
295 
296 
297     @Override
toString()298     public String toString() {
299         StringBuilder sbuf = new StringBuilder();
300         sbuf.append("Scan Cache:  ").append('\n');
301 
302         ArrayList<ScanDetail> list = sort();
303         long now_ms = System.currentTimeMillis();
304         if (list.size() > 0) {
305             for (ScanDetail scanDetail : list) {
306                 ScanResult result = scanDetail.getScanResult();
307                 long milli = now_ms - scanDetail.getSeen();
308                 long ageSec = 0;
309                 long ageMin = 0;
310                 long ageHour = 0;
311                 long ageMilli = 0;
312                 long ageDay = 0;
313                 if (now_ms > scanDetail.getSeen() && scanDetail.getSeen() > 0) {
314                     ageMilli = milli % 1000;
315                     ageSec   = (milli / 1000) % 60;
316                     ageMin   = (milli / (60 * 1000)) % 60;
317                     ageHour  = (milli / (60 * 60 * 1000)) % 24;
318                     ageDay   = (milli / (24 * 60 * 60 * 1000));
319                 }
320                 sbuf.append("{").append(result.BSSID).append(",").append(result.frequency);
321                 sbuf.append(",").append(String.format("%3d", result.level));
322                 if (ageSec > 0 || ageMilli > 0) {
323                     sbuf.append(String.format(",%4d.%02d.%02d.%02d.%03dms", ageDay,
324                             ageHour, ageMin, ageSec, ageMilli));
325                 }
326                 if (result.numIpConfigFailures > 0) {
327                     sbuf.append(",ipfail=");
328                     sbuf.append(result.numIpConfigFailures);
329                 }
330                 sbuf.append("} ");
331             }
332             sbuf.append('\n');
333         }
334 
335         return sbuf.toString();
336     }
337 
338 }
339