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 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 
     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
     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
     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
    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 
    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 
    127         private void enqueu(OSUInfo osuInfo) {
    128             mQueued.addLast(osuInfo);
    129             mBssids.add(osuInfo.getBSSID());
    130         }
    131 
    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 
    143         private int getAndIncrementRetry() {
    144             return mRetry++;
    145         }
    146 
    147         private long getTimestamp() {
    148             return mTimestamp;
    149         }
    150 
    151         public String getFileName() {
    152             return mFileName;
    153         }
    154 
    155         private long getLastBssid() {
    156             return mQueued.getLast().getBSSID();
    157         }
    158 
    159         @Override
    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 
    167     public IconCache(OSUManager osuManager) {
    168         mOsuManager = osuManager;
    169         mPending = new HashMap<>();
    170         mCache = new HashMap<>();
    171     }
    172 
    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 
    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 
    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 
    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 
    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 
    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 
    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