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