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 java.util.ArrayList;
25import java.util.Collection;
26import java.util.Collections;
27import java.util.Comparator;
28import java.util.HashMap;
29
30/**
31 * Maps BSSIDs to their individual ScanDetails for a given WifiConfiguration.
32 */
33public class ScanDetailCache {
34
35    private static final String TAG = "ScanDetailCache";
36    private static final boolean DBG = false;
37
38    private final WifiConfiguration mConfig;
39    private final int mMaxSize;
40    private final int mTrimSize;
41    private final HashMap<String, ScanDetail> mMap;
42
43    /**
44     * Scan Detail cache associated with each configured network.
45     *
46     * The cache size is trimmed down to |trimSize| once it crosses the provided |maxSize|.
47     * Since this operation is relatively expensive, ensure that |maxSize| and |trimSize| are not
48     * too close to each other. |trimSize| should always be <= |maxSize|.
49     *
50     * @param config   WifiConfiguration object corresponding to the network.
51     * @param maxSize  Max size desired for the cache.
52     * @param trimSize Size to trim the cache down to once it reaches |maxSize|.
53     */
54    ScanDetailCache(WifiConfiguration config, int maxSize, int trimSize) {
55        mConfig = config;
56        mMaxSize = maxSize;
57        mTrimSize = trimSize;
58        mMap = new HashMap(16, 0.75f);
59    }
60
61    void put(ScanDetail scanDetail) {
62        // First check if we have reached |maxSize|. if yes, trim it down to |trimSize|.
63        if (mMap.size() >= mMaxSize) {
64            trim();
65        }
66
67        mMap.put(scanDetail.getBSSIDString(), scanDetail);
68    }
69
70    ScanResult get(String bssid) {
71        ScanDetail scanDetail = getScanDetail(bssid);
72        return scanDetail == null ? null : scanDetail.getScanResult();
73    }
74
75    ScanDetail getScanDetail(String bssid) {
76        return mMap.get(bssid);
77    }
78
79    void remove(String bssid) {
80        mMap.remove(bssid);
81    }
82
83    int size() {
84        return mMap.size();
85    }
86
87    boolean isEmpty() {
88        return size() == 0;
89    }
90
91    Collection<String> keySet() {
92        return mMap.keySet();
93    }
94
95    Collection<ScanDetail> values() {
96        return mMap.values();
97    }
98
99    /**
100     * Method to reduce the cache to |mTrimSize| size by removing the oldest entries.
101     * TODO: Investigate if this method can be further optimized.
102     */
103    private void trim() {
104        int currentSize = mMap.size();
105        if (currentSize < mTrimSize) {
106            return; // Nothing to trim
107        }
108        ArrayList<ScanDetail> list = new ArrayList<ScanDetail>(mMap.values());
109        if (list.size() != 0) {
110            // Sort by descending timestamp
111            Collections.sort(list, new Comparator() {
112                public int compare(Object o1, Object o2) {
113                    ScanDetail a = (ScanDetail) o1;
114                    ScanDetail b = (ScanDetail) o2;
115                    if (a.getSeen() > b.getSeen()) {
116                        return 1;
117                    }
118                    if (a.getSeen() < b.getSeen()) {
119                        return -1;
120                    }
121                    return a.getBSSIDString().compareTo(b.getBSSIDString());
122                }
123            });
124        }
125        for (int i = 0; i < currentSize - mTrimSize; i++) {
126            // Remove oldest results from scan cache
127            ScanDetail result = list.get(i);
128            mMap.remove(result.getBSSIDString());
129        }
130    }
131
132    /* @hide */
133    private ArrayList<ScanDetail> sort() {
134        ArrayList<ScanDetail> list = new ArrayList<ScanDetail>(mMap.values());
135        if (list.size() != 0) {
136            Collections.sort(list, new Comparator() {
137                public int compare(Object o1, Object o2) {
138                    ScanResult a = ((ScanDetail) o1).getScanResult();
139                    ScanResult b = ((ScanDetail) o2).getScanResult();
140                    if (a.numIpConfigFailures > b.numIpConfigFailures) {
141                        return 1;
142                    }
143                    if (a.numIpConfigFailures < b.numIpConfigFailures) {
144                        return -1;
145                    }
146                    if (a.seen > b.seen) {
147                        return -1;
148                    }
149                    if (a.seen < b.seen) {
150                        return 1;
151                    }
152                    if (a.level > b.level) {
153                        return -1;
154                    }
155                    if (a.level < b.level) {
156                        return 1;
157                    }
158                    return a.BSSID.compareTo(b.BSSID);
159                }
160            });
161        }
162        return list;
163    }
164
165    /**
166     * Method to get cached scan results that are less than 'age' old.
167     *
168     * @param age long Time window of desired results.
169     * @return WifiConfiguration.Visibility matches in the given visibility
170     */
171    public WifiConfiguration.Visibility getVisibilityByRssi(long age) {
172        WifiConfiguration.Visibility status = new WifiConfiguration.Visibility();
173
174        long now_ms = System.currentTimeMillis();
175        long now_elapsed_ms = SystemClock.elapsedRealtime();
176        for (ScanDetail scanDetail : values()) {
177            ScanResult result = scanDetail.getScanResult();
178            if (scanDetail.getSeen() == 0) {
179                continue;
180            }
181
182            if (result.is5GHz()) {
183                //strictly speaking: [4915, 5825]
184                //number of known BSSID on 5GHz band
185                status.num5 = status.num5 + 1;
186            } else if (result.is24GHz()) {
187                //strictly speaking: [2412, 2482]
188                //number of known BSSID on 2.4Ghz band
189                status.num24 = status.num24 + 1;
190            }
191
192            if (result.timestamp != 0) {
193                if (DBG) {
194                    Log.e("getVisibilityByRssi", " considering " + result.SSID + " " + result.BSSID
195                            + " elapsed=" + now_elapsed_ms + " timestamp=" + result.timestamp
196                            + " age = " + age);
197                }
198                if ((now_elapsed_ms - (result.timestamp / 1000)) > age) continue;
199            } else {
200                // This checks the time at which we have received the scan result from supplicant
201                if ((now_ms - result.seen) > age) continue;
202            }
203
204            if (result.is5GHz()) {
205                if (result.level > status.rssi5) {
206                    status.rssi5 = result.level;
207                    status.age5 = result.seen;
208                    status.BSSID5 = result.BSSID;
209                }
210            } else if (result.is24GHz()) {
211                if (result.level > status.rssi24) {
212                    status.rssi24 = result.level;
213                    status.age24 = result.seen;
214                    status.BSSID24 = result.BSSID;
215                }
216            }
217        }
218
219        return status;
220    }
221
222    /**
223     * Method to get scan matches for the desired time window.  Returns matches by passpoint time if
224     * the WifiConfiguration is passpoint.
225     *
226     * @param age long desired time for matches.
227     * @return WifiConfiguration.Visibility matches in the given visibility
228     */
229    public WifiConfiguration.Visibility getVisibility(long age) {
230        return getVisibilityByRssi(age);
231    }
232
233
234    @Override
235    public String toString() {
236        StringBuilder sbuf = new StringBuilder();
237        sbuf.append("Scan Cache:  ").append('\n');
238
239        ArrayList<ScanDetail> list = sort();
240        long now_ms = System.currentTimeMillis();
241        if (list.size() > 0) {
242            for (ScanDetail scanDetail : list) {
243                ScanResult result = scanDetail.getScanResult();
244                long milli = now_ms - scanDetail.getSeen();
245                long ageSec = 0;
246                long ageMin = 0;
247                long ageHour = 0;
248                long ageMilli = 0;
249                long ageDay = 0;
250                if (now_ms > scanDetail.getSeen() && scanDetail.getSeen() > 0) {
251                    ageMilli = milli % 1000;
252                    ageSec   = (milli / 1000) % 60;
253                    ageMin   = (milli / (60 * 1000)) % 60;
254                    ageHour  = (milli / (60 * 60 * 1000)) % 24;
255                    ageDay   = (milli / (24 * 60 * 60 * 1000));
256                }
257                sbuf.append("{").append(result.BSSID).append(",").append(result.frequency);
258                sbuf.append(",").append(String.format("%3d", result.level));
259                if (ageSec > 0 || ageMilli > 0) {
260                    sbuf.append(String.format(",%4d.%02d.%02d.%02d.%03dms", ageDay,
261                            ageHour, ageMin, ageSec, ageMilli));
262                }
263                if (result.numIpConfigFailures > 0) {
264                    sbuf.append(",ipfail=");
265                    sbuf.append(result.numIpConfigFailures);
266                }
267                sbuf.append("} ");
268            }
269            sbuf.append('\n');
270        }
271
272        return sbuf.toString();
273    }
274
275}
276