1package com.android.server.wifi.hotspot2;
2
3import android.util.Log;
4
5import com.android.server.wifi.Clock;
6import com.android.server.wifi.anqp.ANQPElement;
7import com.android.server.wifi.anqp.Constants;
8
9import java.io.PrintWriter;
10import java.util.ArrayList;
11import java.util.HashMap;
12import java.util.List;
13import java.util.Map;
14
15public class AnqpCache {
16    private static final boolean DBG = false;
17
18    private static final long CACHE_RECHECK = 60000L;
19    private static final boolean STANDARD_ESS = true;  // Regular AP keying; see CacheKey below.
20    private long mLastSweep;
21    private Clock mClock;
22
23    private final HashMap<CacheKey, ANQPData> mANQPCache;
24
25    public AnqpCache(Clock clock) {
26        mClock = clock;
27        mANQPCache = new HashMap<>();
28        mLastSweep = mClock.currentTimeMillis();
29    }
30
31    private static class CacheKey {
32        private final String mSSID;
33        private final long mBSSID;
34        private final long mHESSID;
35
36        private CacheKey(String ssid, long bssid, long hessid) {
37            mSSID = ssid;
38            mBSSID = bssid;
39            mHESSID = hessid;
40        }
41
42        /**
43         * Build an ANQP cache key suitable for the granularity of the key space as follows:
44         *
45         * HESSID   domainID    standardESS     Key content Rationale
46         * -------- ----------- --------------- ----------- --------------------
47         * n/a      zero        n/a             SSID/BSSID  Domain ID indicates unique AP info
48         * not set  set         false           SSID/BSSID  Strict per AP keying override
49         * not set  set         true            SSID        Standard definition of an ESS
50         * set      set         n/a             HESSID      The ESS is defined by the HESSID
51         *
52         * @param network The network to build the key for.
53         * @param standardESS If this parameter is set the "standard" paradigm for an ESS is used
54         *                    for the cache, i.e. all APs with identical SSID is considered an ESS,
55         *                    otherwise caching is performed per AP.
56         * @return A CacheKey.
57         */
58        private static CacheKey buildKey(NetworkDetail network, boolean standardESS) {
59            String ssid;
60            long bssid;
61            long hessid;
62            if (network.getAnqpDomainID() == 0L || (network.getHESSID() == 0L && !standardESS)) {
63                ssid = network.getSSID();
64                bssid = network.getBSSID();
65                hessid = 0L;
66            }
67            else if (network.getHESSID() != 0L && network.getAnqpDomainID() > 0) {
68                ssid = null;
69                bssid = 0L;
70                hessid = network.getHESSID();
71            }
72            else {
73                ssid = network.getSSID();
74                bssid = 0L;
75                hessid = 0L;
76            }
77
78            return new CacheKey(ssid, bssid, hessid);
79        }
80
81        @Override
82        public int hashCode() {
83            if (mHESSID != 0) {
84                return (int)((mHESSID >>> 32) * 31 + mHESSID);
85            }
86            else if (mBSSID != 0) {
87                return (int)((mSSID.hashCode() * 31 + (mBSSID >>> 32)) * 31 + mBSSID);
88            }
89            else {
90                return mSSID.hashCode();
91            }
92        }
93
94        @Override
95        public boolean equals(Object thatObject) {
96            if (thatObject == this) {
97                return true;
98            }
99            else if (thatObject == null || thatObject.getClass() != CacheKey.class) {
100                return false;
101            }
102            CacheKey that = (CacheKey) thatObject;
103            return Utils.compare(that.mSSID, mSSID) == 0 &&
104                    that.mBSSID == mBSSID &&
105                    that.mHESSID == mHESSID;
106        }
107
108        @Override
109        public String toString() {
110            if (mHESSID != 0L) {
111                return "HESSID:" + NetworkDetail.toMACString(mHESSID);
112            }
113            else if (mBSSID != 0L) {
114                return NetworkDetail.toMACString(mBSSID) +
115                        ":<" + Utils.toUnicodeEscapedString(mSSID) + ">";
116            }
117            else {
118                return '<' + Utils.toUnicodeEscapedString(mSSID) + '>';
119            }
120        }
121    }
122
123    public List<Constants.ANQPElementType> initiate(NetworkDetail network,
124                                                    List<Constants.ANQPElementType> querySet) {
125        CacheKey key = CacheKey.buildKey(network, STANDARD_ESS);
126
127        synchronized (mANQPCache) {
128            ANQPData data = mANQPCache.get(key);
129            if (data == null || data.expired()) {
130                mANQPCache.put(key, new ANQPData(mClock, network, data));
131                return querySet;
132            }
133            else {
134                List<Constants.ANQPElementType> newList = data.disjoint(querySet);
135                Log.d(Utils.hs2LogTag(getClass()),
136                        String.format("New ANQP elements for BSSID %012x: %s",
137                                network.getBSSID(), newList));
138                return newList;
139            }
140        }
141    }
142
143    public void update(NetworkDetail network,
144                       Map<Constants.ANQPElementType, ANQPElement> anqpElements) {
145
146        CacheKey key = CacheKey.buildKey(network, STANDARD_ESS);
147
148        // Networks with a 0 ANQP Domain ID are still cached, but with a very short expiry, just
149        // long enough to prevent excessive re-querying.
150        synchronized (mANQPCache) {
151            ANQPData data = mANQPCache.get(key);
152            if (data != null && data.hasData()) {
153                data.merge(anqpElements);
154            }
155            else {
156                data = new ANQPData(mClock, network, anqpElements);
157                mANQPCache.put(key, data);
158            }
159        }
160    }
161
162    public ANQPData getEntry(NetworkDetail network) {
163        ANQPData data;
164
165        CacheKey key = CacheKey.buildKey(network, STANDARD_ESS);
166        synchronized (mANQPCache) {
167            data = mANQPCache.get(key);
168        }
169
170        return data != null && data.isValid(network) ? data : null;
171    }
172
173    public void clear(boolean all, boolean debug) {
174        if (DBG) Log.d(Utils.hs2LogTag(getClass()), "Clearing ANQP cache: all: " + all);
175        long now = mClock.currentTimeMillis();
176        synchronized (mANQPCache) {
177            if (all) {
178                mANQPCache.clear();
179                mLastSweep = now;
180            }
181            else if (now > mLastSweep + CACHE_RECHECK) {
182                List<CacheKey> retirees = new ArrayList<>();
183                for (Map.Entry<CacheKey, ANQPData> entry : mANQPCache.entrySet()) {
184                    if (entry.getValue().expired(now)) {
185                        retirees.add(entry.getKey());
186                    }
187                }
188                for (CacheKey key : retirees) {
189                    mANQPCache.remove(key);
190                    if (debug) {
191                        Log.d(Utils.hs2LogTag(getClass()), "Retired " + key);
192                    }
193                }
194                mLastSweep = now;
195            }
196        }
197    }
198
199    public void dump(PrintWriter out) {
200        out.println("Last sweep " + Utils.toHMS(mClock.currentTimeMillis() - mLastSweep) + " ago.");
201        for (ANQPData anqpData : mANQPCache.values()) {
202            out.println(anqpData.toString(false));
203        }
204    }
205}
206