summaryrefslogtreecommitdiffstats
path: root/service/java/com/android/server/wifi/scanner/BackgroundScanScheduler.java
blob: 01d64e9f5132efe0e52338f34a7367442ca3992a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.wifi.scanner;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiScanner;
import android.net.wifi.WifiScanner.ScanData;
import android.net.wifi.WifiScanner.ScanSettings;
import android.util.ArraySet;
import android.util.Pair;
import android.util.Rational;
import android.util.Slog;

import com.android.server.wifi.WifiNative;
import com.android.server.wifi.scanner.ChannelHelper.ChannelCollection;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

/**
 * <p>This class takes a series of scan requests and formulates the best hardware level scanning
 * schedule it can to try and satisfy requests. The hardware level accepts a series of buckets,
 * where each bucket represents a set of channels and an interval to scan at. This
 * scheduler operates as follows:</p>
 *
 * <p>Each new request is placed in the best predefined bucket. Once all requests have been added
 * the last buckets (lower priority) are placed in the next best bucket until the number of buckets
 * is less than the number supported by the hardware.
 *
 * <p>Finally, the scheduler creates a WifiNative.ScanSettings from the list of buckets which may be
 * passed through the Wifi HAL.</p>
 *
 * <p>This class is not thread safe.</p>
 */
public class BackgroundScanScheduler {

    private static final String TAG = "BackgroundScanScheduler";
    private static final boolean DBG = false;

    public static final int DEFAULT_MAX_BUCKETS = 8;
    // Max channels that can be specified per bucket
    public static final int DEFAULT_MAX_CHANNELS_PER_BUCKET = 16;
    // anecdotally, some chipsets will fail without explanation with a higher batch size, and
    // there is apparently no way to retrieve the maximum batch size
    public static final int DEFAULT_MAX_SCANS_TO_BATCH = 10;
    public static final int DEFAULT_MAX_AP_PER_SCAN = 32;

    /**
     * Value that all scan periods must be an integer multiple of
     */
    private static final int PERIOD_MIN_GCD_MS = 10000;
    /**
     * Default period to use if no buckets are being scheduled
     */
    private static final int DEFAULT_PERIOD_MS = 30000;
    /**
     * Scan report threshold percentage to assign to the schedule by default
     * @see com.android.server.wifi.WifiNative.ScanSettings#report_threshold_percent
     */
    private static final int DEFAULT_REPORT_THRESHOLD_PERCENTAGE = 100;

    /**
     * List of predefined periods (in ms) that buckets can be scheduled at. Ordered by preference
     * if there are not enough buckets for all periods. All periods MUST be an integer multiple of
     * the next smallest bucket with the smallest bucket having a period of PERIOD_MIN_GCD_MS.
     * This requirement allows scans to be scheduled more efficiently because scan requests with
     * intersecting channels will result in those channels being scanned exactly once at the smaller
     * period and no unnecessary scan being scheduled. If this was not the case and two requests
     * had channel 5 with periods of 15 seconds and 25 seconds then channel 5 would be scanned
     * 296  (3600/15 + 3600/25 - 3500/75) times an hour instead of 240 times an hour (3600/15) if
     * the 25s scan is rescheduled at 30s. This is less important with higher periods as it has
     * significantly less impact. Ranking could be done by favoring shorter or longer; however,
     * this would result in straying further from the requested period and possibly power
     * implications if the scan is scheduled at a significantly lower period.
     *
     * For example if the hardware only supports 2 buckets and scans are requested with periods of
     * 40s, 20s and 10s then the two buckets scheduled will have periods 40s and 20s and the 10s
     * scan will be placed in the 20s bucket.
     *
     * If there are special scan requests such as exponential back off, we always dedicate a bucket
     * for each type. Regular scan requests will be packed into the remaining buckets.
     */
    private static final int[] PREDEFINED_BUCKET_PERIODS = {
        3 * PERIOD_MIN_GCD_MS,   // 30s
        12 * PERIOD_MIN_GCD_MS,  // 120s
        48 * PERIOD_MIN_GCD_MS,  // 480s
        1 * PERIOD_MIN_GCD_MS,   // 10s
        6 * PERIOD_MIN_GCD_MS,   // 60s
        192 * PERIOD_MIN_GCD_MS, // 1920s
        24 * PERIOD_MIN_GCD_MS,  // 240s
        96 * PERIOD_MIN_GCD_MS,  // 960s
        384 * PERIOD_MIN_GCD_MS, // 3840s
        -1,                      // place holder for exponential back off scan
    };

    private static final int EXPONENTIAL_BACK_OFF_BUCKET_IDX =
            (PREDEFINED_BUCKET_PERIODS.length - 1);
    private static final int NUM_OF_REGULAR_BUCKETS =
            (PREDEFINED_BUCKET_PERIODS.length - 1);

    /**
     * This class is an intermediate representation for scheduling. This maintins the channel
     * collection to be scanned by the bucket as settings are added to it.
     */
    private class Bucket {
        public int period;
        public int bucketId;
        private final List<ScanSettings> mScanSettingsList = new ArrayList<>();
        private final ChannelCollection mChannelCollection;

        Bucket(int period) {
            this.period = period;
            this.bucketId = 0;
            mScanSettingsList.clear();
            mChannelCollection = mChannelHelper.createChannelCollection();
        }

        /**
         * Copy constructor which populates the settings list from the original bucket object.
         */
        Bucket(Bucket originalBucket) {
            this(originalBucket.period);
            for (ScanSettings settings : originalBucket.getSettingsList()) {
                mScanSettingsList.add(settings);
            }
        }

        /**
         * convert ChannelSpec to native representation
         */
        private WifiNative.ChannelSettings createChannelSettings(int frequency) {
            WifiNative.ChannelSettings channelSettings = new WifiNative.ChannelSettings();
            channelSettings.frequency = frequency;
            return channelSettings;
        }

        public boolean addSettings(ScanSettings scanSettings) {
            mChannelCollection.addChannels(scanSettings);
            return mScanSettingsList.add(scanSettings);
        }

        public boolean removeSettings(ScanSettings scanSettings) {
            if (mScanSettingsList.remove(scanSettings)) {
                // It's difficult to handle settings removal from buckets in terms of
                // maintaining the correct channel collection, so recreate the channel
                // collection from the remaining elements.
                updateChannelCollection();
                return true;
            }
            return false;
        }

        public List<ScanSettings> getSettingsList() {
            return mScanSettingsList;
        }

        public void updateChannelCollection() {
            mChannelCollection.clear();
            for (ScanSettings settings : mScanSettingsList) {
                mChannelCollection.addChannels(settings);
            }
        }

        public ChannelCollection getChannelCollection() {
            return mChannelCollection;
        }

        /**
         * convert the setting for this bucket to HAL representation
         */
        public WifiNative.BucketSettings createBucketSettings(int bucketId, int maxChannels) {
            this.bucketId = bucketId;
            int reportEvents = WifiScanner.REPORT_EVENT_NO_BATCH;
            int maxPeriodInMs = 0;
            int stepCount = 0;
            int bucketIndex = 0;

            for (int i = 0; i < mScanSettingsList.size(); ++i) {
                WifiScanner.ScanSettings setting = mScanSettingsList.get(i);
                int requestedReportEvents = setting.reportEvents;
                if ((requestedReportEvents & WifiScanner.REPORT_EVENT_NO_BATCH) == 0) {
                    reportEvents &= ~WifiScanner.REPORT_EVENT_NO_BATCH;
                }
                if ((requestedReportEvents & WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN) != 0) {
                    reportEvents |= WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN;
                }
                if ((requestedReportEvents & WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT) != 0) {
                    reportEvents |= WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT;
                }
                // For the bucket allocated to exponential back off scan, the values of
                // the exponential back off scan related parameters from the very first
                // setting in the settings list will be used to configure this bucket.
                //
                if (i == 0 && setting.maxPeriodInMs != 0
                        && setting.maxPeriodInMs != setting.periodInMs) {
                    // Align the starting period with one of the pre-defined regular
                    // scan periods. This will optimize the scan schedule when it has
                    // both exponential back off scan and regular scan(s).
                    bucketIndex = findBestRegularBucketIndex(setting.periodInMs,
                                                     NUM_OF_REGULAR_BUCKETS);
                    period = PREDEFINED_BUCKET_PERIODS[bucketIndex];
                    maxPeriodInMs = (setting.maxPeriodInMs < period)
                                    ? period
                                    : setting.maxPeriodInMs;
                    stepCount = setting.stepCount;
                }
            }

            WifiNative.BucketSettings bucketSettings = new WifiNative.BucketSettings();
            bucketSettings.bucket = bucketId;
            bucketSettings.report_events = reportEvents;
            bucketSettings.period_ms = period;
            bucketSettings.max_period_ms = maxPeriodInMs;
            bucketSettings.step_count = stepCount;
            mChannelCollection.fillBucketSettings(bucketSettings, maxChannels);
            return bucketSettings;
        }
    }

    /**
     * Maintains a list of buckets and the number that are active (non-null)
     */
    private class BucketList {
        // Comparator to sort the buckets in order of increasing time periods
        private final Comparator<Bucket> mTimePeriodSortComparator =
                new Comparator<Bucket>() {
                    public int compare(Bucket b1, Bucket b2) {
                        return b1.period - b2.period;
                    }
                };
        private final Bucket[] mBuckets;
        private int mActiveBucketCount = 0;

        BucketList() {
            mBuckets = new Bucket[PREDEFINED_BUCKET_PERIODS.length];
        }

        public void clearAll() {
            Arrays.fill(mBuckets, null);
            mActiveBucketCount = 0;
        }

        public void clear(int index) {
            if (mBuckets[index] != null) {
                --mActiveBucketCount;
                mBuckets[index] = null;
            }
        }

        public Bucket getOrCreate(int index) {
            Bucket bucket = mBuckets[index];
            if (bucket == null) {
                ++mActiveBucketCount;
                bucket = mBuckets[index] = new Bucket(PREDEFINED_BUCKET_PERIODS[index]);
            }
            return bucket;
        }

        public boolean isActive(int index) {
            return mBuckets[index] != null;
        }

        public Bucket get(int index) {
            return mBuckets[index];
        }

        public int size() {
            return mBuckets.length;
        }

        public int getActiveCount() {
            return mActiveBucketCount;
        }

        public int getActiveRegularBucketCount() {
            if (isActive(EXPONENTIAL_BACK_OFF_BUCKET_IDX)) {
                return mActiveBucketCount - 1;
            } else {
                return mActiveBucketCount;
            }
        }

        /**
         * Returns the active regular buckets sorted by their increasing time periods.
         */
        public List<Bucket> getSortedActiveRegularBucketList() {
            ArrayList<Bucket> activeBuckets = new ArrayList<>();
            for (int i = 0; i < mBuckets.length; i++) {
                if (mBuckets[i] != null && i != EXPONENTIAL_BACK_OFF_BUCKET_IDX) {
                    activeBuckets.add(mBuckets[i]);
                }
            }
            Collections.sort(activeBuckets, mTimePeriodSortComparator);
            return activeBuckets;
        }
    }

    private int mMaxBuckets = DEFAULT_MAX_BUCKETS;
    private int mMaxChannelsPerBucket = DEFAULT_MAX_CHANNELS_PER_BUCKET;
    private int mMaxBatch = DEFAULT_MAX_SCANS_TO_BATCH;
    private int mMaxApPerScan = DEFAULT_MAX_AP_PER_SCAN;

    public int getMaxBuckets() {
        return mMaxBuckets;
    }

    public void setMaxBuckets(int maxBuckets) {
        mMaxBuckets = maxBuckets;
    }

    public int getMaxChannelsPerBucket() {
        return mMaxChannelsPerBucket;
    }

    // TODO: find a way to get max channels
    public void setMaxChannelsPerBucket(int maxChannels) {
        mMaxChannelsPerBucket = maxChannels;
    }

    public int getMaxBatch() {
        return mMaxBatch;
    }

    // TODO: find a way to get max batch size
    public void setMaxBatch(int maxBatch) {
        mMaxBatch = maxBatch;
    }

    public int getMaxApPerScan() {
        return mMaxApPerScan;
    }

    public void setMaxApPerScan(int maxApPerScan) {
        mMaxApPerScan = maxApPerScan;
    }

    private final BucketList mBuckets = new BucketList();
    private final ChannelHelper mChannelHelper;
    private WifiNative.ScanSettings mSchedule;
    // This keeps track of the settings to the max time period bucket to which it was scheduled.
    private final Map<ScanSettings, Bucket> mSettingsToScheduledBucket = new HashMap<>();

    public BackgroundScanScheduler(ChannelHelper channelHelper) {
        mChannelHelper = channelHelper;
        createSchedule(new ArrayList<Bucket>(), getMaxChannelsPerBucket());
    }

    /**
     * Updates the schedule from the given set of requests.
     */
    public void updateSchedule(@NonNull Collection<ScanSettings> requests) {
        // create initial schedule
        mBuckets.clearAll();
        for (ScanSettings request : requests) {
            addScanToBuckets(request);
        }

        compactBuckets(getMaxBuckets());

        List<Bucket> bucketList = optimizeBuckets();

        List<Bucket> fixedBucketList =
                fixBuckets(bucketList, getMaxBuckets(), getMaxChannelsPerBucket());

        createSchedule(fixedBucketList, getMaxChannelsPerBucket());
    }

    /**
     * Retrieves the current scanning schedule.
     */
    public @NonNull WifiNative.ScanSettings getSchedule() {
        return mSchedule;
    }

    /**
     * Returns true if the given scan result should be reported to a listener with the given
     * settings.
     */
    public boolean shouldReportFullScanResultForSettings(@NonNull ScanResult result,
            int bucketsScanned, @NonNull ScanSettings settings) {
        return ScanScheduleUtil.shouldReportFullScanResultForSettings(mChannelHelper,
                result, bucketsScanned, settings, getScheduledBucket(settings));
    }

    /**
     * Returns a filtered version of the scan results from the chip that represents only the data
     * requested in the settings. Will return null if the result should not be reported.
     */
    public @Nullable ScanData[] filterResultsForSettings(@NonNull ScanData[] scanDatas,
            @NonNull ScanSettings settings) {
        return ScanScheduleUtil.filterResultsForSettings(mChannelHelper, scanDatas, settings,
                getScheduledBucket(settings));
    }

    /**
     * Retrieves the max time period bucket idx at which this setting was scheduled
     */
    public int getScheduledBucket(ScanSettings settings) {
        Bucket maxScheduledBucket = mSettingsToScheduledBucket.get(settings);
        if (maxScheduledBucket != null) {
            return maxScheduledBucket.bucketId;
        } else {
            Slog.wtf(TAG, "No bucket found for settings");
            return -1;
        }
    }

    /**
     * creates a schedule for the current buckets
     */
    private void createSchedule(List<Bucket> bucketList, int maxChannelsPerBucket) {
        WifiNative.ScanSettings schedule = new WifiNative.ScanSettings();
        schedule.num_buckets = bucketList.size();
        schedule.buckets = new WifiNative.BucketSettings[bucketList.size()];

        schedule.max_ap_per_scan = 0;
        schedule.report_threshold_num_scans = getMaxBatch();

        // set all buckets in schedule
        int bucketId = 0;
        for (Bucket bucket : bucketList) {
            schedule.buckets[bucketId] =
                    bucket.createBucketSettings(bucketId, maxChannelsPerBucket);
            for (ScanSettings settings : bucket.getSettingsList()) {
                // set APs per scan
                if (settings.numBssidsPerScan > schedule.max_ap_per_scan) {
                    schedule.max_ap_per_scan = settings.numBssidsPerScan;
                }
                // set batching
                if (settings.maxScansToCache != 0
                        && settings.maxScansToCache < schedule.report_threshold_num_scans) {
                    schedule.report_threshold_num_scans = settings.maxScansToCache;
                }
            }
            bucketId++;
        }

        schedule.report_threshold_percent = DEFAULT_REPORT_THRESHOLD_PERCENTAGE;

        if (schedule.max_ap_per_scan == 0 || schedule.max_ap_per_scan > getMaxApPerScan()) {
            schedule.max_ap_per_scan = getMaxApPerScan();
        }

        // update base period as gcd of periods
        if (schedule.num_buckets > 0) {
            int gcd = schedule.buckets[0].period_ms;
            for (int b = 1; b < schedule.num_buckets; b++) {
                gcd = Rational.gcd(schedule.buckets[b].period_ms, gcd);
            }

            if (gcd < PERIOD_MIN_GCD_MS) {
                Slog.wtf(TAG, "found gcd less than min gcd");
                gcd = PERIOD_MIN_GCD_MS;
            }

            schedule.base_period_ms = gcd;
        } else {
            schedule.base_period_ms = DEFAULT_PERIOD_MS;
        }

        mSchedule = schedule;
    }

    /**
     * Add a scan to the most appropriate bucket, creating the bucket if necessary.
     */
    private void addScanToBuckets(ScanSettings settings) {
        int bucketIndex;

        if (settings.maxPeriodInMs != 0 && settings.maxPeriodInMs != settings.periodInMs) {
            // exponential back off scan has a dedicated bucket
            bucketIndex = EXPONENTIAL_BACK_OFF_BUCKET_IDX;
        } else {
            bucketIndex = findBestRegularBucketIndex(settings.periodInMs, NUM_OF_REGULAR_BUCKETS);
        }

        mBuckets.getOrCreate(bucketIndex).addSettings(settings);
    }

    /**
     * find closest bucket period to the requested period in all predefined buckets
     */
    private static int findBestRegularBucketIndex(int requestedPeriod, int maxNumBuckets) {
        maxNumBuckets = Math.min(maxNumBuckets, NUM_OF_REGULAR_BUCKETS);
        int index = -1;
        int minDiff = Integer.MAX_VALUE;
        for (int i = 0; i < maxNumBuckets; ++i) {
            int diff = Math.abs(PREDEFINED_BUCKET_PERIODS[i] - requestedPeriod);
            if (diff < minDiff) {
                minDiff = diff;
                index = i;
            }
        }
        if (index == -1) {
            Slog.wtf(TAG, "Could not find best bucket for period " + requestedPeriod + " in "
                     + maxNumBuckets + " buckets");
        }
        return index;
    }

    /**
     * Reduce the number of required buckets by reassigning lower priority buckets to the next
     * closest period bucket.
     */
    private void compactBuckets(int maxBuckets) {
        int maxRegularBuckets = maxBuckets;

        // reserve one bucket for exponential back off scan if there is
        // such request(s)
        if (mBuckets.isActive(EXPONENTIAL_BACK_OFF_BUCKET_IDX)) {
            maxRegularBuckets--;
        }
        for (int i = NUM_OF_REGULAR_BUCKETS - 1;
                i >= 0 && mBuckets.getActiveRegularBucketCount() > maxRegularBuckets; --i) {
            if (mBuckets.isActive(i)) {
                for (ScanSettings scanRequest : mBuckets.get(i).getSettingsList()) {
                    int newBucketIndex = findBestRegularBucketIndex(scanRequest.periodInMs, i);
                    mBuckets.getOrCreate(newBucketIndex).addSettings(scanRequest);
                }
                mBuckets.clear(i);
            }
        }
    }

    /**
     * Clone the provided scan settings fields to a new ScanSettings object.
     */
    private ScanSettings cloneScanSettings(ScanSettings originalSettings) {
        ScanSettings settings = new ScanSettings();
        settings.band = originalSettings.band;
        settings.channels = originalSettings.channels;
        settings.periodInMs = originalSettings.periodInMs;
        settings.reportEvents = originalSettings.reportEvents;
        settings.numBssidsPerScan = originalSettings.numBssidsPerScan;
        settings.maxScansToCache = originalSettings.maxScansToCache;
        settings.maxPeriodInMs = originalSettings.maxPeriodInMs;
        settings.stepCount = originalSettings.stepCount;
        settings.isPnoScan = originalSettings.isPnoScan;
        return settings;
    }

    /**
     * Creates a split scan setting that needs to be added back to the current bucket.
     */
    private ScanSettings createCurrentBucketSplitSettings(ScanSettings originalSettings,
            Set<Integer> currentBucketChannels) {
        ScanSettings currentBucketSettings = cloneScanSettings(originalSettings);
        // Let's create a new settings for the current bucket with the same flags, but the missing
        // channels from the other bucket
        currentBucketSettings.band = WifiScanner.WIFI_BAND_UNSPECIFIED;
        currentBucketSettings.channels = new WifiScanner.ChannelSpec[currentBucketChannels.size()];
        int chanIdx = 0;
        for (Integer channel : currentBucketChannels) {
            currentBucketSettings.channels[chanIdx++] = new WifiScanner.ChannelSpec(channel);
        }
        return currentBucketSettings;
    }

    /**
     * Creates a split scan setting that needs to be added to the target lower time period bucket.
     * The reportEvents field is modified to remove REPORT_EVENT_AFTER_EACH_SCAN because we
     * need this flag only in the higher time period bucket.
     */
    private ScanSettings createTargetBucketSplitSettings(ScanSettings originalSettings,
            Set<Integer> targetBucketChannels) {
        ScanSettings targetBucketSettings = cloneScanSettings(originalSettings);
        // The new settings for the other bucket will have the channels that already in the that
        // bucket. We'll need to do some migration of the |reportEvents| flags.
        targetBucketSettings.band = WifiScanner.WIFI_BAND_UNSPECIFIED;
        targetBucketSettings.channels = new WifiScanner.ChannelSpec[targetBucketChannels.size()];
        int chanIdx = 0;
        for (Integer channel : targetBucketChannels) {
            targetBucketSettings.channels[chanIdx++] = new WifiScanner.ChannelSpec(channel);
        }
        targetBucketSettings.reportEvents =
                originalSettings.reportEvents
                        & (WifiScanner.REPORT_EVENT_NO_BATCH
                                | WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT);
        return targetBucketSettings;
    }

    /**
     * Split the scan settings into 2 so that they can be put into 2 separate buckets.
     * @return The first scan setting needs to be added back to the current bucket
     *         The second scan setting needs to be added to the other bucket
     */
    private Pair<ScanSettings, ScanSettings> createSplitSettings(ScanSettings originalSettings,
            ChannelCollection targetBucketChannelCol) {
        Set<Integer> currentBucketChannels =
                targetBucketChannelCol.getMissingChannelsFromSettings(originalSettings);
        Set<Integer> targetBucketChannels =
                targetBucketChannelCol.getContainingChannelsFromSettings(originalSettings);
        // Two Copy of the original settings
        ScanSettings currentBucketSettings =
                createCurrentBucketSplitSettings(originalSettings, currentBucketChannels);
        ScanSettings targetBucketSettings =
                createTargetBucketSplitSettings(originalSettings, targetBucketChannels);
        return Pair.create(currentBucketSettings, targetBucketSettings);
    }

    /**
     * Try to merge the settings to lower buckets.
     * Check if the channels in this settings is already covered by a lower time period
     * bucket. If it's partially covered, the settings is split else the entire settings
     * is moved to the lower time period bucket.
     * This method updates the |mSettingsToScheduledBucket| mapping.
     * @return Pair<wasMerged, remainingSplitSettings>
     *         wasMerged -  boolean indicating whether the original setting was merged to lower time
     *                      period buckets.
     *         remainingSplitSettings - Partial Scan Settings that need to be added back to the
     *                                  current bucket.
     */
    private Pair<Boolean, ScanSettings> mergeSettingsToLowerBuckets(ScanSettings originalSettings,
            Bucket currentBucket, ListIterator<Bucket> iterTargetBuckets) {
        ScanSettings remainingSplitSettings = null;
        boolean wasMerged = false;
        Bucket maxScheduledBucket = currentBucket;

        while (iterTargetBuckets.hasPrevious()) {
            Bucket targetBucket = iterTargetBuckets.previous();
            ChannelCollection targetBucketChannelCol = targetBucket.getChannelCollection();
            if (targetBucketChannelCol.containsSettings(originalSettings)) {
                targetBucket.addSettings(originalSettings);
                // Update the max scheduled bucket for this setting
                maxScheduledBucket = targetBucket;
                wasMerged = true;
            } else if (targetBucketChannelCol.partiallyContainsSettings(originalSettings)) {
                Pair<ScanSettings, ScanSettings> splitSettings;
                if (remainingSplitSettings == null) {
                    splitSettings = createSplitSettings(originalSettings, targetBucketChannelCol);
                } else {
                    splitSettings =
                            createSplitSettings(remainingSplitSettings, targetBucketChannelCol);
                }
                targetBucket.addSettings(splitSettings.second);
                // Update the |remainingSplitSettings| to keep track of the remaining scan settings.
                // The original settings could be split across multiple buckets.
                remainingSplitSettings = splitSettings.first;
                wasMerged = true;
            }
        }
        // Update the settings to scheduled bucket mapping. This is needed for event
        // reporting lookup
        mSettingsToScheduledBucket.put(originalSettings, maxScheduledBucket);

        return Pair.create(wasMerged, remainingSplitSettings);
    }

    /**
     * Optimize all the active buckets by removing duplicate channels in the buckets.
     * This method tries to go through the settings in all the buckets and checks if the same
     * channels for the setting is already being scanned by another bucked with lower time period.
     * If yes, move the setting to the lower time period bucket. If all the settings from a higher
     * period has been moved out, that bucket can be removed.
     *
     * We're trying to avoid cases where we have the same channels being scanned in different
     * buckets. This is to workaround the fact that the HAL implementations have a max number of
     * cumulative channel across buckets (b/28022609).
     */
    private List<Bucket> optimizeBuckets() {
        mSettingsToScheduledBucket.clear();
        List<Bucket> sortedBuckets = mBuckets.getSortedActiveRegularBucketList();
        ListIterator<Bucket> iterBuckets = sortedBuckets.listIterator();
        // This is needed to keep track of split settings that need to be added back to the same
        // bucket at the end of iterating thru all the settings. This has to be a separate temp list
        // to prevent concurrent modification exceptions during iterations.
        List<ScanSettings> currentBucketSplitSettingsList = new ArrayList<>();

        // We need to go thru each setting starting from the lowest time period bucket and check
        // if they're already contained in a lower time period bucket. If yes, delete the setting
        // from the current bucket and move it to the other bucket. If the settings are only
        // partially contained, split the settings into two and move the partial bucket back
        // to the same bucket. Finally, if all the settings have been moved out, remove the current
        // bucket altogether.
        while (iterBuckets.hasNext()) {
            Bucket currentBucket = iterBuckets.next();
            Iterator<ScanSettings> iterSettings = currentBucket.getSettingsList().iterator();

            currentBucketSplitSettingsList.clear();

            while (iterSettings.hasNext()) {
                ScanSettings currentSettings = iterSettings.next();
                ListIterator<Bucket> iterTargetBuckets =
                        sortedBuckets.listIterator(iterBuckets.previousIndex());

                Pair<Boolean, ScanSettings> mergeResult =
                        mergeSettingsToLowerBuckets(
                                currentSettings, currentBucket, iterTargetBuckets);

                boolean wasMerged = mergeResult.first.booleanValue();
                if (wasMerged) {
                    // Remove the original settings from the current bucket.
                    iterSettings.remove();
                    ScanSettings remainingSplitSettings = mergeResult.second;
                    if (remainingSplitSettings != null) {
                        // Add back the remaining split settings to the current bucket.
                        currentBucketSplitSettingsList.add(remainingSplitSettings);
                    }
                }
            }

            for (ScanSettings splitSettings: currentBucketSplitSettingsList) {
                currentBucket.addSettings(splitSettings);
            }
            if (currentBucket.getSettingsList().isEmpty()) {
                iterBuckets.remove();
            } else {
                // Update the channel collection to account for the removed settings
                currentBucket.updateChannelCollection();
            }
        }

        // Update the settings to scheduled bucket map for all exponential scans.
        if (mBuckets.isActive(EXPONENTIAL_BACK_OFF_BUCKET_IDX)) {
            Bucket exponentialBucket = mBuckets.get(EXPONENTIAL_BACK_OFF_BUCKET_IDX);
            for (ScanSettings settings : exponentialBucket.getSettingsList()) {
                mSettingsToScheduledBucket.put(settings, exponentialBucket);
            }
            sortedBuckets.add(exponentialBucket);
        }

        return sortedBuckets;
    }

    /**
     * Partition the channel set into 2 or more based on the max channels that can be specified for
     * each bucket.
     */
    private List<Set<Integer>> partitionChannelSet(Set<Integer> originalChannelSet,
            int maxChannelsPerBucket) {
        ArrayList<Set<Integer>> channelSetList = new ArrayList();
        ArraySet<Integer> channelSet = new ArraySet<>();
        Iterator<Integer> iterChannels = originalChannelSet.iterator();

        while (iterChannels.hasNext()) {
            channelSet.add(iterChannels.next());
            if (channelSet.size() == maxChannelsPerBucket) {
                channelSetList.add(channelSet);
                channelSet = new ArraySet<>();
            }
        }
        // Add the last partial set if any
        if (!channelSet.isEmpty()) {
            channelSetList.add(channelSet);
        }
        return channelSetList;
    }

    /**
     * Creates a list of split buckets with the channel collection corrected to fit the
     * max channel list size that can be specified. The original channel collection will be split
     * into multiple buckets with the same scan settings.
     * Note: This does not update the mSettingsToScheduledBucket map because this bucket is
     * essentially a copy of the original bucket, so it should not affect the event reporting.
     * This bucket results will come back the same time the original bucket results come back.
     */
    private List<Bucket> createSplitBuckets(Bucket originalBucket, List<Set<Integer>> channelSets) {
        List<Bucket> splitBucketList = new ArrayList<>();
        int channelSetIdx = 0;

        for (Set<Integer> channelSet : channelSets) {
            Bucket splitBucket;
            if (channelSetIdx == 0) {
                // Need to keep the original bucket to keep track of the settings to scheduled
                // bucket mapping.
                splitBucket = originalBucket;
            } else {
                splitBucket = new Bucket(originalBucket);
            }
            ChannelCollection splitBucketChannelCollection = splitBucket.getChannelCollection();
            splitBucketChannelCollection.clear();
            for (Integer channel : channelSet) {
                splitBucketChannelCollection.addChannel(channel);
            }
            channelSetIdx++;
            splitBucketList.add(splitBucket);
        }
        return splitBucketList;
    }

    /**
     * Check if any of the buckets don't fit into the bucket specification and fix it. This
     * creates duplicate buckets to fit all the channels. So, the channels to be scanned
     * will be split across 2 (or more) buckets.
     * TODO: If we reach the max number of buckets, then this fix will be skipped!
     */
    private List<Bucket> fixBuckets(List<Bucket> originalBucketList, int maxBuckets,
            int maxChannelsPerBucket) {
        List<Bucket> fixedBucketList = new ArrayList<>();
        int totalNumBuckets = originalBucketList.size();

        for (Bucket originalBucket : originalBucketList) {
            ChannelCollection channelCollection = originalBucket.getChannelCollection();
            Set<Integer> channelSet = channelCollection.getChannelSet();
            if (channelSet.size() > maxChannelsPerBucket) {
                List<Set<Integer>> channelSetList =
                        partitionChannelSet(channelSet, maxChannelsPerBucket);
                int newTotalNumBuckets = totalNumBuckets + channelSetList.size() - 1;
                if (newTotalNumBuckets <= maxBuckets) {
                    List<Bucket> splitBuckets = createSplitBuckets(originalBucket, channelSetList);
                    for (Bucket bucket : splitBuckets) {
                        fixedBucketList.add(bucket);
                    }
                    totalNumBuckets = newTotalNumBuckets;
                } else {
                    fixedBucketList.add(originalBucket);
                }
            } else {
                fixedBucketList.add(originalBucket);
            }
        }
        return fixedBucketList;
    }
}