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 17package com.android.server.wifi; 18 19import android.net.wifi.ScanResult; 20import android.net.wifi.WifiConfiguration; 21import android.os.SystemClock; 22import android.util.Log; 23 24import java.util.ArrayList; 25import java.util.Collection; 26import java.util.Collections; 27import java.util.Comparator; 28import java.util.HashMap; 29 30/** 31 * Maps BSSIDs to their individual ScanDetails for a given WifiConfiguration. 32 */ 33public class ScanDetailCache { 34 35 private static final String TAG = "ScanDetailCache"; 36 private static final boolean DBG = false; 37 38 private final WifiConfiguration mConfig; 39 private final int mMaxSize; 40 private final int mTrimSize; 41 private final HashMap<String, ScanDetail> mMap; 42 43 /** 44 * Scan Detail cache associated with each configured network. 45 * 46 * The cache size is trimmed down to |trimSize| once it crosses the provided |maxSize|. 47 * Since this operation is relatively expensive, ensure that |maxSize| and |trimSize| are not 48 * too close to each other. |trimSize| should always be <= |maxSize|. 49 * 50 * @param config WifiConfiguration object corresponding to the network. 51 * @param maxSize Max size desired for the cache. 52 * @param trimSize Size to trim the cache down to once it reaches |maxSize|. 53 */ 54 ScanDetailCache(WifiConfiguration config, int maxSize, int trimSize) { 55 mConfig = config; 56 mMaxSize = maxSize; 57 mTrimSize = trimSize; 58 mMap = new HashMap(16, 0.75f); 59 } 60 61 void put(ScanDetail scanDetail) { 62 // First check if we have reached |maxSize|. if yes, trim it down to |trimSize|. 63 if (mMap.size() >= mMaxSize) { 64 trim(); 65 } 66 67 mMap.put(scanDetail.getBSSIDString(), scanDetail); 68 } 69 70 ScanResult get(String bssid) { 71 ScanDetail scanDetail = getScanDetail(bssid); 72 return scanDetail == null ? null : scanDetail.getScanResult(); 73 } 74 75 ScanDetail getScanDetail(String bssid) { 76 return mMap.get(bssid); 77 } 78 79 void remove(String bssid) { 80 mMap.remove(bssid); 81 } 82 83 int size() { 84 return mMap.size(); 85 } 86 87 boolean isEmpty() { 88 return size() == 0; 89 } 90 91 Collection<String> keySet() { 92 return mMap.keySet(); 93 } 94 95 Collection<ScanDetail> values() { 96 return mMap.values(); 97 } 98 99 /** 100 * Method to reduce the cache to |mTrimSize| size by removing the oldest entries. 101 * TODO: Investigate if this method can be further optimized. 102 */ 103 private void trim() { 104 int currentSize = mMap.size(); 105 if (currentSize < mTrimSize) { 106 return; // Nothing to trim 107 } 108 ArrayList<ScanDetail> list = new ArrayList<ScanDetail>(mMap.values()); 109 if (list.size() != 0) { 110 // Sort by descending timestamp 111 Collections.sort(list, new Comparator() { 112 public int compare(Object o1, Object o2) { 113 ScanDetail a = (ScanDetail) o1; 114 ScanDetail b = (ScanDetail) o2; 115 if (a.getSeen() > b.getSeen()) { 116 return 1; 117 } 118 if (a.getSeen() < b.getSeen()) { 119 return -1; 120 } 121 return a.getBSSIDString().compareTo(b.getBSSIDString()); 122 } 123 }); 124 } 125 for (int i = 0; i < currentSize - mTrimSize; i++) { 126 // Remove oldest results from scan cache 127 ScanDetail result = list.get(i); 128 mMap.remove(result.getBSSIDString()); 129 } 130 } 131 132 /* @hide */ 133 private ArrayList<ScanDetail> sort() { 134 ArrayList<ScanDetail> list = new ArrayList<ScanDetail>(mMap.values()); 135 if (list.size() != 0) { 136 Collections.sort(list, new Comparator() { 137 public int compare(Object o1, Object o2) { 138 ScanResult a = ((ScanDetail) o1).getScanResult(); 139 ScanResult b = ((ScanDetail) o2).getScanResult(); 140 if (a.numIpConfigFailures > b.numIpConfigFailures) { 141 return 1; 142 } 143 if (a.numIpConfigFailures < b.numIpConfigFailures) { 144 return -1; 145 } 146 if (a.seen > b.seen) { 147 return -1; 148 } 149 if (a.seen < b.seen) { 150 return 1; 151 } 152 if (a.level > b.level) { 153 return -1; 154 } 155 if (a.level < b.level) { 156 return 1; 157 } 158 return a.BSSID.compareTo(b.BSSID); 159 } 160 }); 161 } 162 return list; 163 } 164 165 /** 166 * Method to get cached scan results that are less than 'age' old. 167 * 168 * @param age long Time window of desired results. 169 * @return WifiConfiguration.Visibility matches in the given visibility 170 */ 171 public WifiConfiguration.Visibility getVisibilityByRssi(long age) { 172 WifiConfiguration.Visibility status = new WifiConfiguration.Visibility(); 173 174 long now_ms = System.currentTimeMillis(); 175 long now_elapsed_ms = SystemClock.elapsedRealtime(); 176 for (ScanDetail scanDetail : values()) { 177 ScanResult result = scanDetail.getScanResult(); 178 if (scanDetail.getSeen() == 0) { 179 continue; 180 } 181 182 if (result.is5GHz()) { 183 //strictly speaking: [4915, 5825] 184 //number of known BSSID on 5GHz band 185 status.num5 = status.num5 + 1; 186 } else if (result.is24GHz()) { 187 //strictly speaking: [2412, 2482] 188 //number of known BSSID on 2.4Ghz band 189 status.num24 = status.num24 + 1; 190 } 191 192 if (result.timestamp != 0) { 193 if (DBG) { 194 Log.e("getVisibilityByRssi", " considering " + result.SSID + " " + result.BSSID 195 + " elapsed=" + now_elapsed_ms + " timestamp=" + result.timestamp 196 + " age = " + age); 197 } 198 if ((now_elapsed_ms - (result.timestamp / 1000)) > age) continue; 199 } else { 200 // This checks the time at which we have received the scan result from supplicant 201 if ((now_ms - result.seen) > age) continue; 202 } 203 204 if (result.is5GHz()) { 205 if (result.level > status.rssi5) { 206 status.rssi5 = result.level; 207 status.age5 = result.seen; 208 status.BSSID5 = result.BSSID; 209 } 210 } else if (result.is24GHz()) { 211 if (result.level > status.rssi24) { 212 status.rssi24 = result.level; 213 status.age24 = result.seen; 214 status.BSSID24 = result.BSSID; 215 } 216 } 217 } 218 219 return status; 220 } 221 222 /** 223 * Method to get scan matches for the desired time window. Returns matches by passpoint time if 224 * the WifiConfiguration is passpoint. 225 * 226 * @param age long desired time for matches. 227 * @return WifiConfiguration.Visibility matches in the given visibility 228 */ 229 public WifiConfiguration.Visibility getVisibility(long age) { 230 return getVisibilityByRssi(age); 231 } 232 233 234 @Override 235 public String toString() { 236 StringBuilder sbuf = new StringBuilder(); 237 sbuf.append("Scan Cache: ").append('\n'); 238 239 ArrayList<ScanDetail> list = sort(); 240 long now_ms = System.currentTimeMillis(); 241 if (list.size() > 0) { 242 for (ScanDetail scanDetail : list) { 243 ScanResult result = scanDetail.getScanResult(); 244 long milli = now_ms - scanDetail.getSeen(); 245 long ageSec = 0; 246 long ageMin = 0; 247 long ageHour = 0; 248 long ageMilli = 0; 249 long ageDay = 0; 250 if (now_ms > scanDetail.getSeen() && scanDetail.getSeen() > 0) { 251 ageMilli = milli % 1000; 252 ageSec = (milli / 1000) % 60; 253 ageMin = (milli / (60 * 1000)) % 60; 254 ageHour = (milli / (60 * 60 * 1000)) % 24; 255 ageDay = (milli / (24 * 60 * 60 * 1000)); 256 } 257 sbuf.append("{").append(result.BSSID).append(",").append(result.frequency); 258 sbuf.append(",").append(String.format("%3d", result.level)); 259 if (ageSec > 0 || ageMilli > 0) { 260 sbuf.append(String.format(",%4d.%02d.%02d.%02d.%03dms", ageDay, 261 ageHour, ageMin, ageSec, ageMilli)); 262 } 263 if (result.numIpConfigFailures > 0) { 264 sbuf.append(",ipfail="); 265 sbuf.append(result.numIpConfigFailures); 266 } 267 sbuf.append("} "); 268 } 269 sbuf.append('\n'); 270 } 271 272 return sbuf.toString(); 273 } 274 275} 276