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