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 java.net.ProtocolException; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import static com.android.anqp.Constants.ANQPElementType.HSIconFile; public class IconCache extends Thread { private static final int CacheSize = 64; private static final int RetryCount = 3; private final OSUManager mOSUManager; private final Map<Long, LinkedList<QuerySet>> mBssQueues = new HashMap<>(); private final Map<IconKey, HSIconFileElement> mCache = new LinkedHashMap<IconKey, HSIconFileElement>() { @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > CacheSize; } }; private static class IconKey { private final long mBSSID; private final long mHESSID; private final String mSSID; private final int mAnqpDomID; private final String mFileName; private IconKey(OSUInfo osuInfo, String fileName) { mBSSID = osuInfo.getBSSID(); mHESSID = osuInfo.getHESSID(); mSSID = osuInfo.getAdvertisingSSID(); mAnqpDomID = osuInfo.getAnqpDomID(); mFileName = fileName; } public String getFileName() { return mFileName; } @Override public boolean equals(Object thatObject) { if (this == thatObject) { return true; } if (thatObject == null || getClass() != thatObject.getClass()) { return false; } IconKey that = (IconKey) thatObject; return mFileName.equals(that.mFileName) && ((mBSSID == that.mBSSID) || ((mAnqpDomID == that.mAnqpDomID) && (mAnqpDomID != 0) && (mHESSID == that.mHESSID) && ((mHESSID != 0) || mSSID.equals(that.mSSID)))); } @Override public int hashCode() { int result = (int) (mBSSID ^ (mBSSID >>> 32)); result = 31 * result + (int) (mHESSID ^ (mHESSID >>> 32)); result = 31 * result + mSSID.hashCode(); result = 31 * result + mAnqpDomID; result = 31 * result + mFileName.hashCode(); return result; } @Override public String toString() { return String.format("%012x:%012x '%s' [%d] + '%s'", mBSSID, mHESSID, mSSID, mAnqpDomID, mFileName); } } private static class QueryEntry { private final IconKey mKey; private int mRetry; private long mLastSent; private QueryEntry(IconKey key) { mKey = key; mLastSent = System.currentTimeMillis(); } private IconKey getKey() { return mKey; } private int bumpRetry() { mLastSent = System.currentTimeMillis(); return mRetry++; } private long age(long now) { return now - mLastSent; } @Override public String toString() { return String.format("Entry %s, retry %d", mKey, mRetry); } } private static class QuerySet { private final OSUInfo mOsuInfo; private final LinkedList<QueryEntry> mEntries; private QuerySet(OSUInfo osuInfo, List<IconInfo> icons) { mOsuInfo = osuInfo; mEntries = new LinkedList<>(); for (IconInfo iconInfo : icons) { mEntries.addLast(new QueryEntry(new IconKey(osuInfo, iconInfo.getFileName()))); } } private QueryEntry peek() { return mEntries.getFirst(); } private QueryEntry pop() { mEntries.removeFirst(); return mEntries.isEmpty() ? null : mEntries.getFirst(); } private boolean isEmpty() { return mEntries.isEmpty(); } private List<QueryEntry> getAllEntries() { return Collections.unmodifiableList(mEntries); } private long getBssid() { return mOsuInfo.getBSSID(); } private OSUInfo getOsuInfo() { return mOsuInfo; } private IconKey updateIcon(String fileName, HSIconFileElement iconFileElement) { IconKey key = null; for (QueryEntry queryEntry : mEntries) { if (queryEntry.getKey().getFileName().equals(fileName)) { key = queryEntry.getKey(); } } if (key == null) { return null; } if (iconFileElement != null) { mOsuInfo.setIconFileElement(iconFileElement, fileName); } else { mOsuInfo.setIconStatus(OSUInfo.IconStatus.NotAvailable); } return key; } private boolean updateIcon(IconKey key, HSIconFileElement iconFileElement) { boolean match = false; for (QueryEntry queryEntry : mEntries) { if (queryEntry.getKey().equals(key)) { match = true; break; } } if (!match) { return false; } if (iconFileElement != null) { mOsuInfo.setIconFileElement(iconFileElement, key.getFileName()); } else { mOsuInfo.setIconStatus(OSUInfo.IconStatus.NotAvailable); } return true; } @Override public String toString() { return "OSU " + mOsuInfo + ": " + mEntries; } } public IconCache(OSUManager osuManager) { mOSUManager = osuManager; } public void clear() { mBssQueues.clear(); mCache.clear(); } private boolean enqueue(QuerySet querySet) { boolean newEntry = false; LinkedList<QuerySet> queries = mBssQueues.get(querySet.getBssid()); if (queries == null) { queries = new LinkedList<>(); mBssQueues.put(querySet.getBssid(), queries); newEntry = true; } queries.addLast(querySet); return newEntry; } public void startIconQuery(OSUInfo osuInfo, List<IconInfo> icons) { Log.d("ZXZ", String.format("Icon query on %012x for %s", osuInfo.getBSSID(), icons)); if (icons == null || icons.isEmpty()) { return; } QuerySet querySet = new QuerySet(osuInfo, icons); for (QueryEntry entry : querySet.getAllEntries()) { HSIconFileElement iconElement = mCache.get(entry.getKey()); if (iconElement != null) { osuInfo.setIconFileElement(iconElement, entry.getKey().getFileName()); mOSUManager.iconResults(Arrays.asList(osuInfo)); return; } } if (enqueue(querySet)) { initiateQuery(querySet.getBssid()); } } private void initiateQuery(long bssid) { LinkedList<QuerySet> queryEntries = mBssQueues.get(bssid); if (queryEntries == null) { return; } else if (queryEntries.isEmpty()) { mBssQueues.remove(bssid); return; } QuerySet querySet = queryEntries.getFirst(); QueryEntry queryEntry = querySet.peek(); if (queryEntry.bumpRetry() >= RetryCount) { QueryEntry newEntry = querySet.pop(); if (newEntry == null) { // No more entries in this QuerySet, advance to the next set. querySet.getOsuInfo().setIconStatus(OSUInfo.IconStatus.NotAvailable); queryEntries.removeFirst(); if (queryEntries.isEmpty()) { // No further QuerySet on this BSSID, drop the bucket and bail. mBssQueues.remove(bssid); return; } else { querySet = queryEntries.getFirst(); queryEntry = querySet.peek(); queryEntry.bumpRetry(); } } } mOSUManager.doIconQuery(bssid, queryEntry.getKey().getFileName()); } public void notifyIconReceived(long bssid, String fileName, byte[] iconData) { Log.d("ZXZ", String.format("Icon '%s':%d received from %012x", fileName, iconData != null ? iconData.length : -1, bssid)); IconKey key; HSIconFileElement iconFileElement = null; List<OSUInfo> updates = new ArrayList<>(); LinkedList<QuerySet> querySets = mBssQueues.get(bssid); if (querySets == null || querySets.isEmpty()) { Log.d(OSUManager.TAG, String.format("Spurious icon response from %012x for '%s' (%d) bytes", bssid, fileName, iconData != null ? iconData.length : -1)); Log.d("ZXZ", "query set: " + querySets + ", BSS queues: " + Utils.bssidsToString(mBssQueues.keySet())); return; } else { QuerySet querySet = querySets.removeFirst(); if (iconData != null) { 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); } } key = querySet.updateIcon(fileName, iconFileElement); if (key == null) { Log.d(OSUManager.TAG, String.format("Spurious icon response from %012x for '%s' (%d) bytes", bssid, fileName, iconData != null ? iconData.length : -1)); Log.d("ZXZ", "query set: " + querySets + ", BSS queues: " + Utils.bssidsToString(mBssQueues.keySet())); querySets.addFirst(querySet); return; } if (iconFileElement != null) { mCache.put(key, iconFileElement); } if (querySet.isEmpty()) { mBssQueues.remove(bssid); } updates.add(querySet.getOsuInfo()); } // Update any other pending entries that matches the ESS of the currently resolved icon Iterator<Map.Entry<Long, LinkedList<QuerySet>>> bssIterator = mBssQueues.entrySet().iterator(); while (bssIterator.hasNext()) { Map.Entry<Long, LinkedList<QuerySet>> bssEntries = bssIterator.next(); Iterator<QuerySet> querySetIterator = bssEntries.getValue().iterator(); while (querySetIterator.hasNext()) { QuerySet querySet = querySetIterator.next(); if (querySet.updateIcon(key, iconFileElement)) { querySetIterator.remove(); updates.add(querySet.getOsuInfo()); } } if (bssEntries.getValue().isEmpty()) { bssIterator.remove(); } } initiateQuery(bssid); mOSUManager.iconResults(updates); } private static final long RequeryTimeLow = 6000L; private static final long RequeryTimeHigh = 15000L; public void tickle(boolean wifiOff) { synchronized (mCache) { if (wifiOff) { mBssQueues.clear(); } else { long now = System.currentTimeMillis(); Iterator<Map.Entry<Long, LinkedList<QuerySet>>> bssIterator = mBssQueues.entrySet().iterator(); while (bssIterator.hasNext()) { // Get the list of entries for this BSSID Map.Entry<Long, LinkedList<QuerySet>> bssEntries = bssIterator.next(); Iterator<QuerySet> querySetIterator = bssEntries.getValue().iterator(); while (querySetIterator.hasNext()) { QuerySet querySet = querySetIterator.next(); QueryEntry queryEntry = querySet.peek(); long age = queryEntry.age(now); if (age > RequeryTimeHigh) { // Timed out entry, move on to the next. queryEntry = querySet.pop(); if (queryEntry == null) { // Empty query set, update status and remove it. querySet.getOsuInfo() .setIconStatus(OSUInfo.IconStatus.NotAvailable); querySetIterator.remove(); } else { // Start a query on the next entry and bail out of the set iteration initiateQuery(querySet.getBssid()); break; } } else if (age > RequeryTimeLow) { // Re-issue queries for qualified entries and bail out of set iteration initiateQuery(querySet.getBssid()); break; } } if (bssEntries.getValue().isEmpty()) { // Kill the whole bucket if the set list is empty bssIterator.remove(); } } } } } }