• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
IconKey(OSUInfo osuInfo, String fileName)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 
getFileName()55         public String getFileName() {
56             return mFileName;
57         }
58 
59         @Override
equals(Object thatObject)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
hashCode()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
toString()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 
QueryEntry(IconKey key)98         private QueryEntry(IconKey key) {
99             mKey = key;
100             mLastSent = System.currentTimeMillis();
101         }
102 
getKey()103         private IconKey getKey() {
104             return mKey;
105         }
106 
bumpRetry()107         private int bumpRetry() {
108             mLastSent = System.currentTimeMillis();
109             return mRetry++;
110         }
111 
age(long now)112         private long age(long now) {
113             return now - mLastSent;
114         }
115 
116         @Override
toString()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 
QuerySet(OSUInfo osuInfo, List<IconInfo> icons)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 
peek()134         private QueryEntry peek() {
135             return mEntries.getFirst();
136         }
137 
pop()138         private QueryEntry pop() {
139             mEntries.removeFirst();
140             return mEntries.isEmpty() ? null : mEntries.getFirst();
141         }
142 
isEmpty()143         private boolean isEmpty() {
144             return mEntries.isEmpty();
145         }
146 
getAllEntries()147         private List<QueryEntry> getAllEntries() {
148             return Collections.unmodifiableList(mEntries);
149         }
150 
getBssid()151         private long getBssid() {
152             return mOsuInfo.getBSSID();
153         }
154 
getOsuInfo()155         private OSUInfo getOsuInfo() {
156             return mOsuInfo;
157         }
158 
updateIcon(String fileName, HSIconFileElement iconFileElement)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 
updateIcon(IconKey key, HSIconFileElement iconFileElement)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
toString()199         public String toString() {
200             return "OSU " + mOsuInfo + ": " + mEntries;
201         }
202     }
203 
IconCache(OSUManager osuManager)204     public IconCache(OSUManager osuManager) {
205         mOSUManager = osuManager;
206     }
207 
clear()208     public void clear() {
209         mBssQueues.clear();
210         mCache.clear();
211     }
212 
enqueue(QuerySet querySet)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 
startIconQuery(OSUInfo osuInfo, List<IconInfo> icons)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 
initiateQuery(long bssid)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 
notifyIconReceived(long bssid, String fileName, byte[] iconData)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 
tickle(boolean wifiOff)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