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