Home | History | Annotate | Download | only in scanner
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.server.wifi.scanner;
     18 
     19 import android.annotation.NonNull;
     20 import android.annotation.Nullable;
     21 import android.net.wifi.ScanResult;
     22 import android.net.wifi.WifiScanner;
     23 import android.net.wifi.WifiScanner.ScanData;
     24 import android.net.wifi.WifiScanner.ScanSettings;
     25 import android.util.ArraySet;
     26 import android.util.Pair;
     27 import android.util.Rational;
     28 import android.util.Slog;
     29 
     30 import com.android.server.wifi.WifiNative;
     31 import com.android.server.wifi.scanner.ChannelHelper.ChannelCollection;
     32 
     33 import java.util.ArrayList;
     34 import java.util.Arrays;
     35 import java.util.Collection;
     36 import java.util.Collections;
     37 import java.util.Comparator;
     38 import java.util.HashMap;
     39 import java.util.HashSet;
     40 import java.util.Iterator;
     41 import java.util.List;
     42 import java.util.ListIterator;
     43 import java.util.Map;
     44 import java.util.Set;
     45 
     46 /**
     47  * <p>This class takes a series of scan requests and formulates the best hardware level scanning
     48  * schedule it can to try and satisfy requests. The hardware level accepts a series of buckets,
     49  * where each bucket represents a set of channels and an interval to scan at. This
     50  * scheduler operates as follows:</p>
     51  *
     52  * <p>Each new request is placed in the best predefined bucket. Once all requests have been added
     53  * the last buckets (lower priority) are placed in the next best bucket until the number of buckets
     54  * is less than the number supported by the hardware.
     55  *
     56  * <p>Finally, the scheduler creates a WifiNative.ScanSettings from the list of buckets which may be
     57  * passed through the Wifi HAL.</p>
     58  *
     59  * <p>This class is not thread safe.</p>
     60  */
     61 public class BackgroundScanScheduler {
     62 
     63     private static final String TAG = "BackgroundScanScheduler";
     64     private static final boolean DBG = false;
     65 
     66     public static final int DEFAULT_MAX_BUCKETS = 8;
     67     // Max channels that can be specified per bucket
     68     public static final int DEFAULT_MAX_CHANNELS_PER_BUCKET = 16;
     69     // anecdotally, some chipsets will fail without explanation with a higher batch size, and
     70     // there is apparently no way to retrieve the maximum batch size
     71     public static final int DEFAULT_MAX_SCANS_TO_BATCH = 10;
     72     public static final int DEFAULT_MAX_AP_PER_SCAN = 32;
     73 
     74     /**
     75      * Value that all scan periods must be an integer multiple of
     76      */
     77     private static final int PERIOD_MIN_GCD_MS = 10000;
     78     /**
     79      * Default period to use if no buckets are being scheduled
     80      */
     81     private static final int DEFAULT_PERIOD_MS = 30000;
     82     /**
     83      * Scan report threshold percentage to assign to the schedule by default
     84      * @see com.android.server.wifi.WifiNative.ScanSettings#report_threshold_percent
     85      */
     86     private static final int DEFAULT_REPORT_THRESHOLD_PERCENTAGE = 100;
     87 
     88     /**
     89      * List of predefined periods (in ms) that buckets can be scheduled at. Ordered by preference
     90      * if there are not enough buckets for all periods. All periods MUST be an integer multiple of
     91      * the next smallest bucket with the smallest bucket having a period of PERIOD_MIN_GCD_MS.
     92      * This requirement allows scans to be scheduled more efficiently because scan requests with
     93      * intersecting channels will result in those channels being scanned exactly once at the smaller
     94      * period and no unnecessary scan being scheduled. If this was not the case and two requests
     95      * had channel 5 with periods of 15 seconds and 25 seconds then channel 5 would be scanned
     96      * 296  (3600/15 + 3600/25 - 3500/75) times an hour instead of 240 times an hour (3600/15) if
     97      * the 25s scan is rescheduled at 30s. This is less important with higher periods as it has
     98      * significantly less impact. Ranking could be done by favoring shorter or longer; however,
     99      * this would result in straying further from the requested period and possibly power
    100      * implications if the scan is scheduled at a significantly lower period.
    101      *
    102      * For example if the hardware only supports 2 buckets and scans are requested with periods of
    103      * 40s, 20s and 10s then the two buckets scheduled will have periods 40s and 20s and the 10s
    104      * scan will be placed in the 20s bucket.
    105      *
    106      * If there are special scan requests such as exponential back off, we always dedicate a bucket
    107      * for each type. Regular scan requests will be packed into the remaining buckets.
    108      */
    109     private static final int[] PREDEFINED_BUCKET_PERIODS = {
    110         3 * PERIOD_MIN_GCD_MS,   // 30s
    111         12 * PERIOD_MIN_GCD_MS,  // 120s
    112         48 * PERIOD_MIN_GCD_MS,  // 480s
    113         1 * PERIOD_MIN_GCD_MS,   // 10s
    114         6 * PERIOD_MIN_GCD_MS,   // 60s
    115         192 * PERIOD_MIN_GCD_MS, // 1920s
    116         24 * PERIOD_MIN_GCD_MS,  // 240s
    117         96 * PERIOD_MIN_GCD_MS,  // 960s
    118         384 * PERIOD_MIN_GCD_MS, // 3840s
    119         -1,                      // place holder for exponential back off scan
    120     };
    121 
    122     private static final int EXPONENTIAL_BACK_OFF_BUCKET_IDX =
    123             (PREDEFINED_BUCKET_PERIODS.length - 1);
    124     private static final int NUM_OF_REGULAR_BUCKETS =
    125             (PREDEFINED_BUCKET_PERIODS.length - 1);
    126 
    127     /**
    128      * This class is an intermediate representation for scheduling. This maintins the channel
    129      * collection to be scanned by the bucket as settings are added to it.
    130      */
    131     private class Bucket {
    132         public int period;
    133         public int bucketId;
    134         private final List<ScanSettings> mScanSettingsList = new ArrayList<>();
    135         private final ChannelCollection mChannelCollection;
    136 
    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