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.scanner;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.net.wifi.ScanResult;
22 import android.net.wifi.WifiScanner;
23 import android.net.wifi.WifiScanner.ScanData;
24 import android.net.wifi.WifiScanner.ScanSettings;
25 import android.util.ArraySet;
26 import android.util.Pair;
27 import android.util.Rational;
28 import android.util.Slog;
29 
30 import com.android.server.wifi.WifiNative;
31 import com.android.server.wifi.scanner.ChannelHelper.ChannelCollection;
32 
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.Collection;
36 import java.util.Collections;
37 import java.util.Comparator;
38 import java.util.HashMap;
39 import java.util.Iterator;
40 import java.util.List;
41 import java.util.ListIterator;
42 import java.util.Map;
43 import java.util.Set;
44 
45 /**
46  * <p>This class takes a series of scan requests and formulates the best hardware level scanning
47  * schedule it can to try and satisfy requests. The hardware level accepts a series of buckets,
48  * where each bucket represents a set of channels and an interval to scan at. This
49  * scheduler operates as follows:</p>
50  *
51  * <p>Each new request is placed in the best predefined bucket. Once all requests have been added
52  * the last buckets (lower priority) are placed in the next best bucket until the number of buckets
53  * is less than the number supported by the hardware.
54  *
55  * <p>Finally, the scheduler creates a WifiNative.ScanSettings from the list of buckets which may be
56  * passed through the Wifi HAL.</p>
57  *
58  * <p>This class is not thread safe.</p>
59  */
60 public class BackgroundScanScheduler {
61 
62     private static final String TAG = "BackgroundScanScheduler";
63     private static final boolean DBG = false;
64 
65     public static final int DEFAULT_MAX_BUCKETS = 8;
66     // Max channels that can be specified per bucket
67     public static final int DEFAULT_MAX_CHANNELS_PER_BUCKET = 16;
68     // anecdotally, some chipsets will fail without explanation with a higher batch size, and
69     // there is apparently no way to retrieve the maximum batch size
70     public static final int DEFAULT_MAX_SCANS_TO_BATCH = 10;
71     public static final int DEFAULT_MAX_AP_PER_SCAN = 32;
72 
73     /**
74      * Value that all scan periods must be an integer multiple of
75      */
76     private static final int PERIOD_MIN_GCD_MS = 10000;
77     /**
78      * Default period to use if no buckets are being scheduled
79      */
80     private static final int DEFAULT_PERIOD_MS = 30000;
81     /**
82      * Scan report threshold percentage to assign to the schedule by default
83      * @see com.android.server.wifi.WifiNative.ScanSettings#report_threshold_percent
84      */
85     private static final int DEFAULT_REPORT_THRESHOLD_PERCENTAGE = 100;
86 
87     /**
88      * List of predefined periods (in ms) that buckets can be scheduled at. Ordered by preference
89      * if there are not enough buckets for all periods. All periods MUST be an integer multiple of
90      * the next smallest bucket with the smallest bucket having a period of PERIOD_MIN_GCD_MS.
91      * This requirement allows scans to be scheduled more efficiently because scan requests with
92      * intersecting channels will result in those channels being scanned exactly once at the smaller
93      * period and no unnecessary scan being scheduled. If this was not the case and two requests
94      * had channel 5 with periods of 15 seconds and 25 seconds then channel 5 would be scanned
95      * 296  (3600/15 + 3600/25 - 3500/75) times an hour instead of 240 times an hour (3600/15) if
96      * the 25s scan is rescheduled at 30s. This is less important with higher periods as it has
97      * significantly less impact. Ranking could be done by favoring shorter or longer; however,
98      * this would result in straying further from the requested period and possibly power
99      * implications if the scan is scheduled at a significantly lower period.
100      *
101      * For example if the hardware only supports 2 buckets and scans are requested with periods of
102      * 40s, 20s and 10s then the two buckets scheduled will have periods 40s and 20s and the 10s
103      * scan will be placed in the 20s bucket.
104      *
105      * If there are special scan requests such as exponential back off, we always dedicate a bucket
106      * for each type. Regular scan requests will be packed into the remaining buckets.
107      */
108     private static final int[] PREDEFINED_BUCKET_PERIODS = {
109         3 * PERIOD_MIN_GCD_MS,   // 30s
110         12 * PERIOD_MIN_GCD_MS,  // 120s
111         48 * PERIOD_MIN_GCD_MS,  // 480s
112         1 * PERIOD_MIN_GCD_MS,   // 10s
113         6 * PERIOD_MIN_GCD_MS,   // 60s
114         192 * PERIOD_MIN_GCD_MS, // 1920s
115         24 * PERIOD_MIN_GCD_MS,  // 240s
116         96 * PERIOD_MIN_GCD_MS,  // 960s
117         384 * PERIOD_MIN_GCD_MS, // 3840s
118         -1,                      // place holder for exponential back off scan
119     };
120 
121     private static final int EXPONENTIAL_BACK_OFF_BUCKET_IDX =
122             (PREDEFINED_BUCKET_PERIODS.length - 1);
123     private static final int NUM_OF_REGULAR_BUCKETS =
124             (PREDEFINED_BUCKET_PERIODS.length - 1);
125 
126     /**
127      * This class is an intermediate representation for scheduling. This maintins the channel
128      * collection to be scanned by the bucket as settings are added to it.
129      */
130     private class Bucket {
131         public int period;
132         public int bucketId;
133         private final List<ScanSettings> mScanSettingsList = new ArrayList<>();
134         private final ChannelCollection mChannelCollection;
135 
Bucket(int period)136         Bucket(int period) {
137             this.period = period;
138             this.bucketId = 0;
139             mScanSettingsList.clear();
140             mChannelCollection = mChannelHelper.createChannelCollection();
141         }
142 
143         /**
144          * Copy constructor which populates the settings list from the original bucket object.
145          */
Bucket(Bucket originalBucket)146         Bucket(Bucket originalBucket) {
147             this(originalBucket.period);
148             for (ScanSettings settings : originalBucket.getSettingsList()) {
149                 mScanSettingsList.add(settings);
150             }
151         }
152 
153         /**
154          * convert ChannelSpec to native representation
155          */
createChannelSettings(int frequency)156         private WifiNative.ChannelSettings createChannelSettings(int frequency) {
157             WifiNative.ChannelSettings channelSettings = new WifiNative.ChannelSettings();
158             channelSettings.frequency = frequency;
159             return channelSettings;
160         }
161 
addSettings(ScanSettings scanSettings)162         public boolean addSettings(ScanSettings scanSettings) {
163             mChannelCollection.addChannels(scanSettings);
164             return mScanSettingsList.add(scanSettings);
165         }
166 
removeSettings(ScanSettings scanSettings)167         public boolean removeSettings(ScanSettings scanSettings) {
168             if (mScanSettingsList.remove(scanSettings)) {
169                 // It's difficult to handle settings removal from buckets in terms of
170                 // maintaining the correct channel collection, so recreate the channel
171                 // collection from the remaining elements.
172                 updateChannelCollection();
173                 return true;
174             }
175             return false;
176         }
177 
getSettingsList()178         public List<ScanSettings> getSettingsList() {
179             return mScanSettingsList;
180         }
181 
updateChannelCollection()182         public void updateChannelCollection() {
183             mChannelCollection.clear();
184             for (ScanSettings settings : mScanSettingsList) {
185                 mChannelCollection.addChannels(settings);
186             }
187         }
188 
getChannelCollection()189         public ChannelCollection getChannelCollection() {
190             return mChannelCollection;
191         }
192 
193         /**
194          * convert the setting for this bucket to HAL representation
195          */
createBucketSettings(int bucketId, int maxChannels)196         public WifiNative.BucketSettings createBucketSettings(int bucketId, int maxChannels) {
197             this.bucketId = bucketId;
198             int reportEvents = WifiScanner.REPORT_EVENT_NO_BATCH;
199             int maxPeriodInMs = 0;
200             int stepCount = 0;
201             int bucketIndex = 0;
202 
203             for (int i = 0; i < mScanSettingsList.size(); ++i) {
204                 WifiScanner.ScanSettings setting = mScanSettingsList.get(i);
205                 int requestedReportEvents = setting.reportEvents;
206                 if ((requestedReportEvents & WifiScanner.REPORT_EVENT_NO_BATCH) == 0) {
207                     reportEvents &= ~WifiScanner.REPORT_EVENT_NO_BATCH;
208                 }
209                 if ((requestedReportEvents & WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN) != 0) {
210                     reportEvents |= WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN;
211                 }
212                 if ((requestedReportEvents & WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT) != 0) {
213                     reportEvents |= WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT;
214                 }
215                 // For the bucket allocated to exponential back off scan, the values of
216                 // the exponential back off scan related parameters from the very first
217                 // setting in the settings list will be used to configure this bucket.
218                 //
219                 if (i == 0 && setting.maxPeriodInMs != 0
220                         && setting.maxPeriodInMs != setting.periodInMs) {
221                     // Align the starting period with one of the pre-defined regular
222                     // scan periods. This will optimize the scan schedule when it has
223                     // both exponential back off scan and regular scan(s).
224                     bucketIndex = findBestRegularBucketIndex(setting.periodInMs,
225                                                      NUM_OF_REGULAR_BUCKETS);
226                     period = PREDEFINED_BUCKET_PERIODS[bucketIndex];
227                     maxPeriodInMs = (setting.maxPeriodInMs < period)
228                                     ? period
229                                     : setting.maxPeriodInMs;
230                     stepCount = setting.stepCount;
231                 }
232             }
233 
234             WifiNative.BucketSettings bucketSettings = new WifiNative.BucketSettings();
235             bucketSettings.bucket = bucketId;
236             bucketSettings.report_events = reportEvents;
237             bucketSettings.period_ms = period;
238             bucketSettings.max_period_ms = maxPeriodInMs;
239             bucketSettings.step_count = stepCount;
240             mChannelCollection.fillBucketSettings(bucketSettings, maxChannels);
241             return bucketSettings;
242         }
243     }
244 
245     /**
246      * Maintains a list of buckets and the number that are active (non-null)
247      */
248     private class BucketList {
249         // Comparator to sort the buckets in order of increasing time periods
250         private final Comparator<Bucket> mTimePeriodSortComparator =
251                 new Comparator<Bucket>() {
252                     public int compare(Bucket b1, Bucket b2) {
253                         return b1.period - b2.period;
254                     }
255                 };
256         private final Bucket[] mBuckets;
257         private int mActiveBucketCount = 0;
258 
BucketList()259         BucketList() {
260             mBuckets = new Bucket[PREDEFINED_BUCKET_PERIODS.length];
261         }
262 
clearAll()263         public void clearAll() {
264             Arrays.fill(mBuckets, null);
265             mActiveBucketCount = 0;
266         }
267 
clear(int index)268         public void clear(int index) {
269             if (mBuckets[index] != null) {
270                 --mActiveBucketCount;
271                 mBuckets[index] = null;
272             }
273         }
274 
getOrCreate(int index)275         public Bucket getOrCreate(int index) {
276             Bucket bucket = mBuckets[index];
277             if (bucket == null) {
278                 ++mActiveBucketCount;
279                 bucket = mBuckets[index] = new Bucket(PREDEFINED_BUCKET_PERIODS[index]);
280             }
281             return bucket;
282         }
283 
isActive(int index)284         public boolean isActive(int index) {
285             return mBuckets[index] != null;
286         }
287 
get(int index)288         public Bucket get(int index) {
289             return mBuckets[index];
290         }
291 
size()292         public int size() {
293             return mBuckets.length;
294         }
295 
getActiveCount()296         public int getActiveCount() {
297             return mActiveBucketCount;
298         }
299 
getActiveRegularBucketCount()300         public int getActiveRegularBucketCount() {
301             if (isActive(EXPONENTIAL_BACK_OFF_BUCKET_IDX)) {
302                 return mActiveBucketCount - 1;
303             } else {
304                 return mActiveBucketCount;
305             }
306         }
307 
308         /**
309          * Returns the active regular buckets sorted by their increasing time periods.
310          */
getSortedActiveRegularBucketList()311         public List<Bucket> getSortedActiveRegularBucketList() {
312             ArrayList<Bucket> activeBuckets = new ArrayList<>();
313             for (int i = 0; i < mBuckets.length; i++) {
314                 if (mBuckets[i] != null && i != EXPONENTIAL_BACK_OFF_BUCKET_IDX) {
315                     activeBuckets.add(mBuckets[i]);
316                 }
317             }
318             Collections.sort(activeBuckets, mTimePeriodSortComparator);
319             return activeBuckets;
320         }
321     }
322 
323     private int mMaxBuckets = DEFAULT_MAX_BUCKETS;
324     private int mMaxChannelsPerBucket = DEFAULT_MAX_CHANNELS_PER_BUCKET;
325     private int mMaxBatch = DEFAULT_MAX_SCANS_TO_BATCH;
326     private int mMaxApPerScan = DEFAULT_MAX_AP_PER_SCAN;
327 
getMaxBuckets()328     public int getMaxBuckets() {
329         return mMaxBuckets;
330     }
331 
setMaxBuckets(int maxBuckets)332     public void setMaxBuckets(int maxBuckets) {
333         mMaxBuckets = maxBuckets;
334     }
335 
getMaxChannelsPerBucket()336     public int getMaxChannelsPerBucket() {
337         return mMaxChannelsPerBucket;
338     }
339 
340     // TODO: find a way to get max channels
setMaxChannelsPerBucket(int maxChannels)341     public void setMaxChannelsPerBucket(int maxChannels) {
342         mMaxChannelsPerBucket = maxChannels;
343     }
344 
getMaxBatch()345     public int getMaxBatch() {
346         return mMaxBatch;
347     }
348 
349     // TODO: find a way to get max batch size
setMaxBatch(int maxBatch)350     public void setMaxBatch(int maxBatch) {
351         mMaxBatch = maxBatch;
352     }
353 
getMaxApPerScan()354     public int getMaxApPerScan() {
355         return mMaxApPerScan;
356     }
357 
setMaxApPerScan(int maxApPerScan)358     public void setMaxApPerScan(int maxApPerScan) {
359         mMaxApPerScan = maxApPerScan;
360     }
361 
362     private final BucketList mBuckets = new BucketList();
363     private final ChannelHelper mChannelHelper;
364     private WifiNative.ScanSettings mSchedule;
365     // This keeps track of the settings to the max time period bucket to which it was scheduled.
366     private final Map<ScanSettings, Bucket> mSettingsToScheduledBucket = new HashMap<>();
367 
BackgroundScanScheduler(ChannelHelper channelHelper)368     public BackgroundScanScheduler(ChannelHelper channelHelper) {
369         mChannelHelper = channelHelper;
370         createSchedule(new ArrayList<Bucket>(), getMaxChannelsPerBucket());
371     }
372 
373     /**
374      * Updates the schedule from the given set of requests.
375      */
updateSchedule(@onNull Collection<ScanSettings> requests)376     public void updateSchedule(@NonNull Collection<ScanSettings> requests) {
377         // create initial schedule
378         mBuckets.clearAll();
379         for (ScanSettings request : requests) {
380             addScanToBuckets(request);
381         }
382 
383         compactBuckets(getMaxBuckets());
384 
385         List<Bucket> bucketList = optimizeBuckets();
386 
387         List<Bucket> fixedBucketList =
388                 fixBuckets(bucketList, getMaxBuckets(), getMaxChannelsPerBucket());
389 
390         createSchedule(fixedBucketList, getMaxChannelsPerBucket());
391     }
392 
393     /**
394      * Retrieves the current scanning schedule.
395      */
getSchedule()396     public @NonNull WifiNative.ScanSettings getSchedule() {
397         return mSchedule;
398     }
399 
400     /**
401      * Returns true if the given scan result should be reported to a listener with the given
402      * settings.
403      */
shouldReportFullScanResultForSettings(@onNull ScanResult result, int bucketsScanned, @NonNull ScanSettings settings)404     public boolean shouldReportFullScanResultForSettings(@NonNull ScanResult result,
405             int bucketsScanned, @NonNull ScanSettings settings) {
406         return ScanScheduleUtil.shouldReportFullScanResultForSettings(mChannelHelper,
407                 result, bucketsScanned, settings, getScheduledBucket(settings));
408     }
409 
410     /**
411      * Returns a filtered version of the scan results from the chip that represents only the data
412      * requested in the settings. Will return null if the result should not be reported.
413      */
filterResultsForSettings(@onNull ScanData[] scanDatas, @NonNull ScanSettings settings)414     public @Nullable ScanData[] filterResultsForSettings(@NonNull ScanData[] scanDatas,
415             @NonNull ScanSettings settings) {
416         return ScanScheduleUtil.filterResultsForSettings(mChannelHelper, scanDatas, settings,
417                 getScheduledBucket(settings));
418     }
419 
420     /**
421      * Retrieves the max time period bucket idx at which this setting was scheduled
422      */
getScheduledBucket(ScanSettings settings)423     public int getScheduledBucket(ScanSettings settings) {
424         Bucket maxScheduledBucket = mSettingsToScheduledBucket.get(settings);
425         if (maxScheduledBucket != null) {
426             return maxScheduledBucket.bucketId;
427         } else {
428             Slog.wtf(TAG, "No bucket found for settings");
429             return -1;
430         }
431     }
432 
433     /**
434      * creates a schedule for the current buckets
435      */
createSchedule(List<Bucket> bucketList, int maxChannelsPerBucket)436     private void createSchedule(List<Bucket> bucketList, int maxChannelsPerBucket) {
437         WifiNative.ScanSettings schedule = new WifiNative.ScanSettings();
438         schedule.num_buckets = bucketList.size();
439         schedule.buckets = new WifiNative.BucketSettings[bucketList.size()];
440 
441         schedule.max_ap_per_scan = 0;
442         schedule.report_threshold_num_scans = getMaxBatch();
443 
444         // set all buckets in schedule
445         int bucketId = 0;
446         for (Bucket bucket : bucketList) {
447             schedule.buckets[bucketId] =
448                     bucket.createBucketSettings(bucketId, maxChannelsPerBucket);
449             for (ScanSettings settings : bucket.getSettingsList()) {
450                 // set APs per scan
451                 if (settings.numBssidsPerScan > schedule.max_ap_per_scan) {
452                     schedule.max_ap_per_scan = settings.numBssidsPerScan;
453                 }
454                 // set batching
455                 if (settings.maxScansToCache != 0
456                         && settings.maxScansToCache < schedule.report_threshold_num_scans) {
457                     schedule.report_threshold_num_scans = settings.maxScansToCache;
458                 }
459             }
460             bucketId++;
461         }
462 
463         schedule.report_threshold_percent = DEFAULT_REPORT_THRESHOLD_PERCENTAGE;
464 
465         if (schedule.max_ap_per_scan == 0 || schedule.max_ap_per_scan > getMaxApPerScan()) {
466             schedule.max_ap_per_scan = getMaxApPerScan();
467         }
468 
469         // update base period as gcd of periods
470         if (schedule.num_buckets > 0) {
471             int gcd = schedule.buckets[0].period_ms;
472             for (int b = 1; b < schedule.num_buckets; b++) {
473                 gcd = Rational.gcd(schedule.buckets[b].period_ms, gcd);
474             }
475 
476             if (gcd < PERIOD_MIN_GCD_MS) {
477                 Slog.wtf(TAG, "found gcd less than min gcd");
478                 gcd = PERIOD_MIN_GCD_MS;
479             }
480 
481             schedule.base_period_ms = gcd;
482         } else {
483             schedule.base_period_ms = DEFAULT_PERIOD_MS;
484         }
485 
486         mSchedule = schedule;
487     }
488 
489     /**
490      * Add a scan to the most appropriate bucket, creating the bucket if necessary.
491      */
addScanToBuckets(ScanSettings settings)492     private void addScanToBuckets(ScanSettings settings) {
493         int bucketIndex;
494 
495         if (settings.maxPeriodInMs != 0 && settings.maxPeriodInMs != settings.periodInMs) {
496             // exponential back off scan has a dedicated bucket
497             bucketIndex = EXPONENTIAL_BACK_OFF_BUCKET_IDX;
498         } else {
499             bucketIndex = findBestRegularBucketIndex(settings.periodInMs, NUM_OF_REGULAR_BUCKETS);
500         }
501 
502         mBuckets.getOrCreate(bucketIndex).addSettings(settings);
503     }
504 
505     /**
506      * find closest bucket period to the requested period in all predefined buckets
507      */
findBestRegularBucketIndex(int requestedPeriod, int maxNumBuckets)508     private static int findBestRegularBucketIndex(int requestedPeriod, int maxNumBuckets) {
509         maxNumBuckets = Math.min(maxNumBuckets, NUM_OF_REGULAR_BUCKETS);
510         int index = -1;
511         int minDiff = Integer.MAX_VALUE;
512         for (int i = 0; i < maxNumBuckets; ++i) {
513             int diff = Math.abs(PREDEFINED_BUCKET_PERIODS[i] - requestedPeriod);
514             if (diff < minDiff) {
515                 minDiff = diff;
516                 index = i;
517             }
518         }
519         if (index == -1) {
520             Slog.wtf(TAG, "Could not find best bucket for period " + requestedPeriod + " in "
521                      + maxNumBuckets + " buckets");
522         }
523         return index;
524     }
525 
526     /**
527      * Reduce the number of required buckets by reassigning lower priority buckets to the next
528      * closest period bucket.
529      */
compactBuckets(int maxBuckets)530     private void compactBuckets(int maxBuckets) {
531         int maxRegularBuckets = maxBuckets;
532 
533         // reserve one bucket for exponential back off scan if there is
534         // such request(s)
535         if (mBuckets.isActive(EXPONENTIAL_BACK_OFF_BUCKET_IDX)) {
536             maxRegularBuckets--;
537         }
538         for (int i = NUM_OF_REGULAR_BUCKETS - 1;
539                 i >= 0 && mBuckets.getActiveRegularBucketCount() > maxRegularBuckets; --i) {
540             if (mBuckets.isActive(i)) {
541                 for (ScanSettings scanRequest : mBuckets.get(i).getSettingsList()) {
542                     int newBucketIndex = findBestRegularBucketIndex(scanRequest.periodInMs, i);
543                     mBuckets.getOrCreate(newBucketIndex).addSettings(scanRequest);
544                 }
545                 mBuckets.clear(i);
546             }
547         }
548     }
549 
550     /**
551      * Clone the provided scan settings fields to a new ScanSettings object.
552      */
cloneScanSettings(ScanSettings originalSettings)553     private ScanSettings cloneScanSettings(ScanSettings originalSettings) {
554         ScanSettings settings = new ScanSettings();
555         settings.band = originalSettings.band;
556         settings.channels = originalSettings.channels;
557         settings.periodInMs = originalSettings.periodInMs;
558         settings.reportEvents = originalSettings.reportEvents;
559         settings.numBssidsPerScan = originalSettings.numBssidsPerScan;
560         settings.maxScansToCache = originalSettings.maxScansToCache;
561         settings.maxPeriodInMs = originalSettings.maxPeriodInMs;
562         settings.stepCount = originalSettings.stepCount;
563         settings.isPnoScan = originalSettings.isPnoScan;
564         return settings;
565     }
566 
567     /**
568      * Creates a split scan setting that needs to be added back to the current bucket.
569      */
createCurrentBucketSplitSettings(ScanSettings originalSettings, Set<Integer> currentBucketChannels)570     private ScanSettings createCurrentBucketSplitSettings(ScanSettings originalSettings,
571             Set<Integer> currentBucketChannels) {
572         ScanSettings currentBucketSettings = cloneScanSettings(originalSettings);
573         // Let's create a new settings for the current bucket with the same flags, but the missing
574         // channels from the other bucket
575         currentBucketSettings.band = WifiScanner.WIFI_BAND_UNSPECIFIED;
576         currentBucketSettings.channels = new WifiScanner.ChannelSpec[currentBucketChannels.size()];
577         int chanIdx = 0;
578         for (Integer channel : currentBucketChannels) {
579             currentBucketSettings.channels[chanIdx++] = new WifiScanner.ChannelSpec(channel);
580         }
581         return currentBucketSettings;
582     }
583 
584     /**
585      * Creates a split scan setting that needs to be added to the target lower time period bucket.
586      * The reportEvents field is modified to remove REPORT_EVENT_AFTER_EACH_SCAN because we
587      * need this flag only in the higher time period bucket.
588      */
createTargetBucketSplitSettings(ScanSettings originalSettings, Set<Integer> targetBucketChannels)589     private ScanSettings createTargetBucketSplitSettings(ScanSettings originalSettings,
590             Set<Integer> targetBucketChannels) {
591         ScanSettings targetBucketSettings = cloneScanSettings(originalSettings);
592         // The new settings for the other bucket will have the channels that already in the that
593         // bucket. We'll need to do some migration of the |reportEvents| flags.
594         targetBucketSettings.band = WifiScanner.WIFI_BAND_UNSPECIFIED;
595         targetBucketSettings.channels = new WifiScanner.ChannelSpec[targetBucketChannels.size()];
596         int chanIdx = 0;
597         for (Integer channel : targetBucketChannels) {
598             targetBucketSettings.channels[chanIdx++] = new WifiScanner.ChannelSpec(channel);
599         }
600         targetBucketSettings.reportEvents =
601                 originalSettings.reportEvents
602                         & (WifiScanner.REPORT_EVENT_NO_BATCH
603                                 | WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT);
604         return targetBucketSettings;
605     }
606 
607     /**
608      * Split the scan settings into 2 so that they can be put into 2 separate buckets.
609      * @return The first scan setting needs to be added back to the current bucket
610      *         The second scan setting needs to be added to the other bucket
611      */
createSplitSettings(ScanSettings originalSettings, ChannelCollection targetBucketChannelCol)612     private Pair<ScanSettings, ScanSettings> createSplitSettings(ScanSettings originalSettings,
613             ChannelCollection targetBucketChannelCol) {
614         Set<Integer> currentBucketChannels =
615                 targetBucketChannelCol.getMissingChannelsFromSettings(originalSettings);
616         Set<Integer> targetBucketChannels =
617                 targetBucketChannelCol.getContainingChannelsFromSettings(originalSettings);
618         // Two Copy of the original settings
619         ScanSettings currentBucketSettings =
620                 createCurrentBucketSplitSettings(originalSettings, currentBucketChannels);
621         ScanSettings targetBucketSettings =
622                 createTargetBucketSplitSettings(originalSettings, targetBucketChannels);
623         return Pair.create(currentBucketSettings, targetBucketSettings);
624     }
625 
626     /**
627      * Try to merge the settings to lower buckets.
628      * Check if the channels in this settings is already covered by a lower time period
629      * bucket. If it's partially covered, the settings is split else the entire settings
630      * is moved to the lower time period bucket.
631      * This method updates the |mSettingsToScheduledBucket| mapping.
632      * @return Pair<wasMerged, remainingSplitSettings>
633      *         wasMerged -  boolean indicating whether the original setting was merged to lower time
634      *                      period buckets.
635      *         remainingSplitSettings - Partial Scan Settings that need to be added back to the
636      *                                  current bucket.
637      */
mergeSettingsToLowerBuckets(ScanSettings originalSettings, Bucket currentBucket, ListIterator<Bucket> iterTargetBuckets)638     private Pair<Boolean, ScanSettings> mergeSettingsToLowerBuckets(ScanSettings originalSettings,
639             Bucket currentBucket, ListIterator<Bucket> iterTargetBuckets) {
640         ScanSettings remainingSplitSettings = null;
641         boolean wasMerged = false;
642         Bucket maxScheduledBucket = currentBucket;
643 
644         while (iterTargetBuckets.hasPrevious()) {
645             Bucket targetBucket = iterTargetBuckets.previous();
646             ChannelCollection targetBucketChannelCol = targetBucket.getChannelCollection();
647             if (targetBucketChannelCol.containsSettings(originalSettings)) {
648                 targetBucket.addSettings(originalSettings);
649                 // Update the max scheduled bucket for this setting
650                 maxScheduledBucket = targetBucket;
651                 wasMerged = true;
652             } else if (targetBucketChannelCol.partiallyContainsSettings(originalSettings)) {
653                 Pair<ScanSettings, ScanSettings> splitSettings;
654                 if (remainingSplitSettings == null) {
655                     splitSettings = createSplitSettings(originalSettings, targetBucketChannelCol);
656                 } else {
657                     splitSettings =
658                             createSplitSettings(remainingSplitSettings, targetBucketChannelCol);
659                 }
660                 targetBucket.addSettings(splitSettings.second);
661                 // Update the |remainingSplitSettings| to keep track of the remaining scan settings.
662                 // The original settings could be split across multiple buckets.
663                 remainingSplitSettings = splitSettings.first;
664                 wasMerged = true;
665             }
666         }
667         // Update the settings to scheduled bucket mapping. This is needed for event
668         // reporting lookup
669         mSettingsToScheduledBucket.put(originalSettings, maxScheduledBucket);
670 
671         return Pair.create(wasMerged, remainingSplitSettings);
672     }
673 
674     /**
675      * Optimize all the active buckets by removing duplicate channels in the buckets.
676      * This method tries to go through the settings in all the buckets and checks if the same
677      * channels for the setting is already being scanned by another bucked with lower time period.
678      * If yes, move the setting to the lower time period bucket. If all the settings from a higher
679      * period has been moved out, that bucket can be removed.
680      *
681      * We're trying to avoid cases where we have the same channels being scanned in different
682      * buckets. This is to workaround the fact that the HAL implementations have a max number of
683      * cumulative channel across buckets (b/28022609).
684      */
optimizeBuckets()685     private List<Bucket> optimizeBuckets() {
686         mSettingsToScheduledBucket.clear();
687         List<Bucket> sortedBuckets = mBuckets.getSortedActiveRegularBucketList();
688         ListIterator<Bucket> iterBuckets = sortedBuckets.listIterator();
689         // This is needed to keep track of split settings that need to be added back to the same
690         // bucket at the end of iterating thru all the settings. This has to be a separate temp list
691         // to prevent concurrent modification exceptions during iterations.
692         List<ScanSettings> currentBucketSplitSettingsList = new ArrayList<>();
693 
694         // We need to go thru each setting starting from the lowest time period bucket and check
695         // if they're already contained in a lower time period bucket. If yes, delete the setting
696         // from the current bucket and move it to the other bucket. If the settings are only
697         // partially contained, split the settings into two and move the partial bucket back
698         // to the same bucket. Finally, if all the settings have been moved out, remove the current
699         // bucket altogether.
700         while (iterBuckets.hasNext()) {
701             Bucket currentBucket = iterBuckets.next();
702             Iterator<ScanSettings> iterSettings = currentBucket.getSettingsList().iterator();
703 
704             currentBucketSplitSettingsList.clear();
705 
706             while (iterSettings.hasNext()) {
707                 ScanSettings currentSettings = iterSettings.next();
708                 ListIterator<Bucket> iterTargetBuckets =
709                         sortedBuckets.listIterator(iterBuckets.previousIndex());
710 
711                 Pair<Boolean, ScanSettings> mergeResult =
712                         mergeSettingsToLowerBuckets(
713                                 currentSettings, currentBucket, iterTargetBuckets);
714 
715                 boolean wasMerged = mergeResult.first.booleanValue();
716                 if (wasMerged) {
717                     // Remove the original settings from the current bucket.
718                     iterSettings.remove();
719                     ScanSettings remainingSplitSettings = mergeResult.second;
720                     if (remainingSplitSettings != null) {
721                         // Add back the remaining split settings to the current bucket.
722                         currentBucketSplitSettingsList.add(remainingSplitSettings);
723                     }
724                 }
725             }
726 
727             for (ScanSettings splitSettings: currentBucketSplitSettingsList) {
728                 currentBucket.addSettings(splitSettings);
729             }
730             if (currentBucket.getSettingsList().isEmpty()) {
731                 iterBuckets.remove();
732             } else {
733                 // Update the channel collection to account for the removed settings
734                 currentBucket.updateChannelCollection();
735             }
736         }
737 
738         // Update the settings to scheduled bucket map for all exponential scans.
739         if (mBuckets.isActive(EXPONENTIAL_BACK_OFF_BUCKET_IDX)) {
740             Bucket exponentialBucket = mBuckets.get(EXPONENTIAL_BACK_OFF_BUCKET_IDX);
741             for (ScanSettings settings : exponentialBucket.getSettingsList()) {
742                 mSettingsToScheduledBucket.put(settings, exponentialBucket);
743             }
744             sortedBuckets.add(exponentialBucket);
745         }
746 
747         return sortedBuckets;
748     }
749 
750     /**
751      * Partition the channel set into 2 or more based on the max channels that can be specified for
752      * each bucket.
753      */
partitionChannelSet(Set<Integer> originalChannelSet, int maxChannelsPerBucket)754     private List<Set<Integer>> partitionChannelSet(Set<Integer> originalChannelSet,
755             int maxChannelsPerBucket) {
756         ArrayList<Set<Integer>> channelSetList = new ArrayList();
757         ArraySet<Integer> channelSet = new ArraySet<>();
758         Iterator<Integer> iterChannels = originalChannelSet.iterator();
759 
760         while (iterChannels.hasNext()) {
761             channelSet.add(iterChannels.next());
762             if (channelSet.size() == maxChannelsPerBucket) {
763                 channelSetList.add(channelSet);
764                 channelSet = new ArraySet<>();
765             }
766         }
767         // Add the last partial set if any
768         if (!channelSet.isEmpty()) {
769             channelSetList.add(channelSet);
770         }
771         return channelSetList;
772     }
773 
774     /**
775      * Creates a list of split buckets with the channel collection corrected to fit the
776      * max channel list size that can be specified. The original channel collection will be split
777      * into multiple buckets with the same scan settings.
778      * Note: This does not update the mSettingsToScheduledBucket map because this bucket is
779      * essentially a copy of the original bucket, so it should not affect the event reporting.
780      * This bucket results will come back the same time the original bucket results come back.
781      */
createSplitBuckets(Bucket originalBucket, List<Set<Integer>> channelSets)782     private List<Bucket> createSplitBuckets(Bucket originalBucket, List<Set<Integer>> channelSets) {
783         List<Bucket> splitBucketList = new ArrayList<>();
784         int channelSetIdx = 0;
785 
786         for (Set<Integer> channelSet : channelSets) {
787             Bucket splitBucket;
788             if (channelSetIdx == 0) {
789                 // Need to keep the original bucket to keep track of the settings to scheduled
790                 // bucket mapping.
791                 splitBucket = originalBucket;
792             } else {
793                 splitBucket = new Bucket(originalBucket);
794             }
795             ChannelCollection splitBucketChannelCollection = splitBucket.getChannelCollection();
796             splitBucketChannelCollection.clear();
797             for (Integer channel : channelSet) {
798                 splitBucketChannelCollection.addChannel(channel);
799             }
800             channelSetIdx++;
801             splitBucketList.add(splitBucket);
802         }
803         return splitBucketList;
804     }
805 
806     /**
807      * Check if any of the buckets don't fit into the bucket specification and fix it. This
808      * creates duplicate buckets to fit all the channels. So, the channels to be scanned
809      * will be split across 2 (or more) buckets.
810      * TODO: If we reach the max number of buckets, then this fix will be skipped!
811      */
fixBuckets(List<Bucket> originalBucketList, int maxBuckets, int maxChannelsPerBucket)812     private List<Bucket> fixBuckets(List<Bucket> originalBucketList, int maxBuckets,
813             int maxChannelsPerBucket) {
814         List<Bucket> fixedBucketList = new ArrayList<>();
815         int totalNumBuckets = originalBucketList.size();
816 
817         for (Bucket originalBucket : originalBucketList) {
818             ChannelCollection channelCollection = originalBucket.getChannelCollection();
819             Set<Integer> channelSet = channelCollection.getChannelSet();
820             if (channelSet.size() > maxChannelsPerBucket) {
821                 List<Set<Integer>> channelSetList =
822                         partitionChannelSet(channelSet, maxChannelsPerBucket);
823                 int newTotalNumBuckets = totalNumBuckets + channelSetList.size() - 1;
824                 if (newTotalNumBuckets <= maxBuckets) {
825                     List<Bucket> splitBuckets = createSplitBuckets(originalBucket, channelSetList);
826                     for (Bucket bucket : splitBuckets) {
827                         fixedBucketList.add(bucket);
828                     }
829                     totalNumBuckets = newTotalNumBuckets;
830                 } else {
831                     fixedBucketList.add(originalBucket);
832                 }
833             } else {
834                 fixedBucketList.add(originalBucket);
835             }
836         }
837         return fixedBucketList;
838     }
839 }
840