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