Home | History | Annotate | Download | only in osu
      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 
     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 
     55         public String getFileName() {
     56             return mFileName;
     57         }
     58 
     59         @Override
     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
     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
     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 
     98         private QueryEntry(IconKey key) {
     99             mKey = key;
    100             mLastSent = System.currentTimeMillis();
    101         }
    102 
    103         private IconKey getKey() {
    104             return mKey;
    105         }
    106 
    107         private int bumpRetry() {
    108             mLastSent = System.currentTimeMillis();
    109             return mRetry++;
    110         }
    111 
    112         private long age(long now) {
    113             return now - mLastSent;
    114         }
    115 
    116         @Override
    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 
    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 
    134         private QueryEntry peek() {
    135             return mEntries.getFirst();
    136         }
    137 
    138         private QueryEntry pop() {
    139             mEntries.removeFirst();
    140             return mEntries.isEmpty() ? null : mEntries.getFirst();
    141         }
    142 
    143         private boolean isEmpty() {
    144             return mEntries.isEmpty();
    145         }
    146 
    147         private List<QueryEntry> getAllEntries() {
    148             return Collections.unmodifiableList(mEntries);
    149         }
    150 
    151         private long getBssid() {
    152             return mOsuInfo.getBSSID();
    153         }
    154 
    155         private OSUInfo getOsuInfo() {
    156             return mOsuInfo;
    157         }
    158 
    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 
    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
    199         public String toString() {
    200             return "OSU " + mOsuInfo + ": " + mEntries;
    201         }
    202     }
    203 
    204     public IconCache(OSUManager osuManager) {
    205         mOSUManager = osuManager;
    206     }
    207 
    208     public void clear() {
    209         mBssQueues.clear();
    210         mCache.clear();
    211     }
    212 
    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 
    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 
    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 
    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 
    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