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 
17 package com.android.server.wifi;
18 
19 import android.annotation.NonNull;
20 import android.net.wifi.ScanResult;
21 import android.net.wifi.WifiConfiguration;
22 
23 import com.android.server.wifi.hotspot2.NetworkDetail;
24 
25 import java.util.ArrayList;
26 import java.util.Collection;
27 import java.util.Collections;
28 import java.util.Comparator;
29 import java.util.HashMap;
30 
31 /**
32  * Maps BSSIDs to their individual ScanDetails for a given WifiConfiguration.
33  */
34 public class ScanDetailCache {
35 
36     private static final String TAG = "ScanDetailCache";
37     private static final boolean DBG = false;
38 
39     private final WifiConfiguration mConfig;
40     private final int mMaxSize;
41     private final int mTrimSize;
42     private final HashMap<String, ScanDetail> mMap;
43 
44     /**
45      * Scan Detail cache associated with each configured network.
46      *
47      * The cache size is trimmed down to |trimSize| once it crosses the provided |maxSize|.
48      * Since this operation is relatively expensive, ensure that |maxSize| and |trimSize| are not
49      * too close to each other. |trimSize| should always be <= |maxSize|.
50      *
51      * @param config   WifiConfiguration object corresponding to the network.
52      * @param maxSize  Max size desired for the cache.
53      * @param trimSize Size to trim the cache down to once it reaches |maxSize|.
54      */
ScanDetailCache(WifiConfiguration config, int maxSize, int trimSize)55     ScanDetailCache(WifiConfiguration config, int maxSize, int trimSize) {
56         mConfig = config;
57         mMaxSize = maxSize;
58         mTrimSize = trimSize;
59         mMap = new HashMap(16, 0.75f);
60     }
61 
put(ScanDetail scanDetail)62     void put(ScanDetail scanDetail) {
63         // First check if we have reached |maxSize|. if yes, trim it down to |trimSize|.
64         if (mMap.size() >= mMaxSize) {
65             trim();
66         }
67 
68         mMap.put(scanDetail.getBSSIDString(), scanDetail);
69     }
70 
71     /**
72      * Get ScanResult object corresponding to the provided BSSID.
73      *
74      * @param bssid provided BSSID
75      * @return {@code null} if no match ScanResult is found.
76      */
getScanResult(String bssid)77     public ScanResult getScanResult(String bssid) {
78         ScanDetail scanDetail = getScanDetail(bssid);
79         return scanDetail == null ? null : scanDetail.getScanResult();
80     }
81 
82     /**
83      * Get ScanDetail object corresponding to the provided BSSID.
84      *
85      * @param bssid provided BSSID
86      * @return {@code null} if no match ScanDetail is found.
87      */
getScanDetail(@onNull String bssid)88     public ScanDetail getScanDetail(@NonNull String bssid) {
89         return mMap.get(bssid);
90     }
91 
remove(@onNull String bssid)92     void remove(@NonNull String bssid) {
93         mMap.remove(bssid);
94     }
95 
size()96     int size() {
97         return mMap.size();
98     }
99 
isEmpty()100     boolean isEmpty() {
101         return size() == 0;
102     }
103 
keySet()104     Collection<String> keySet() {
105         return mMap.keySet();
106     }
107 
values()108     Collection<ScanDetail> values() {
109         return mMap.values();
110     }
111 
112     /**
113      * Method to reduce the cache to |mTrimSize| size by removing the oldest entries.
114      * TODO: Investigate if this method can be further optimized.
115      */
trim()116     private void trim() {
117         int currentSize = mMap.size();
118         if (currentSize < mTrimSize) {
119             return; // Nothing to trim
120         }
121         ArrayList<ScanDetail> list = new ArrayList<ScanDetail>(mMap.values());
122         if (list.size() != 0) {
123             // Sort by ascending timestamp (oldest scan results first)
124             Collections.sort(list, new Comparator() {
125                 public int compare(Object o1, Object o2) {
126                     ScanDetail a = (ScanDetail) o1;
127                     ScanDetail b = (ScanDetail) o2;
128                     if (a.getSeen() > b.getSeen()) {
129                         return 1;
130                     }
131                     if (a.getSeen() < b.getSeen()) {
132                         return -1;
133                     }
134                     return a.getBSSIDString().compareTo(b.getBSSIDString());
135                 }
136             });
137         }
138         for (int i = 0; i < currentSize - mTrimSize; i++) {
139             // Remove oldest results from scan cache
140             ScanDetail result = list.get(i);
141             mMap.remove(result.getBSSIDString());
142         }
143     }
144 
145     /**
146      * Return the most recent ScanResult for this network, or null if non exists.
147      */
getMostRecentScanResult()148     public ScanResult getMostRecentScanResult() {
149         ArrayList<ScanDetail> list = sort();
150         if (list.size() == 0) {
151             return null;
152         }
153         return list.get(0).getScanResult();
154     }
155 
156     /**
157      * Returns the list of sorted ScanDetails in descending order of timestamp, followed by
158      * descending order of RSSI.
159      * @hide
160      **/
sort()161     private ArrayList<ScanDetail> sort() {
162         ArrayList<ScanDetail> list = new ArrayList<ScanDetail>(mMap.values());
163         if (list.size() != 0) {
164             Collections.sort(list, new Comparator() {
165                 public int compare(Object o1, Object o2) {
166                     ScanResult a = ((ScanDetail) o1).getScanResult();
167                     ScanResult b = ((ScanDetail) o2).getScanResult();
168                     if (a.seen > b.seen) {
169                         return -1;
170                     }
171                     if (a.seen < b.seen) {
172                         return 1;
173                     }
174                     if (a.level > b.level) {
175                         return -1;
176                     }
177                     if (a.level < b.level) {
178                         return 1;
179                     }
180                     return a.BSSID.compareTo(b.BSSID);
181                 }
182             });
183         }
184         return list;
185     }
186 
187     @Override
toString()188     public String toString() {
189         StringBuilder sbuf = new StringBuilder();
190         sbuf.append("Scan Cache:  ").append('\n');
191 
192         ArrayList<ScanDetail> list = sort();
193         long now_ms = System.currentTimeMillis();
194         if (list.size() > 0) {
195             for (ScanDetail scanDetail : list) {
196                 ScanResult result = scanDetail.getScanResult();
197                 long milli = now_ms - scanDetail.getSeen();
198                 long ageSec = 0;
199                 long ageMin = 0;
200                 long ageHour = 0;
201                 long ageMilli = 0;
202                 long ageDay = 0;
203                 if (now_ms > scanDetail.getSeen() && scanDetail.getSeen() > 0) {
204                     ageMilli = milli % 1000;
205                     ageSec   = (milli / 1000) % 60;
206                     ageMin   = (milli / (60 * 1000)) % 60;
207                     ageHour  = (milli / (60 * 60 * 1000)) % 24;
208                     ageDay   = (milli / (24 * 60 * 60 * 1000));
209                 }
210                 sbuf.append("{").append(result.BSSID).append(",").append(result.frequency);
211                 sbuf.append(",").append(result.level);
212                 if (ageSec > 0 || ageMilli > 0) {
213                     sbuf.append("," + ageDay + "." + ageHour + "." + ageMin + "." + ageSec + "."
214                             + ageMilli + "ms");
215                 }
216                 NetworkDetail networkDetail = scanDetail.getNetworkDetail();
217                 if (networkDetail != null && networkDetail.isInterworking()) {
218                     sbuf.append(",Ant=").append(networkDetail.getAnt()).append(",Internet=")
219                             .append(networkDetail.isInternet());
220                 }
221                 sbuf.append("} ");
222             }
223             sbuf.append('\n');
224         }
225 
226         return sbuf.toString();
227     }
228 
229 }
230