1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.wifi.scanner;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.net.wifi.ScanResult;
22import android.net.wifi.WifiScanner;
23import android.net.wifi.WifiScanner.ScanData;
24import android.net.wifi.WifiScanner.ScanSettings;
25import android.util.ArraySet;
26import android.util.Pair;
27import android.util.Rational;
28import android.util.Slog;
29
30import com.android.server.wifi.WifiNative;
31import com.android.server.wifi.scanner.ChannelHelper.ChannelCollection;
32
33import java.util.ArrayList;
34import java.util.Arrays;
35import java.util.Collection;
36import java.util.Collections;
37import java.util.Comparator;
38import java.util.HashMap;
39import java.util.Iterator;
40import java.util.List;
41import java.util.ListIterator;
42import java.util.Map;
43import 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 */
60public 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
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         */
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         */
156        private WifiNative.ChannelSettings createChannelSettings(int frequency) {
157            WifiNative.ChannelSettings channelSettings = new WifiNative.ChannelSettings();
158            channelSettings.frequency = frequency;
159            return channelSettings;
160        }
161
162        public boolean addSettings(ScanSettings scanSettings) {
163            mChannelCollection.addChannels(scanSettings);
164            return mScanSettingsList.add(scanSettings);
165        }
166
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
178        public List<ScanSettings> getSettingsList() {
179            return mScanSettingsList;
180        }
181
182        public void updateChannelCollection() {
183            mChannelCollection.clear();
184            for (ScanSettings settings : mScanSettingsList) {
185                mChannelCollection.addChannels(settings);
186            }
187        }
188
189        public ChannelCollection getChannelCollection() {
190            return mChannelCollection;
191        }
192
193        /**
194         * convert the setting for this bucket to HAL representation
195         */
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
259        BucketList() {
260            mBuckets = new Bucket[PREDEFINED_BUCKET_PERIODS.length];
261        }
262
263        public void clearAll() {
264            Arrays.fill(mBuckets, null);
265            mActiveBucketCount = 0;
266        }
267
268        public void clear(int index) {
269            if (mBuckets[index] != null) {
270                --mActiveBucketCount;
271                mBuckets[index] = null;
272            }
273        }
274
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
284        public boolean isActive(int index) {
285            return mBuckets[index] != null;
286        }
287
288        public Bucket get(int index) {
289            return mBuckets[index];
290        }
291
292        public int size() {
293            return mBuckets.length;
294        }
295
296        public int getActiveCount() {
297            return mActiveBucketCount;
298        }
299
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         */
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
328    public int getMaxBuckets() {
329        return mMaxBuckets;
330    }
331
332    public void setMaxBuckets(int maxBuckets) {
333        mMaxBuckets = maxBuckets;
334    }
335
336    public int getMaxChannelsPerBucket() {
337        return mMaxChannelsPerBucket;
338    }
339
340    // TODO: find a way to get max channels
341    public void setMaxChannelsPerBucket(int maxChannels) {
342        mMaxChannelsPerBucket = maxChannels;
343    }
344
345    public int getMaxBatch() {
346        return mMaxBatch;
347    }
348
349    // TODO: find a way to get max batch size
350    public void setMaxBatch(int maxBatch) {
351        mMaxBatch = maxBatch;
352    }
353
354    public int getMaxApPerScan() {
355        return mMaxApPerScan;
356    }
357
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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