1 package com.android.hotspot2.osu; 2 3 import android.util.Log; 4 5 import com.android.anqp.HSIconFileElement; 6 import com.android.anqp.IconInfo; 7 import com.android.hotspot2.Utils; 8 9 import java.net.ProtocolException; 10 import java.nio.BufferUnderflowException; 11 import java.nio.ByteBuffer; 12 import java.nio.ByteOrder; 13 import java.util.ArrayList; 14 import java.util.Arrays; 15 import java.util.Collections; 16 import java.util.HashMap; 17 import java.util.Iterator; 18 import java.util.LinkedHashMap; 19 import java.util.LinkedList; 20 import java.util.List; 21 import java.util.Map; 22 23 import static com.android.anqp.Constants.ANQPElementType.HSIconFile; 24 25 public class IconCache extends Thread { 26 private static final int CacheSize = 64; 27 private static final int RetryCount = 3; 28 29 private final OSUManager mOSUManager; 30 private final Map<Long, LinkedList<QuerySet>> mBssQueues = new HashMap<>(); 31 32 private final Map<IconKey, HSIconFileElement> mCache = 33 new LinkedHashMap<IconKey, HSIconFileElement>() { 34 @Override 35 protected boolean removeEldestEntry(Map.Entry eldest) { 36 return size() > CacheSize; 37 } 38 }; 39 40 private static class IconKey { 41 private final long mBSSID; 42 private final long mHESSID; 43 private final String mSSID; 44 private final int mAnqpDomID; 45 private final String mFileName; 46 IconKey(OSUInfo osuInfo, String fileName)47 private IconKey(OSUInfo osuInfo, String fileName) { 48 mBSSID = osuInfo.getBSSID(); 49 mHESSID = osuInfo.getHESSID(); 50 mSSID = osuInfo.getAdvertisingSSID(); 51 mAnqpDomID = osuInfo.getAnqpDomID(); 52 mFileName = fileName; 53 } 54 getFileName()55 public String getFileName() { 56 return mFileName; 57 } 58 59 @Override equals(Object thatObject)60 public boolean equals(Object thatObject) { 61 if (this == thatObject) { 62 return true; 63 } 64 if (thatObject == null || getClass() != thatObject.getClass()) { 65 return false; 66 } 67 68 IconKey that = (IconKey) thatObject; 69 70 return mFileName.equals(that.mFileName) && ((mBSSID == that.mBSSID) || 71 ((mAnqpDomID == that.mAnqpDomID) && (mAnqpDomID != 0) && 72 (mHESSID == that.mHESSID) && ((mHESSID != 0) 73 || mSSID.equals(that.mSSID)))); 74 } 75 76 @Override hashCode()77 public int hashCode() { 78 int result = (int) (mBSSID ^ (mBSSID >>> 32)); 79 result = 31 * result + (int) (mHESSID ^ (mHESSID >>> 32)); 80 result = 31 * result + mSSID.hashCode(); 81 result = 31 * result + mAnqpDomID; 82 result = 31 * result + mFileName.hashCode(); 83 return result; 84 } 85 86 @Override toString()87 public String toString() { 88 return String.format("%012x:%012x '%s' [%d] + '%s'", 89 mBSSID, mHESSID, mSSID, mAnqpDomID, mFileName); 90 } 91 } 92 93 private static class QueryEntry { 94 private final IconKey mKey; 95 private int mRetry; 96 private long mLastSent; 97 QueryEntry(IconKey key)98 private QueryEntry(IconKey key) { 99 mKey = key; 100 mLastSent = System.currentTimeMillis(); 101 } 102 getKey()103 private IconKey getKey() { 104 return mKey; 105 } 106 bumpRetry()107 private int bumpRetry() { 108 mLastSent = System.currentTimeMillis(); 109 return mRetry++; 110 } 111 age(long now)112 private long age(long now) { 113 return now - mLastSent; 114 } 115 116 @Override toString()117 public String toString() { 118 return String.format("Entry %s, retry %d", mKey, mRetry); 119 } 120 } 121 122 private static class QuerySet { 123 private final OSUInfo mOsuInfo; 124 private final LinkedList<QueryEntry> mEntries; 125 QuerySet(OSUInfo osuInfo, List<IconInfo> icons)126 private QuerySet(OSUInfo osuInfo, List<IconInfo> icons) { 127 mOsuInfo = osuInfo; 128 mEntries = new LinkedList<>(); 129 for (IconInfo iconInfo : icons) { 130 mEntries.addLast(new QueryEntry(new IconKey(osuInfo, iconInfo.getFileName()))); 131 } 132 } 133 peek()134 private QueryEntry peek() { 135 return mEntries.getFirst(); 136 } 137 pop()138 private QueryEntry pop() { 139 mEntries.removeFirst(); 140 return mEntries.isEmpty() ? null : mEntries.getFirst(); 141 } 142 isEmpty()143 private boolean isEmpty() { 144 return mEntries.isEmpty(); 145 } 146 getAllEntries()147 private List<QueryEntry> getAllEntries() { 148 return Collections.unmodifiableList(mEntries); 149 } 150 getBssid()151 private long getBssid() { 152 return mOsuInfo.getBSSID(); 153 } 154 getOsuInfo()155 private OSUInfo getOsuInfo() { 156 return mOsuInfo; 157 } 158 updateIcon(String fileName, HSIconFileElement iconFileElement)159 private IconKey updateIcon(String fileName, HSIconFileElement iconFileElement) { 160 IconKey key = null; 161 for (QueryEntry queryEntry : mEntries) { 162 if (queryEntry.getKey().getFileName().equals(fileName)) { 163 key = queryEntry.getKey(); 164 } 165 } 166 if (key == null) { 167 return null; 168 } 169 170 if (iconFileElement != null) { 171 mOsuInfo.setIconFileElement(iconFileElement, fileName); 172 } else { 173 mOsuInfo.setIconStatus(OSUInfo.IconStatus.NotAvailable); 174 } 175 return key; 176 } 177 updateIcon(IconKey key, HSIconFileElement iconFileElement)178 private boolean updateIcon(IconKey key, HSIconFileElement iconFileElement) { 179 boolean match = false; 180 for (QueryEntry queryEntry : mEntries) { 181 if (queryEntry.getKey().equals(key)) { 182 match = true; 183 break; 184 } 185 } 186 if (!match) { 187 return false; 188 } 189 190 if (iconFileElement != null) { 191 mOsuInfo.setIconFileElement(iconFileElement, key.getFileName()); 192 } else { 193 mOsuInfo.setIconStatus(OSUInfo.IconStatus.NotAvailable); 194 } 195 return true; 196 } 197 198 @Override toString()199 public String toString() { 200 return "OSU " + mOsuInfo + ": " + mEntries; 201 } 202 } 203 IconCache(OSUManager osuManager)204 public IconCache(OSUManager osuManager) { 205 mOSUManager = osuManager; 206 } 207 clear()208 public void clear() { 209 mBssQueues.clear(); 210 mCache.clear(); 211 } 212 enqueue(QuerySet querySet)213 private boolean enqueue(QuerySet querySet) { 214 boolean newEntry = false; 215 LinkedList<QuerySet> queries = mBssQueues.get(querySet.getBssid()); 216 if (queries == null) { 217 queries = new LinkedList<>(); 218 mBssQueues.put(querySet.getBssid(), queries); 219 newEntry = true; 220 } 221 queries.addLast(querySet); 222 return newEntry; 223 } 224 startIconQuery(OSUInfo osuInfo, List<IconInfo> icons)225 public void startIconQuery(OSUInfo osuInfo, List<IconInfo> icons) { 226 Log.d("ZXZ", String.format("Icon query on %012x for %s", osuInfo.getBSSID(), icons)); 227 if (icons == null || icons.isEmpty()) { 228 return; 229 } 230 231 QuerySet querySet = new QuerySet(osuInfo, icons); 232 for (QueryEntry entry : querySet.getAllEntries()) { 233 HSIconFileElement iconElement = mCache.get(entry.getKey()); 234 if (iconElement != null) { 235 osuInfo.setIconFileElement(iconElement, entry.getKey().getFileName()); 236 mOSUManager.iconResults(Arrays.asList(osuInfo)); 237 return; 238 } 239 } 240 if (enqueue(querySet)) { 241 initiateQuery(querySet.getBssid()); 242 } 243 } 244 initiateQuery(long bssid)245 private void initiateQuery(long bssid) { 246 LinkedList<QuerySet> queryEntries = mBssQueues.get(bssid); 247 if (queryEntries == null) { 248 return; 249 } else if (queryEntries.isEmpty()) { 250 mBssQueues.remove(bssid); 251 return; 252 } 253 254 QuerySet querySet = queryEntries.getFirst(); 255 QueryEntry queryEntry = querySet.peek(); 256 if (queryEntry.bumpRetry() >= RetryCount) { 257 QueryEntry newEntry = querySet.pop(); 258 if (newEntry == null) { 259 // No more entries in this QuerySet, advance to the next set. 260 querySet.getOsuInfo().setIconStatus(OSUInfo.IconStatus.NotAvailable); 261 queryEntries.removeFirst(); 262 if (queryEntries.isEmpty()) { 263 // No further QuerySet on this BSSID, drop the bucket and bail. 264 mBssQueues.remove(bssid); 265 return; 266 } else { 267 querySet = queryEntries.getFirst(); 268 queryEntry = querySet.peek(); 269 queryEntry.bumpRetry(); 270 } 271 } 272 } 273 mOSUManager.doIconQuery(bssid, queryEntry.getKey().getFileName()); 274 } 275 notifyIconReceived(long bssid, String fileName, byte[] iconData)276 public void notifyIconReceived(long bssid, String fileName, byte[] iconData) { 277 Log.d("ZXZ", String.format("Icon '%s':%d received from %012x", 278 fileName, iconData != null ? iconData.length : -1, bssid)); 279 IconKey key; 280 HSIconFileElement iconFileElement = null; 281 List<OSUInfo> updates = new ArrayList<>(); 282 283 LinkedList<QuerySet> querySets = mBssQueues.get(bssid); 284 if (querySets == null || querySets.isEmpty()) { 285 Log.d(OSUManager.TAG, 286 String.format("Spurious icon response from %012x for '%s' (%d) bytes", 287 bssid, fileName, iconData != null ? iconData.length : -1)); 288 Log.d("ZXZ", "query set: " + querySets 289 + ", BSS queues: " + Utils.bssidsToString(mBssQueues.keySet())); 290 return; 291 } else { 292 QuerySet querySet = querySets.removeFirst(); 293 if (iconData != null) { 294 try { 295 iconFileElement = new HSIconFileElement(HSIconFile, 296 ByteBuffer.wrap(iconData).order(ByteOrder.LITTLE_ENDIAN)); 297 } catch (ProtocolException | BufferUnderflowException e) { 298 Log.e(OSUManager.TAG, "Failed to parse ANQP icon file: " + e); 299 } 300 } 301 key = querySet.updateIcon(fileName, iconFileElement); 302 if (key == null) { 303 Log.d(OSUManager.TAG, 304 String.format("Spurious icon response from %012x for '%s' (%d) bytes", 305 bssid, fileName, iconData != null ? iconData.length : -1)); 306 Log.d("ZXZ", "query set: " + querySets + ", BSS queues: " 307 + Utils.bssidsToString(mBssQueues.keySet())); 308 querySets.addFirst(querySet); 309 return; 310 } 311 312 if (iconFileElement != null) { 313 mCache.put(key, iconFileElement); 314 } 315 316 if (querySet.isEmpty()) { 317 mBssQueues.remove(bssid); 318 } 319 updates.add(querySet.getOsuInfo()); 320 } 321 322 // Update any other pending entries that matches the ESS of the currently resolved icon 323 Iterator<Map.Entry<Long, LinkedList<QuerySet>>> bssIterator = 324 mBssQueues.entrySet().iterator(); 325 while (bssIterator.hasNext()) { 326 Map.Entry<Long, LinkedList<QuerySet>> bssEntries = bssIterator.next(); 327 Iterator<QuerySet> querySetIterator = bssEntries.getValue().iterator(); 328 while (querySetIterator.hasNext()) { 329 QuerySet querySet = querySetIterator.next(); 330 if (querySet.updateIcon(key, iconFileElement)) { 331 querySetIterator.remove(); 332 updates.add(querySet.getOsuInfo()); 333 } 334 } 335 if (bssEntries.getValue().isEmpty()) { 336 bssIterator.remove(); 337 } 338 } 339 340 initiateQuery(bssid); 341 342 mOSUManager.iconResults(updates); 343 } 344 345 private static final long RequeryTimeLow = 6000L; 346 private static final long RequeryTimeHigh = 15000L; 347 tickle(boolean wifiOff)348 public void tickle(boolean wifiOff) { 349 synchronized (mCache) { 350 if (wifiOff) { 351 mBssQueues.clear(); 352 } else { 353 long now = System.currentTimeMillis(); 354 355 Iterator<Map.Entry<Long, LinkedList<QuerySet>>> bssIterator = 356 mBssQueues.entrySet().iterator(); 357 while (bssIterator.hasNext()) { 358 // Get the list of entries for this BSSID 359 Map.Entry<Long, LinkedList<QuerySet>> bssEntries = bssIterator.next(); 360 Iterator<QuerySet> querySetIterator = bssEntries.getValue().iterator(); 361 while (querySetIterator.hasNext()) { 362 QuerySet querySet = querySetIterator.next(); 363 QueryEntry queryEntry = querySet.peek(); 364 long age = queryEntry.age(now); 365 if (age > RequeryTimeHigh) { 366 // Timed out entry, move on to the next. 367 queryEntry = querySet.pop(); 368 if (queryEntry == null) { 369 // Empty query set, update status and remove it. 370 querySet.getOsuInfo() 371 .setIconStatus(OSUInfo.IconStatus.NotAvailable); 372 querySetIterator.remove(); 373 } else { 374 // Start a query on the next entry and bail out of the set iteration 375 initiateQuery(querySet.getBssid()); 376 break; 377 } 378 } else if (age > RequeryTimeLow) { 379 // Re-issue queries for qualified entries and bail out of set iteration 380 initiateQuery(querySet.getBssid()); 381 break; 382 } 383 } 384 if (bssEntries.getValue().isEmpty()) { 385 // Kill the whole bucket if the set list is empty 386 bssIterator.remove(); 387 } 388 } 389 } 390 } 391 } 392 } 393