1package com.android.hotspot2.osu;
2
3import android.util.Log;
4
5import com.android.anqp.HSIconFileElement;
6import com.android.anqp.IconInfo;
7import com.android.hotspot2.Utils;
8import com.android.hotspot2.flow.OSUInfo;
9
10import java.net.ProtocolException;
11import java.nio.BufferUnderflowException;
12import java.nio.ByteBuffer;
13import java.nio.ByteOrder;
14import java.util.Arrays;
15import java.util.Collection;
16import java.util.HashMap;
17import java.util.HashSet;
18import java.util.Iterator;
19import java.util.LinkedList;
20import java.util.List;
21import java.util.Locale;
22import java.util.Map;
23import java.util.Set;
24
25import static com.android.anqp.Constants.ANQPElementType.HSIconFile;
26
27public 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