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 Bucket(int period)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 */ Bucket(Bucket originalBucket)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 */ createChannelSettings(int frequency)156 private WifiNative.ChannelSettings createChannelSettings(int frequency) { 157 WifiNative.ChannelSettings channelSettings = new WifiNative.ChannelSettings(); 158 channelSettings.frequency = frequency; 159 return channelSettings; 160 } 161 addSettings(ScanSettings scanSettings)162 public boolean addSettings(ScanSettings scanSettings) { 163 mChannelCollection.addChannels(scanSettings); 164 return mScanSettingsList.add(scanSettings); 165 } 166 removeSettings(ScanSettings scanSettings)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 getSettingsList()178 public List<ScanSettings> getSettingsList() { 179 return mScanSettingsList; 180 } 181 updateChannelCollection()182 public void updateChannelCollection() { 183 mChannelCollection.clear(); 184 for (ScanSettings settings : mScanSettingsList) { 185 mChannelCollection.addChannels(settings); 186 } 187 } 188 getChannelCollection()189 public ChannelCollection getChannelCollection() { 190 return mChannelCollection; 191 } 192 193 /** 194 * convert the setting for this bucket to HAL representation 195 */ createBucketSettings(int bucketId, int maxChannels)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 BucketList()259 BucketList() { 260 mBuckets = new Bucket[PREDEFINED_BUCKET_PERIODS.length]; 261 } 262 clearAll()263 public void clearAll() { 264 Arrays.fill(mBuckets, null); 265 mActiveBucketCount = 0; 266 } 267 clear(int index)268 public void clear(int index) { 269 if (mBuckets[index] != null) { 270 --mActiveBucketCount; 271 mBuckets[index] = null; 272 } 273 } 274 getOrCreate(int index)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 isActive(int index)284 public boolean isActive(int index) { 285 return mBuckets[index] != null; 286 } 287 get(int index)288 public Bucket get(int index) { 289 return mBuckets[index]; 290 } 291 size()292 public int size() { 293 return mBuckets.length; 294 } 295 getActiveCount()296 public int getActiveCount() { 297 return mActiveBucketCount; 298 } 299 getActiveRegularBucketCount()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 */ getSortedActiveRegularBucketList()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 getMaxBuckets()328 public int getMaxBuckets() { 329 return mMaxBuckets; 330 } 331 setMaxBuckets(int maxBuckets)332 public void setMaxBuckets(int maxBuckets) { 333 mMaxBuckets = maxBuckets; 334 } 335 getMaxChannelsPerBucket()336 public int getMaxChannelsPerBucket() { 337 return mMaxChannelsPerBucket; 338 } 339 340 // TODO: find a way to get max channels setMaxChannelsPerBucket(int maxChannels)341 public void setMaxChannelsPerBucket(int maxChannels) { 342 mMaxChannelsPerBucket = maxChannels; 343 } 344 getMaxBatch()345 public int getMaxBatch() { 346 return mMaxBatch; 347 } 348 349 // TODO: find a way to get max batch size setMaxBatch(int maxBatch)350 public void setMaxBatch(int maxBatch) { 351 mMaxBatch = maxBatch; 352 } 353 getMaxApPerScan()354 public int getMaxApPerScan() { 355 return mMaxApPerScan; 356 } 357 setMaxApPerScan(int maxApPerScan)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 BackgroundScanScheduler(ChannelHelper channelHelper)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 */ updateSchedule(@onNull Collection<ScanSettings> requests)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 */ getSchedule()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 */ shouldReportFullScanResultForSettings(@onNull ScanResult result, int bucketsScanned, @NonNull ScanSettings settings)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 */ filterResultsForSettings(@onNull ScanData[] scanDatas, @NonNull ScanSettings settings)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 */ getScheduledBucket(ScanSettings settings)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 */ createSchedule(List<Bucket> bucketList, int maxChannelsPerBucket)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 */ addScanToBuckets(ScanSettings settings)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 */ findBestRegularBucketIndex(int requestedPeriod, int maxNumBuckets)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 */ compactBuckets(int maxBuckets)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 */ cloneScanSettings(ScanSettings originalSettings)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 */ createCurrentBucketSplitSettings(ScanSettings originalSettings, Set<Integer> currentBucketChannels)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 */ createTargetBucketSplitSettings(ScanSettings originalSettings, Set<Integer> targetBucketChannels)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 */ createSplitSettings(ScanSettings originalSettings, ChannelCollection targetBucketChannelCol)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 */ mergeSettingsToLowerBuckets(ScanSettings originalSettings, Bucket currentBucket, ListIterator<Bucket> iterTargetBuckets)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 */ optimizeBuckets()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 */ partitionChannelSet(Set<Integer> originalChannelSet, int maxChannelsPerBucket)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 */ createSplitBuckets(Bucket originalBucket, List<Set<Integer>> channelSets)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 */ fixBuckets(List<Bucket> originalBucketList, int maxBuckets, int maxChannelsPerBucket)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