BackgroundScanScheduler.java revision 1b266ec1524c516a7ebf25b76801db86427ba68b
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.HashSet;
40import java.util.Iterator;
41import java.util.List;
42import java.util.ListIterator;
43import java.util.Map;
44import 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 */
61public 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
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         */
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         */
157        private WifiNative.ChannelSettings createChannelSettings(int frequency) {
158            WifiNative.ChannelSettings channelSettings = new WifiNative.ChannelSettings();
159            channelSettings.frequency = frequency;
160            return channelSettings;
161        }
162
163        public boolean addSettings(ScanSettings scanSettings) {
164            mChannelCollection.addChannels(scanSettings);
165            return mScanSettingsList.add(scanSettings);
166        }
167
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
179        public List<ScanSettings> getSettingsList() {
180            return mScanSettingsList;
181        }
182
183        public void updateChannelCollection() {
184            mChannelCollection.clear();
185            for (ScanSettings settings : mScanSettingsList) {
186                mChannelCollection.addChannels(settings);
187            }
188        }
189
190        public ChannelCollection getChannelCollection() {
191            return mChannelCollection;
192        }
193
194        /**
195         * convert the setting for this bucket to HAL representation
196         */
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
260        BucketList() {
261            mBuckets = new Bucket[PREDEFINED_BUCKET_PERIODS.length];
262        }
263
264        public void clearAll() {
265            Arrays.fill(mBuckets, null);
266            mActiveBucketCount = 0;
267        }
268
269        public void clear(int index) {
270            if (mBuckets[index] != null) {
271                --mActiveBucketCount;
272                mBuckets[index] = null;
273            }
274        }
275
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
285        public boolean isActive(int index) {
286            return mBuckets[index] != null;
287        }
288
289        public Bucket get(int index) {
290            return mBuckets[index];
291        }
292
293        public int size() {
294            return mBuckets.length;
295        }
296
297        public int getActiveCount() {
298            return mActiveBucketCount;
299        }
300
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         */
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
329    public int getMaxBuckets() {
330        return mMaxBuckets;
331    }
332
333    public void setMaxBuckets(int maxBuckets) {
334        mMaxBuckets = maxBuckets;
335    }
336
337    public int getMaxChannelsPerBucket() {
338        return mMaxChannelsPerBucket;
339    }
340
341    // TODO: find a way to get max channels
342    public void setMaxChannelsPerBucket(int maxChannels) {
343        mMaxChannelsPerBucket = maxChannels;
344    }
345
346    public int getMaxBatch() {
347        return mMaxBatch;
348    }
349
350    // TODO: find a way to get max batch size
351    public void setMaxBatch(int maxBatch) {
352        mMaxBatch = maxBatch;
353    }
354
355    public int getMaxApPerScan() {
356        return mMaxApPerScan;
357    }
358
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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