• 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 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 
EssKey(OSUInfo osuInfo)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
equals(Object thatObject)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
hashCode()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
toString()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 
FileEntry(OSUInfo osuInfo, String fileName)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 
enqueu(OSUInfo osuInfo)127         private void enqueu(OSUInfo osuInfo) {
128             mQueued.addLast(osuInfo);
129             mBssids.add(osuInfo.getBSSID());
130         }
131 
update(long bssid, HSIconFileElement iconFileElement)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 
getAndIncrementRetry()143         private int getAndIncrementRetry() {
144             return mRetry++;
145         }
146 
getTimestamp()147         private long getTimestamp() {
148             return mTimestamp;
149         }
150 
getFileName()151         public String getFileName() {
152             return mFileName;
153         }
154 
getLastBssid()155         private long getLastBssid() {
156             return mQueued.getLast().getBSSID();
157         }
158 
159         @Override
toString()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 
IconCache(OSUManager osuManager)167     public IconCache(OSUManager osuManager) {
168         mOsuManager = osuManager;
169         mPending = new HashMap<>();
170         mCache = new HashMap<>();
171     }
172 
resolveIcons(Collection<OSUInfo> osuInfos)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 
getIcon(OSUInfo osuInfo)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 
notifyIconReceived(long bssid, String fileName, byte[] iconData)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 
tick(boolean wifiOff)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 
get(EssKey key, String fileName)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 
put(EssKey key, String fileName, HSIconFileElement icon)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 
enqueue(EssKey key, String fileName, OSUInfo osuInfo)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