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();
}
}
}
}
}
}