1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.wifi;
18
19import android.net.wifi.ScanResult;
20import android.net.wifi.WifiConfiguration;
21import android.os.SystemClock;
22import android.util.Log;
23
24import com.android.server.wifi.hotspot2.PasspointMatch;
25import com.android.server.wifi.hotspot2.PasspointMatchInfo;
26import com.android.server.wifi.hotspot2.pps.HomeSP;
27
28import java.util.ArrayList;
29import java.util.Collection;
30import java.util.Collections;
31import java.util.Comparator;
32import java.util.Iterator;
33import java.util.concurrent.ConcurrentHashMap;
34
35/**
36 * Maps BSSIDs to their individual ScanDetails for a given WifiConfiguration.
37 */
38public class ScanDetailCache {
39
40    private static final String TAG = "ScanDetailCache";
41    private static final boolean DBG = false;
42
43    private WifiConfiguration mConfig;
44    private ConcurrentHashMap<String, ScanDetail> mMap;
45    private ConcurrentHashMap<String, PasspointMatchInfo> mPasspointMatches;
46
47    ScanDetailCache(WifiConfiguration config) {
48        mConfig = config;
49        mMap = new ConcurrentHashMap(16, 0.75f, 2);
50        mPasspointMatches = new ConcurrentHashMap(16, 0.75f, 2);
51    }
52
53    void put(ScanDetail scanDetail) {
54        put(scanDetail, null, null);
55    }
56
57    void put(ScanDetail scanDetail, PasspointMatch match, HomeSP homeSp) {
58
59        mMap.put(scanDetail.getBSSIDString(), scanDetail);
60
61        if (match != null && homeSp != null) {
62            mPasspointMatches.put(scanDetail.getBSSIDString(),
63                    new PasspointMatchInfo(match, scanDetail, homeSp));
64        }
65    }
66
67    ScanResult get(String bssid) {
68        ScanDetail scanDetail = getScanDetail(bssid);
69        return scanDetail == null ? null : scanDetail.getScanResult();
70    }
71
72    ScanDetail getScanDetail(String bssid) {
73        return mMap.get(bssid);
74    }
75
76    void remove(String bssid) {
77        mMap.remove(bssid);
78    }
79
80    int size() {
81        return mMap.size();
82    }
83
84    boolean isEmpty() {
85        return size() == 0;
86    }
87
88    ScanDetail getFirst() {
89        Iterator<ScanDetail> it = mMap.values().iterator();
90        return it.hasNext() ? it.next() : null;
91    }
92
93    Collection<String> keySet() {
94        return mMap.keySet();
95    }
96
97    Collection<ScanDetail> values() {
98        return mMap.values();
99    }
100
101    /**
102     * Method to reduce the cache to the given size by removing the oldest entries.
103     *
104     * @param num int target cache size
105     */
106    public void trim(int num) {
107        int currentSize = mMap.size();
108        if (currentSize <= num) {
109            return; // Nothing to trim
110        }
111        ArrayList<ScanDetail> list = new ArrayList<ScanDetail>(mMap.values());
112        if (list.size() != 0) {
113            // Sort by descending timestamp
114            Collections.sort(list, new Comparator() {
115                public int compare(Object o1, Object o2) {
116                    ScanDetail a = (ScanDetail) o1;
117                    ScanDetail b = (ScanDetail) o2;
118                    if (a.getSeen() > b.getSeen()) {
119                        return 1;
120                    }
121                    if (a.getSeen() < b.getSeen()) {
122                        return -1;
123                    }
124                    return a.getBSSIDString().compareTo(b.getBSSIDString());
125                }
126            });
127        }
128        for (int i = 0; i < currentSize - num; i++) {
129            // Remove oldest results from scan cache
130            ScanDetail result = list.get(i);
131            mMap.remove(result.getBSSIDString());
132            mPasspointMatches.remove(result.getBSSIDString());
133        }
134    }
135
136    /* @hide */
137    private ArrayList<ScanDetail> sort() {
138        ArrayList<ScanDetail> list = new ArrayList<ScanDetail>(mMap.values());
139        if (list.size() != 0) {
140            Collections.sort(list, new Comparator() {
141                public int compare(Object o1, Object o2) {
142                    ScanResult a = ((ScanDetail) o1).getScanResult();
143                    ScanResult b = ((ScanDetail) o2).getScanResult();
144                    if (a.numIpConfigFailures > b.numIpConfigFailures) {
145                        return 1;
146                    }
147                    if (a.numIpConfigFailures < b.numIpConfigFailures) {
148                        return -1;
149                    }
150                    if (a.seen > b.seen) {
151                        return -1;
152                    }
153                    if (a.seen < b.seen) {
154                        return 1;
155                    }
156                    if (a.level > b.level) {
157                        return -1;
158                    }
159                    if (a.level < b.level) {
160                        return 1;
161                    }
162                    return a.BSSID.compareTo(b.BSSID);
163                }
164            });
165        }
166        return list;
167    }
168
169    /**
170     * Method to get cached scan results that are less than 'age' old.
171     *
172     * @param age long Time window of desired results.
173     * @return WifiConfiguration.Visibility matches in the given visibility
174     */
175    public WifiConfiguration.Visibility getVisibilityByRssi(long age) {
176        WifiConfiguration.Visibility status = new WifiConfiguration.Visibility();
177
178        long now_ms = System.currentTimeMillis();
179        long now_elapsed_ms = SystemClock.elapsedRealtime();
180        for (ScanDetail scanDetail : values()) {
181            ScanResult result = scanDetail.getScanResult();
182            if (scanDetail.getSeen() == 0) {
183                continue;
184            }
185
186            if (result.is5GHz()) {
187                //strictly speaking: [4915, 5825]
188                //number of known BSSID on 5GHz band
189                status.num5 = status.num5 + 1;
190            } else if (result.is24GHz()) {
191                //strictly speaking: [2412, 2482]
192                //number of known BSSID on 2.4Ghz band
193                status.num24 = status.num24 + 1;
194            }
195
196            if (result.timestamp != 0) {
197                if (DBG) {
198                    Log.e("getVisibilityByRssi", " considering " + result.SSID + " " + result.BSSID
199                            + " elapsed=" + now_elapsed_ms + " timestamp=" + result.timestamp
200                            + " age = " + age);
201                }
202                if ((now_elapsed_ms - (result.timestamp / 1000)) > age) continue;
203            } else {
204                // This checks the time at which we have received the scan result from supplicant
205                if ((now_ms - result.seen) > age) continue;
206            }
207
208            if (result.is5GHz()) {
209                if (result.level > status.rssi5) {
210                    status.rssi5 = result.level;
211                    status.age5 = result.seen;
212                    status.BSSID5 = result.BSSID;
213                }
214            } else if (result.is24GHz()) {
215                if (result.level > status.rssi24) {
216                    status.rssi24 = result.level;
217                    status.age24 = result.seen;
218                    status.BSSID24 = result.BSSID;
219                }
220            }
221        }
222
223        return status;
224    }
225
226    /**
227     * Method returning the Visibility based on passpoint match time.
228     *
229     * @param age long Desired time window for matches.
230     * @return WifiConfiguration.Visibility matches in the given visibility
231     */
232    public WifiConfiguration.Visibility getVisibilityByPasspointMatch(long age) {
233
234        long now_ms = System.currentTimeMillis();
235        PasspointMatchInfo pmiBest24 = null, pmiBest5 = null;
236
237        for (PasspointMatchInfo pmi : mPasspointMatches.values()) {
238            ScanDetail scanDetail = pmi.getScanDetail();
239            if (scanDetail == null) continue;
240            ScanResult result = scanDetail.getScanResult();
241            if (result == null) continue;
242
243            if (scanDetail.getSeen() == 0) continue;
244
245            if ((now_ms - result.seen) > age) continue;
246
247            if (result.is5GHz()) {
248                if (pmiBest5 == null || pmiBest5.compareTo(pmi) < 0) {
249                    pmiBest5 = pmi;
250                }
251            } else if (result.is24GHz()) {
252                if (pmiBest24 == null || pmiBest24.compareTo(pmi) < 0) {
253                    pmiBest24 = pmi;
254                }
255            }
256        }
257
258        WifiConfiguration.Visibility status = new WifiConfiguration.Visibility();
259        String logMsg = "Visiblity by passpoint match returned ";
260        if (pmiBest5 != null) {
261            ScanResult result = pmiBest5.getScanDetail().getScanResult();
262            status.rssi5 = result.level;
263            status.age5 = result.seen;
264            status.BSSID5 = result.BSSID;
265            logMsg += "5 GHz BSSID of " + result.BSSID;
266        }
267        if (pmiBest24 != null) {
268            ScanResult result = pmiBest24.getScanDetail().getScanResult();
269            status.rssi24 = result.level;
270            status.age24 = result.seen;
271            status.BSSID24 = result.BSSID;
272            logMsg += "2.4 GHz BSSID of " + result.BSSID;
273        }
274
275        Log.d(TAG, logMsg);
276
277        return status;
278    }
279
280    /**
281     * Method to get scan matches for the desired time window.  Returns matches by passpoint time if
282     * the WifiConfiguration is passpoint.
283     *
284     * @param age long desired time for matches.
285     * @return WifiConfiguration.Visibility matches in the given visibility
286     */
287    public WifiConfiguration.Visibility getVisibility(long age) {
288        if (mConfig.isPasspoint()) {
289            return getVisibilityByPasspointMatch(age);
290        } else {
291            return getVisibilityByRssi(age);
292        }
293    }
294
295
296
297    @Override
298    public String toString() {
299        StringBuilder sbuf = new StringBuilder();
300        sbuf.append("Scan Cache:  ").append('\n');
301
302        ArrayList<ScanDetail> list = sort();
303        long now_ms = System.currentTimeMillis();
304        if (list.size() > 0) {
305            for (ScanDetail scanDetail : list) {
306                ScanResult result = scanDetail.getScanResult();
307                long milli = now_ms - scanDetail.getSeen();
308                long ageSec = 0;
309                long ageMin = 0;
310                long ageHour = 0;
311                long ageMilli = 0;
312                long ageDay = 0;
313                if (now_ms > scanDetail.getSeen() && scanDetail.getSeen() > 0) {
314                    ageMilli = milli % 1000;
315                    ageSec   = (milli / 1000) % 60;
316                    ageMin   = (milli / (60 * 1000)) % 60;
317                    ageHour  = (milli / (60 * 60 * 1000)) % 24;
318                    ageDay   = (milli / (24 * 60 * 60 * 1000));
319                }
320                sbuf.append("{").append(result.BSSID).append(",").append(result.frequency);
321                sbuf.append(",").append(String.format("%3d", result.level));
322                if (ageSec > 0 || ageMilli > 0) {
323                    sbuf.append(String.format(",%4d.%02d.%02d.%02d.%03dms", ageDay,
324                            ageHour, ageMin, ageSec, ageMilli));
325                }
326                if (result.numIpConfigFailures > 0) {
327                    sbuf.append(",ipfail=");
328                    sbuf.append(result.numIpConfigFailures);
329                }
330                sbuf.append("} ");
331            }
332            sbuf.append('\n');
333        }
334
335        return sbuf.toString();
336    }
337
338}
339