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    Collection<String> keySet() {
89        return mMap.keySet();
90    }
91
92    Collection<ScanDetail> values() {
93        return mMap.values();
94    }
95
96    /**
97     * Method to reduce the cache to the given size by removing the oldest entries.
98     *
99     * @param num int target cache size
100     */
101    public void trim(int num) {
102        int currentSize = mMap.size();
103        if (currentSize <= num) {
104            return; // Nothing to trim
105        }
106        ArrayList<ScanDetail> list = new ArrayList<ScanDetail>(mMap.values());
107        if (list.size() != 0) {
108            // Sort by descending timestamp
109            Collections.sort(list, new Comparator() {
110                public int compare(Object o1, Object o2) {
111                    ScanDetail a = (ScanDetail) o1;
112                    ScanDetail b = (ScanDetail) o2;
113                    if (a.getSeen() > b.getSeen()) {
114                        return 1;
115                    }
116                    if (a.getSeen() < b.getSeen()) {
117                        return -1;
118                    }
119                    return a.getBSSIDString().compareTo(b.getBSSIDString());
120                }
121            });
122        }
123        for (int i = 0; i < currentSize - num; i++) {
124            // Remove oldest results from scan cache
125            ScanDetail result = list.get(i);
126            mMap.remove(result.getBSSIDString());
127            mPasspointMatches.remove(result.getBSSIDString());
128        }
129    }
130
131    /* @hide */
132    private ArrayList<ScanDetail> sort() {
133        ArrayList<ScanDetail> list = new ArrayList<ScanDetail>(mMap.values());
134        if (list.size() != 0) {
135            Collections.sort(list, new Comparator() {
136                public int compare(Object o1, Object o2) {
137                    ScanResult a = ((ScanDetail) o1).getScanResult();
138                    ScanResult b = ((ScanDetail) o2).getScanResult();
139                    if (a.numIpConfigFailures > b.numIpConfigFailures) {
140                        return 1;
141                    }
142                    if (a.numIpConfigFailures < b.numIpConfigFailures) {
143                        return -1;
144                    }
145                    if (a.seen > b.seen) {
146                        return -1;
147                    }
148                    if (a.seen < b.seen) {
149                        return 1;
150                    }
151                    if (a.level > b.level) {
152                        return -1;
153                    }
154                    if (a.level < b.level) {
155                        return 1;
156                    }
157                    return a.BSSID.compareTo(b.BSSID);
158                }
159            });
160        }
161        return list;
162    }
163
164    /**
165     * Method to get cached scan results that are less than 'age' old.
166     *
167     * @param age long Time window of desired results.
168     * @return WifiConfiguration.Visibility matches in the given visibility
169     */
170    public WifiConfiguration.Visibility getVisibilityByRssi(long age) {
171        WifiConfiguration.Visibility status = new WifiConfiguration.Visibility();
172
173        long now_ms = System.currentTimeMillis();
174        long now_elapsed_ms = SystemClock.elapsedRealtime();
175        for (ScanDetail scanDetail : values()) {
176            ScanResult result = scanDetail.getScanResult();
177            if (scanDetail.getSeen() == 0) {
178                continue;
179            }
180
181            if (result.is5GHz()) {
182                //strictly speaking: [4915, 5825]
183                //number of known BSSID on 5GHz band
184                status.num5 = status.num5 + 1;
185            } else if (result.is24GHz()) {
186                //strictly speaking: [2412, 2482]
187                //number of known BSSID on 2.4Ghz band
188                status.num24 = status.num24 + 1;
189            }
190
191            if (result.timestamp != 0) {
192                if (DBG) {
193                    Log.e("getVisibilityByRssi", " considering " + result.SSID + " " + result.BSSID
194                            + " elapsed=" + now_elapsed_ms + " timestamp=" + result.timestamp
195                            + " age = " + age);
196                }
197                if ((now_elapsed_ms - (result.timestamp / 1000)) > age) continue;
198            } else {
199                // This checks the time at which we have received the scan result from supplicant
200                if ((now_ms - result.seen) > age) continue;
201            }
202
203            if (result.is5GHz()) {
204                if (result.level > status.rssi5) {
205                    status.rssi5 = result.level;
206                    status.age5 = result.seen;
207                    status.BSSID5 = result.BSSID;
208                }
209            } else if (result.is24GHz()) {
210                if (result.level > status.rssi24) {
211                    status.rssi24 = result.level;
212                    status.age24 = result.seen;
213                    status.BSSID24 = result.BSSID;
214                }
215            }
216        }
217
218        return status;
219    }
220
221    /**
222     * Method returning the Visibility based on passpoint match time.
223     *
224     * @param age long Desired time window for matches.
225     * @return WifiConfiguration.Visibility matches in the given visibility
226     */
227    public WifiConfiguration.Visibility getVisibilityByPasspointMatch(long age) {
228
229        long now_ms = System.currentTimeMillis();
230        PasspointMatchInfo pmiBest24 = null, pmiBest5 = null;
231
232        for (PasspointMatchInfo pmi : mPasspointMatches.values()) {
233            ScanDetail scanDetail = pmi.getScanDetail();
234            if (scanDetail == null) continue;
235            ScanResult result = scanDetail.getScanResult();
236            if (result == null) continue;
237
238            if (scanDetail.getSeen() == 0) continue;
239
240            if ((now_ms - result.seen) > age) continue;
241
242            if (result.is5GHz()) {
243                if (pmiBest5 == null || pmiBest5.compareTo(pmi) < 0) {
244                    pmiBest5 = pmi;
245                }
246            } else if (result.is24GHz()) {
247                if (pmiBest24 == null || pmiBest24.compareTo(pmi) < 0) {
248                    pmiBest24 = pmi;
249                }
250            }
251        }
252
253        WifiConfiguration.Visibility status = new WifiConfiguration.Visibility();
254        String logMsg = "Visiblity by passpoint match returned ";
255        if (pmiBest5 != null) {
256            ScanResult result = pmiBest5.getScanDetail().getScanResult();
257            status.rssi5 = result.level;
258            status.age5 = result.seen;
259            status.BSSID5 = result.BSSID;
260            logMsg += "5 GHz BSSID of " + result.BSSID;
261        }
262        if (pmiBest24 != null) {
263            ScanResult result = pmiBest24.getScanDetail().getScanResult();
264            status.rssi24 = result.level;
265            status.age24 = result.seen;
266            status.BSSID24 = result.BSSID;
267            logMsg += "2.4 GHz BSSID of " + result.BSSID;
268        }
269
270        Log.d(TAG, logMsg);
271
272        return status;
273    }
274
275    /**
276     * Method to get scan matches for the desired time window.  Returns matches by passpoint time if
277     * the WifiConfiguration is passpoint.
278     *
279     * @param age long desired time for matches.
280     * @return WifiConfiguration.Visibility matches in the given visibility
281     */
282    public WifiConfiguration.Visibility getVisibility(long age) {
283        if (mConfig.isPasspoint()) {
284            return getVisibilityByPasspointMatch(age);
285        } else {
286            return getVisibilityByRssi(age);
287        }
288    }
289
290
291
292    @Override
293    public String toString() {
294        StringBuilder sbuf = new StringBuilder();
295        sbuf.append("Scan Cache:  ").append('\n');
296
297        ArrayList<ScanDetail> list = sort();
298        long now_ms = System.currentTimeMillis();
299        if (list.size() > 0) {
300            for (ScanDetail scanDetail : list) {
301                ScanResult result = scanDetail.getScanResult();
302                long milli = now_ms - scanDetail.getSeen();
303                long ageSec = 0;
304                long ageMin = 0;
305                long ageHour = 0;
306                long ageMilli = 0;
307                long ageDay = 0;
308                if (now_ms > scanDetail.getSeen() && scanDetail.getSeen() > 0) {
309                    ageMilli = milli % 1000;
310                    ageSec   = (milli / 1000) % 60;
311                    ageMin   = (milli / (60 * 1000)) % 60;
312                    ageHour  = (milli / (60 * 60 * 1000)) % 24;
313                    ageDay   = (milli / (24 * 60 * 60 * 1000));
314                }
315                sbuf.append("{").append(result.BSSID).append(",").append(result.frequency);
316                sbuf.append(",").append(String.format("%3d", result.level));
317                if (ageSec > 0 || ageMilli > 0) {
318                    sbuf.append(String.format(",%4d.%02d.%02d.%02d.%03dms", ageDay,
319                            ageHour, ageMin, ageSec, ageMilli));
320                }
321                if (result.numIpConfigFailures > 0) {
322                    sbuf.append(",ipfail=");
323                    sbuf.append(result.numIpConfigFailures);
324                }
325                sbuf.append("} ");
326            }
327            sbuf.append('\n');
328        }
329
330        return sbuf.toString();
331    }
332
333}
334