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.Iterator;
     40 import java.util.List;
     41 import java.util.ListIterator;
     42 import java.util.Map;
     43 import 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  */
     60 public 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