package com.android.hotspot2.osu; import android.util.Log; import com.android.anqp.HSIconFileElement; import com.android.anqp.IconInfo; import com.android.hotspot2.Utils; import com.android.hotspot2.flow.OSUInfo; import java.net.ProtocolException; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import static com.android.anqp.Constants.ANQPElementType.HSIconFile; public class IconCache extends Thread { // Preferred icon parameters private static final Set ICON_TYPES = new HashSet<>(Arrays.asList("image/png", "image/jpeg")); private static final int ICON_WIDTH = 64; private static final int ICON_HEIGHT = 64; public static final Locale LOCALE = java.util.Locale.getDefault(); private static final int MAX_RETRY = 3; private static final long REQUERY_TIME = 5000L; private static final long REQUERY_TIMEOUT = 120000L; private final OSUManager mOsuManager; private final Map> mPending; private final Map> mCache; private static class EssKey { private final int mAnqpDomainId; private final long mBssid; private final long mHessid; private final String mSsid; private EssKey(OSUInfo osuInfo) { mAnqpDomainId = osuInfo.getAnqpDomID(); mBssid = osuInfo.getBSSID(); mHessid = osuInfo.getHESSID(); mSsid = osuInfo.getAdvertisingSsid(); } /* * ANQP ID 1 ANQP ID 2 * 0 0 BSSID equality * 0 X BSSID equality * Y X BSSID equality * X X Then: * * HESSID1 HESSID2 * 0 0 compare SSIDs * 0 X not equal * Y X not equal * X X equal */ @Override public boolean equals(Object thatObject) { if (this == thatObject) { return true; } if (thatObject == null || getClass() != thatObject.getClass()) { return false; } EssKey that = (EssKey) thatObject; if (mAnqpDomainId != 0 && mAnqpDomainId == that.mAnqpDomainId) { return mHessid == that.mHessid && (mHessid != 0 || mSsid.equals(that.mSsid)); } else { return mBssid == that.mBssid; } } @Override public int hashCode() { if (mAnqpDomainId == 0) { return (int) (mBssid ^ (mBssid >>> 32)); } else if (mHessid != 0) { return mAnqpDomainId * 31 + (int) (mHessid ^ (mHessid >>> 32)); } else { return mAnqpDomainId * 31 + mSsid.hashCode(); } } @Override public String toString() { if (mAnqpDomainId == 0) { return String.format("BSS %012x", mBssid); } else if (mHessid != 0) { return String.format("ESS %012x [%d]", mBssid, mAnqpDomainId); } else { return String.format("ESS '%s' [%d]", mSsid, mAnqpDomainId); } } } private static class FileEntry { private final String mFileName; private int mRetry = 0; private final long mTimestamp; private final LinkedList mQueued; private final Set mBssids; private FileEntry(OSUInfo osuInfo, String fileName) { mFileName = fileName; mQueued = new LinkedList<>(); mBssids = new HashSet<>(); mQueued.addLast(osuInfo); mBssids.add(osuInfo.getBSSID()); mTimestamp = System.currentTimeMillis(); } private void enqueu(OSUInfo osuInfo) { mQueued.addLast(osuInfo); mBssids.add(osuInfo.getBSSID()); } private int update(long bssid, HSIconFileElement iconFileElement) { if (!mBssids.contains(bssid)) { return 0; } Log.d(OSUManager.TAG, "Updating icon on " + mQueued.size() + " osus"); for (OSUInfo osuInfo : mQueued) { osuInfo.setIconFileElement(iconFileElement, mFileName); } return mQueued.size(); } private int getAndIncrementRetry() { return mRetry++; } private long getTimestamp() { return mTimestamp; } public String getFileName() { return mFileName; } private long getLastBssid() { return mQueued.getLast().getBSSID(); } @Override public String toString() { return String.format("'%s', retry %d, age %d, BSSIDs: %s", mFileName, mRetry, System.currentTimeMillis() - mTimestamp, Utils.bssidsToString(mBssids)); } } public IconCache(OSUManager osuManager) { mOsuManager = osuManager; mPending = new HashMap<>(); mCache = new HashMap<>(); } public int resolveIcons(Collection osuInfos) { Set current = new HashSet<>(); int modCount = 0; for (OSUInfo osuInfo : osuInfos) { EssKey key = new EssKey(osuInfo); current.add(key); if (osuInfo.getIconStatus() == OSUInfo.IconStatus.NotQueried) { List iconInfo = osuInfo.getIconInfo(LOCALE, ICON_TYPES, ICON_WIDTH, ICON_HEIGHT); if (iconInfo.isEmpty()) { osuInfo.setIconStatus(OSUInfo.IconStatus.NotAvailable); continue; } String fileName = iconInfo.get(0).getFileName(); HSIconFileElement iconFileElement = get(key, fileName); if (iconFileElement != null) { osuInfo.setIconFileElement(iconFileElement, fileName); Log.d(OSUManager.TAG, "Icon cache hit for " + osuInfo + "/" + fileName); modCount++; } else { FileEntry fileEntry = enqueue(key, fileName, osuInfo); if (fileEntry != null) { Log.d(OSUManager.TAG, "Initiating icon query for " + osuInfo + "/" + fileName); mOsuManager.doIconQuery(osuInfo.getBSSID(), fileName); } else { Log.d(OSUManager.TAG, "Piggybacking icon query for " + osuInfo + "/" + fileName); } } } } // Drop all non-current ESS's Iterator pendingKeys = mPending.keySet().iterator(); while (pendingKeys.hasNext()) { EssKey key = pendingKeys.next(); if (!current.contains(key)) { pendingKeys.remove(); } } Iterator cacheKeys = mCache.keySet().iterator(); while (cacheKeys.hasNext()) { EssKey key = cacheKeys.next(); if (!current.contains(key)) { cacheKeys.remove(); } } return modCount; } public HSIconFileElement getIcon(OSUInfo osuInfo) { List iconInfos = osuInfo.getIconInfo(LOCALE, ICON_TYPES, ICON_WIDTH, ICON_HEIGHT); if (iconInfos == null || iconInfos.isEmpty()) { return null; } EssKey key = new EssKey(osuInfo); Map fileMap = mCache.get(key); return fileMap != null ? fileMap.get(iconInfos.get(0).getFileName()) : null; } public int notifyIconReceived(long bssid, String fileName, byte[] iconData) { Log.d(OSUManager.TAG, String.format("Icon '%s':%d received from %012x", fileName, iconData != null ? iconData.length : -1, bssid)); if (fileName == null || iconData == null) { return 0; } HSIconFileElement iconFileElement; try { iconFileElement = new HSIconFileElement(HSIconFile, ByteBuffer.wrap(iconData).order(ByteOrder.LITTLE_ENDIAN)); } catch (ProtocolException | BufferUnderflowException e) { Log.e(OSUManager.TAG, "Failed to parse ANQP icon file: " + e); return 0; } int updates = 0; Iterator>> entries = mPending.entrySet().iterator(); while (entries.hasNext()) { Map.Entry> entry = entries.next(); Map fileMap = entry.getValue(); FileEntry fileEntry = fileMap.get(fileName); updates = fileEntry.update(bssid, iconFileElement); if (updates > 0) { put(entry.getKey(), fileName, iconFileElement); fileMap.remove(fileName); if (fileMap.isEmpty()) { entries.remove(); } break; } } return updates; } public void tick(boolean wifiOff) { if (wifiOff) { mPending.clear(); mCache.clear(); return; } Iterator>> entries = mPending.entrySet().iterator(); long now = System.currentTimeMillis(); while (entries.hasNext()) { Map fileMap = entries.next().getValue(); Iterator> fileEntries = fileMap.entrySet().iterator(); while (fileEntries.hasNext()) { FileEntry fileEntry = fileEntries.next().getValue(); long age = now - fileEntry.getTimestamp(); if (age > REQUERY_TIMEOUT || fileEntry.getAndIncrementRetry() > MAX_RETRY) { fileEntries.remove(); } else if (age > REQUERY_TIME) { mOsuManager.doIconQuery(fileEntry.getLastBssid(), fileEntry.getFileName()); } } if (fileMap.isEmpty()) { entries.remove(); } } } private HSIconFileElement get(EssKey key, String fileName) { Map fileMap = mCache.get(key); if (fileMap == null) { return null; } return fileMap.get(fileName); } private void put(EssKey key, String fileName, HSIconFileElement icon) { Map fileMap = mCache.get(key); if (fileMap == null) { fileMap = new HashMap<>(); mCache.put(key, fileMap); } fileMap.put(fileName, icon); } private FileEntry enqueue(EssKey key, String fileName, OSUInfo osuInfo) { Map entryMap = mPending.get(key); if (entryMap == null) { entryMap = new HashMap<>(); mPending.put(key, entryMap); } FileEntry fileEntry = entryMap.get(fileName); osuInfo.setIconStatus(OSUInfo.IconStatus.InProgress); if (fileEntry == null) { fileEntry = new FileEntry(osuInfo, fileName); entryMap.put(fileName, fileEntry); return fileEntry; } fileEntry.enqueu(osuInfo); return null; } }