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 import com.android.hotspot2.flow.OSUInfo; 9 10 import java.net.ProtocolException; 11 import java.nio.BufferUnderflowException; 12 import java.nio.ByteBuffer; 13 import java.nio.ByteOrder; 14 import java.util.Arrays; 15 import java.util.Collection; 16 import java.util.HashMap; 17 import java.util.HashSet; 18 import java.util.Iterator; 19 import java.util.LinkedList; 20 import java.util.List; 21 import java.util.Locale; 22 import java.util.Map; 23 import java.util.Set; 24 25 import static com.android.anqp.Constants.ANQPElementType.HSIconFile; 26 27 public class IconCache extends Thread { 28 // Preferred icon parameters 29 private static final Set<String> ICON_TYPES = 30 new HashSet<>(Arrays.asList("image/png", "image/jpeg")); 31 private static final int ICON_WIDTH = 64; 32 private static final int ICON_HEIGHT = 64; 33 public static final Locale LOCALE = java.util.Locale.getDefault(); 34 35 private static final int MAX_RETRY = 3; 36 private static final long REQUERY_TIME = 5000L; 37 private static final long REQUERY_TIMEOUT = 120000L; 38 39 private final OSUManager mOsuManager; 40 private final Map<EssKey, Map<String, FileEntry>> mPending; 41 private final Map<EssKey, Map<String, HSIconFileElement>> mCache; 42 43 private static class EssKey { 44 private final int mAnqpDomainId; 45 private final long mBssid; 46 private final long mHessid; 47 private final String mSsid; 48 EssKey(OSUInfo osuInfo)49 private EssKey(OSUInfo osuInfo) { 50 mAnqpDomainId = osuInfo.getAnqpDomID(); 51 mBssid = osuInfo.getBSSID(); 52 mHessid = osuInfo.getHESSID(); 53 mSsid = osuInfo.getAdvertisingSsid(); 54 } 55 56 /* 57 * ANQP ID 1 ANQP ID 2 58 * 0 0 BSSID equality 59 * 0 X BSSID equality 60 * Y X BSSID equality 61 * X X Then: 62 * 63 * HESSID1 HESSID2 64 * 0 0 compare SSIDs 65 * 0 X not equal 66 * Y X not equal 67 * X X equal 68 */ 69 70 @Override equals(Object thatObject)71 public boolean equals(Object thatObject) { 72 if (this == thatObject) { 73 return true; 74 } 75 if (thatObject == null || getClass() != thatObject.getClass()) { 76 return false; 77 } 78 79 EssKey that = (EssKey) thatObject; 80 if (mAnqpDomainId != 0 && mAnqpDomainId == that.mAnqpDomainId) { 81 return mHessid == that.mHessid 82 && (mHessid != 0 || mSsid.equals(that.mSsid)); 83 } else { 84 return mBssid == that.mBssid; 85 } 86 } 87 88 @Override hashCode()89 public int hashCode() { 90 if (mAnqpDomainId == 0) { 91 return (int) (mBssid ^ (mBssid >>> 32)); 92 } else if (mHessid != 0) { 93 return mAnqpDomainId * 31 + (int) (mHessid ^ (mHessid >>> 32)); 94 } else { 95 return mAnqpDomainId * 31 + mSsid.hashCode(); 96 } 97 } 98 99 @Override toString()100 public String toString() { 101 if (mAnqpDomainId == 0) { 102 return String.format("BSS %012x", mBssid); 103 } else if (mHessid != 0) { 104 return String.format("ESS %012x [%d]", mBssid, mAnqpDomainId); 105 } else { 106 return String.format("ESS '%s' [%d]", mSsid, mAnqpDomainId); 107 } 108 } 109 } 110 111 private static class FileEntry { 112 private final String mFileName; 113 private int mRetry = 0; 114 private final long mTimestamp; 115 private final LinkedList<OSUInfo> mQueued; 116 private final Set<Long> mBssids; 117 FileEntry(OSUInfo osuInfo, String fileName)118 private FileEntry(OSUInfo osuInfo, String fileName) { 119 mFileName = fileName; 120 mQueued = new LinkedList<>(); 121 mBssids = new HashSet<>(); 122 mQueued.addLast(osuInfo); 123 mBssids.add(osuInfo.getBSSID()); 124 mTimestamp = System.currentTimeMillis(); 125 } 126 enqueu(OSUInfo osuInfo)127 private void enqueu(OSUInfo osuInfo) { 128 mQueued.addLast(osuInfo); 129 mBssids.add(osuInfo.getBSSID()); 130 } 131 update(long bssid, HSIconFileElement iconFileElement)132 private int update(long bssid, HSIconFileElement iconFileElement) { 133 if (!mBssids.contains(bssid)) { 134 return 0; 135 } 136 Log.d(OSUManager.TAG, "Updating icon on " + mQueued.size() + " osus"); 137 for (OSUInfo osuInfo : mQueued) { 138 osuInfo.setIconFileElement(iconFileElement, mFileName); 139 } 140 return mQueued.size(); 141 } 142 getAndIncrementRetry()143 private int getAndIncrementRetry() { 144 return mRetry++; 145 } 146 getTimestamp()147 private long getTimestamp() { 148 return mTimestamp; 149 } 150 getFileName()151 public String getFileName() { 152 return mFileName; 153 } 154 getLastBssid()155 private long getLastBssid() { 156 return mQueued.getLast().getBSSID(); 157 } 158 159 @Override toString()160 public String toString() { 161 return String.format("'%s', retry %d, age %d, BSSIDs: %s", 162 mFileName, mRetry, 163 System.currentTimeMillis() - mTimestamp, Utils.bssidsToString(mBssids)); 164 } 165 } 166 IconCache(OSUManager osuManager)167 public IconCache(OSUManager osuManager) { 168 mOsuManager = osuManager; 169 mPending = new HashMap<>(); 170 mCache = new HashMap<>(); 171 } 172 resolveIcons(Collection<OSUInfo> osuInfos)173 public int resolveIcons(Collection<OSUInfo> osuInfos) { 174 Set<EssKey> current = new HashSet<>(); 175 int modCount = 0; 176 for (OSUInfo osuInfo : osuInfos) { 177 EssKey key = new EssKey(osuInfo); 178 current.add(key); 179 180 if (osuInfo.getIconStatus() == OSUInfo.IconStatus.NotQueried) { 181 List<IconInfo> iconInfo = 182 osuInfo.getIconInfo(LOCALE, ICON_TYPES, ICON_WIDTH, ICON_HEIGHT); 183 if (iconInfo.isEmpty()) { 184 osuInfo.setIconStatus(OSUInfo.IconStatus.NotAvailable); 185 continue; 186 } 187 188 String fileName = iconInfo.get(0).getFileName(); 189 HSIconFileElement iconFileElement = get(key, fileName); 190 if (iconFileElement != null) { 191 osuInfo.setIconFileElement(iconFileElement, fileName); 192 Log.d(OSUManager.TAG, "Icon cache hit for " + osuInfo + "/" + fileName); 193 modCount++; 194 } else { 195 FileEntry fileEntry = enqueue(key, fileName, osuInfo); 196 if (fileEntry != null) { 197 Log.d(OSUManager.TAG, "Initiating icon query for " 198 + osuInfo + "/" + fileName); 199 mOsuManager.doIconQuery(osuInfo.getBSSID(), fileName); 200 } else { 201 Log.d(OSUManager.TAG, "Piggybacking icon query for " 202 + osuInfo + "/" + fileName); 203 } 204 } 205 } 206 } 207 208 // Drop all non-current ESS's 209 Iterator<EssKey> pendingKeys = mPending.keySet().iterator(); 210 while (pendingKeys.hasNext()) { 211 EssKey key = pendingKeys.next(); 212 if (!current.contains(key)) { 213 pendingKeys.remove(); 214 } 215 } 216 Iterator<EssKey> cacheKeys = mCache.keySet().iterator(); 217 while (cacheKeys.hasNext()) { 218 EssKey key = cacheKeys.next(); 219 if (!current.contains(key)) { 220 cacheKeys.remove(); 221 } 222 } 223 return modCount; 224 } 225 getIcon(OSUInfo osuInfo)226 public HSIconFileElement getIcon(OSUInfo osuInfo) { 227 List<IconInfo> iconInfos = osuInfo.getIconInfo(LOCALE, ICON_TYPES, ICON_WIDTH, ICON_HEIGHT); 228 if (iconInfos == null || iconInfos.isEmpty()) { 229 return null; 230 } 231 EssKey key = new EssKey(osuInfo); 232 Map<String, HSIconFileElement> fileMap = mCache.get(key); 233 return fileMap != null ? fileMap.get(iconInfos.get(0).getFileName()) : null; 234 } 235 notifyIconReceived(long bssid, String fileName, byte[] iconData)236 public int notifyIconReceived(long bssid, String fileName, byte[] iconData) { 237 Log.d(OSUManager.TAG, String.format("Icon '%s':%d received from %012x", 238 fileName, iconData != null ? iconData.length : -1, bssid)); 239 if (fileName == null || iconData == null) { 240 return 0; 241 } 242 243 HSIconFileElement iconFileElement; 244 try { 245 iconFileElement = new HSIconFileElement(HSIconFile, 246 ByteBuffer.wrap(iconData).order(ByteOrder.LITTLE_ENDIAN)); 247 } catch (ProtocolException | BufferUnderflowException e) { 248 Log.e(OSUManager.TAG, "Failed to parse ANQP icon file: " + e); 249 return 0; 250 } 251 252 int updates = 0; 253 Iterator<Map.Entry<EssKey, Map<String, FileEntry>>> entries = 254 mPending.entrySet().iterator(); 255 256 while (entries.hasNext()) { 257 Map.Entry<EssKey, Map<String, FileEntry>> entry = entries.next(); 258 259 Map<String, FileEntry> fileMap = entry.getValue(); 260 FileEntry fileEntry = fileMap.get(fileName); 261 updates = fileEntry.update(bssid, iconFileElement); 262 if (updates > 0) { 263 put(entry.getKey(), fileName, iconFileElement); 264 fileMap.remove(fileName); 265 if (fileMap.isEmpty()) { 266 entries.remove(); 267 } 268 break; 269 } 270 } 271 return updates; 272 } 273 tick(boolean wifiOff)274 public void tick(boolean wifiOff) { 275 if (wifiOff) { 276 mPending.clear(); 277 mCache.clear(); 278 return; 279 } 280 281 Iterator<Map.Entry<EssKey, Map<String, FileEntry>>> entries = 282 mPending.entrySet().iterator(); 283 284 long now = System.currentTimeMillis(); 285 while (entries.hasNext()) { 286 Map<String, FileEntry> fileMap = entries.next().getValue(); 287 Iterator<Map.Entry<String, FileEntry>> fileEntries = fileMap.entrySet().iterator(); 288 while (fileEntries.hasNext()) { 289 FileEntry fileEntry = fileEntries.next().getValue(); 290 long age = now - fileEntry.getTimestamp(); 291 if (age > REQUERY_TIMEOUT || fileEntry.getAndIncrementRetry() > MAX_RETRY) { 292 fileEntries.remove(); 293 } else if (age > REQUERY_TIME) { 294 mOsuManager.doIconQuery(fileEntry.getLastBssid(), fileEntry.getFileName()); 295 } 296 } 297 if (fileMap.isEmpty()) { 298 entries.remove(); 299 } 300 } 301 } 302 get(EssKey key, String fileName)303 private HSIconFileElement get(EssKey key, String fileName) { 304 Map<String, HSIconFileElement> fileMap = mCache.get(key); 305 if (fileMap == null) { 306 return null; 307 } 308 return fileMap.get(fileName); 309 } 310 put(EssKey key, String fileName, HSIconFileElement icon)311 private void put(EssKey key, String fileName, HSIconFileElement icon) { 312 Map<String, HSIconFileElement> fileMap = mCache.get(key); 313 if (fileMap == null) { 314 fileMap = new HashMap<>(); 315 mCache.put(key, fileMap); 316 } 317 fileMap.put(fileName, icon); 318 } 319 enqueue(EssKey key, String fileName, OSUInfo osuInfo)320 private FileEntry enqueue(EssKey key, String fileName, OSUInfo osuInfo) { 321 Map<String, FileEntry> entryMap = mPending.get(key); 322 if (entryMap == null) { 323 entryMap = new HashMap<>(); 324 mPending.put(key, entryMap); 325 } 326 327 FileEntry fileEntry = entryMap.get(fileName); 328 osuInfo.setIconStatus(OSUInfo.IconStatus.InProgress); 329 if (fileEntry == null) { 330 fileEntry = new FileEntry(osuInfo, fileName); 331 entryMap.put(fileName, fileEntry); 332 return fileEntry; 333 } 334 fileEntry.enqueu(osuInfo); 335 return null; 336 } 337 } 338