/*
|
* Copyright (C) 2015 The Android Open Source Project
|
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
*/
|
|
package com.android.server.wifi.scanner;
|
|
import android.annotation.NonNull;
|
import android.annotation.Nullable;
|
import android.net.wifi.ScanResult;
|
import android.net.wifi.WifiScanner;
|
import android.net.wifi.WifiScanner.ScanData;
|
import android.net.wifi.WifiScanner.ScanSettings;
|
import android.util.ArraySet;
|
import android.util.Pair;
|
import android.util.Rational;
|
import android.util.Slog;
|
|
import com.android.server.wifi.WifiNative;
|
import com.android.server.wifi.scanner.ChannelHelper.ChannelCollection;
|
|
import java.util.ArrayList;
|
import java.util.Arrays;
|
import java.util.Collection;
|
import java.util.Collections;
|
import java.util.Comparator;
|
import java.util.HashMap;
|
import java.util.Iterator;
|
import java.util.List;
|
import java.util.ListIterator;
|
import java.util.Map;
|
import java.util.Set;
|
|
/**
|
* <p>This class takes a series of scan requests and formulates the best hardware level scanning
|
* schedule it can to try and satisfy requests. The hardware level accepts a series of buckets,
|
* where each bucket represents a set of channels and an interval to scan at. This
|
* scheduler operates as follows:</p>
|
*
|
* <p>Each new request is placed in the best predefined bucket. Once all requests have been added
|
* the last buckets (lower priority) are placed in the next best bucket until the number of buckets
|
* is less than the number supported by the hardware.
|
*
|
* <p>Finally, the scheduler creates a WifiNative.ScanSettings from the list of buckets which may be
|
* passed through the Wifi HAL.</p>
|
*
|
* <p>This class is not thread safe.</p>
|
*/
|
public class BackgroundScanScheduler {
|
|
private static final String TAG = "BackgroundScanScheduler";
|
private static final boolean DBG = false;
|
|
public static final int DEFAULT_MAX_BUCKETS = 8;
|
// Max channels that can be specified per bucket
|
public static final int DEFAULT_MAX_CHANNELS_PER_BUCKET = 16;
|
// anecdotally, some chipsets will fail without explanation with a higher batch size, and
|
// there is apparently no way to retrieve the maximum batch size
|
public static final int DEFAULT_MAX_SCANS_TO_BATCH = 10;
|
public static final int DEFAULT_MAX_AP_PER_SCAN = 32;
|
|
/**
|
* Value that all scan periods must be an integer multiple of
|
*/
|
private static final int PERIOD_MIN_GCD_MS = 10000;
|
/**
|
* Default period to use if no buckets are being scheduled
|
*/
|
private static final int DEFAULT_PERIOD_MS = 30000;
|
/**
|
* Scan report threshold percentage to assign to the schedule by default
|
* @see com.android.server.wifi.WifiNative.ScanSettings#report_threshold_percent
|
*/
|
private static final int DEFAULT_REPORT_THRESHOLD_PERCENTAGE = 100;
|
|
/**
|
* List of predefined periods (in ms) that buckets can be scheduled at. Ordered by preference
|
* if there are not enough buckets for all periods. All periods MUST be an integer multiple of
|
* the next smallest bucket with the smallest bucket having a period of PERIOD_MIN_GCD_MS.
|
* This requirement allows scans to be scheduled more efficiently because scan requests with
|
* intersecting channels will result in those channels being scanned exactly once at the smaller
|
* period and no unnecessary scan being scheduled. If this was not the case and two requests
|
* had channel 5 with periods of 15 seconds and 25 seconds then channel 5 would be scanned
|
* 296 (3600/15 + 3600/25 - 3500/75) times an hour instead of 240 times an hour (3600/15) if
|
* the 25s scan is rescheduled at 30s. This is less important with higher periods as it has
|
* significantly less impact. Ranking could be done by favoring shorter or longer; however,
|
* this would result in straying further from the requested period and possibly power
|
* implications if the scan is scheduled at a significantly lower period.
|
*
|
* For example if the hardware only supports 2 buckets and scans are requested with periods of
|
* 40s, 20s and 10s then the two buckets scheduled will have periods 40s and 20s and the 10s
|
* scan will be placed in the 20s bucket.
|
*
|
* If there are special scan requests such as exponential back off, we always dedicate a bucket
|
* for each type. Regular scan requests will be packed into the remaining buckets.
|
*/
|
private static final int[] PREDEFINED_BUCKET_PERIODS = {
|
3 * PERIOD_MIN_GCD_MS, // 30s
|
12 * PERIOD_MIN_GCD_MS, // 120s
|
48 * PERIOD_MIN_GCD_MS, // 480s
|
1 * PERIOD_MIN_GCD_MS, // 10s
|
6 * PERIOD_MIN_GCD_MS, // 60s
|
192 * PERIOD_MIN_GCD_MS, // 1920s
|
24 * PERIOD_MIN_GCD_MS, // 240s
|
96 * PERIOD_MIN_GCD_MS, // 960s
|
384 * PERIOD_MIN_GCD_MS, // 3840s
|
-1, // place holder for exponential back off scan
|
};
|
|
private static final int EXPONENTIAL_BACK_OFF_BUCKET_IDX =
|
(PREDEFINED_BUCKET_PERIODS.length - 1);
|
private static final int NUM_OF_REGULAR_BUCKETS =
|
(PREDEFINED_BUCKET_PERIODS.length - 1);
|
|
/**
|
* This class is an intermediate representation for scheduling. This maintins the channel
|
* collection to be scanned by the bucket as settings are added to it.
|
*/
|
private class Bucket {
|
public int period;
|
public int bucketId;
|
private final List<ScanSettings> mScanSettingsList = new ArrayList<>();
|
private final ChannelCollection mChannelCollection;
|
|
Bucket(int period) {
|
this.period = period;
|
this.bucketId = 0;
|
mScanSettingsList.clear();
|
mChannelCollection = mChannelHelper.createChannelCollection();
|
}
|
|
/**
|
* Copy constructor which populates the settings list from the original bucket object.
|
*/
|
Bucket(Bucket originalBucket) {
|
this(originalBucket.period);
|
for (ScanSettings settings : originalBucket.getSettingsList()) {
|
mScanSettingsList.add(settings);
|
}
|
}
|
|
/**
|
* convert ChannelSpec to native representation
|
*/
|
private WifiNative.ChannelSettings createChannelSettings(int frequency) {
|
WifiNative.ChannelSettings channelSettings = new WifiNative.ChannelSettings();
|
channelSettings.frequency = frequency;
|
return channelSettings;
|
}
|
|
public boolean addSettings(ScanSettings scanSettings) {
|
mChannelCollection.addChannels(scanSettings);
|
return mScanSettingsList.add(scanSettings);
|
}
|
|
public boolean removeSettings(ScanSettings scanSettings) {
|
if (mScanSettingsList.remove(scanSettings)) {
|
// It's difficult to handle settings removal from buckets in terms of
|
// maintaining the correct channel collection, so recreate the channel
|
// collection from the remaining elements.
|
updateChannelCollection();
|
return true;
|
}
|
return false;
|
}
|
|
public List<ScanSettings> getSettingsList() {
|
return mScanSettingsList;
|
}
|
|
public void updateChannelCollection() {
|
mChannelCollection.clear();
|
for (ScanSettings settings : mScanSettingsList) {
|
mChannelCollection.addChannels(settings);
|
}
|
}
|
|
public ChannelCollection getChannelCollection() {
|
return mChannelCollection;
|
}
|
|
/**
|
* convert the setting for this bucket to HAL representation
|
*/
|
public WifiNative.BucketSettings createBucketSettings(int bucketId, int maxChannels) {
|
this.bucketId = bucketId;
|
int reportEvents = WifiScanner.REPORT_EVENT_NO_BATCH;
|
int maxPeriodInMs = 0;
|
int stepCount = 0;
|
int bucketIndex = 0;
|
|
for (int i = 0; i < mScanSettingsList.size(); ++i) {
|
WifiScanner.ScanSettings setting = mScanSettingsList.get(i);
|
int requestedReportEvents = setting.reportEvents;
|
if ((requestedReportEvents & WifiScanner.REPORT_EVENT_NO_BATCH) == 0) {
|
reportEvents &= ~WifiScanner.REPORT_EVENT_NO_BATCH;
|
}
|
if ((requestedReportEvents & WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN) != 0) {
|
reportEvents |= WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN;
|
}
|
if ((requestedReportEvents & WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT) != 0) {
|
reportEvents |= WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT;
|
}
|
// For the bucket allocated to exponential back off scan, the values of
|
// the exponential back off scan related parameters from the very first
|
// setting in the settings list will be used to configure this bucket.
|
//
|
if (i == 0 && setting.maxPeriodInMs != 0
|
&& setting.maxPeriodInMs != setting.periodInMs) {
|
// Align the starting period with one of the pre-defined regular
|
// scan periods. This will optimize the scan schedule when it has
|
// both exponential back off scan and regular scan(s).
|
bucketIndex = findBestRegularBucketIndex(setting.periodInMs,
|
NUM_OF_REGULAR_BUCKETS);
|
period = PREDEFINED_BUCKET_PERIODS[bucketIndex];
|
maxPeriodInMs = (setting.maxPeriodInMs < period)
|
? period
|
: setting.maxPeriodInMs;
|
stepCount = setting.stepCount;
|
}
|
}
|
|
WifiNative.BucketSettings bucketSettings = new WifiNative.BucketSettings();
|
bucketSettings.bucket = bucketId;
|
bucketSettings.report_events = reportEvents;
|
bucketSettings.period_ms = period;
|
bucketSettings.max_period_ms = maxPeriodInMs;
|
bucketSettings.step_count = stepCount;
|
mChannelCollection.fillBucketSettings(bucketSettings, maxChannels);
|
return bucketSettings;
|
}
|
}
|
|
/**
|
* Maintains a list of buckets and the number that are active (non-null)
|
*/
|
private class BucketList {
|
// Comparator to sort the buckets in order of increasing time periods
|
private final Comparator<Bucket> mTimePeriodSortComparator =
|
new Comparator<Bucket>() {
|
public int compare(Bucket b1, Bucket b2) {
|
return b1.period - b2.period;
|
}
|
};
|
private final Bucket[] mBuckets;
|
private int mActiveBucketCount = 0;
|
|
BucketList() {
|
mBuckets = new Bucket[PREDEFINED_BUCKET_PERIODS.length];
|
}
|
|
public void clearAll() {
|
Arrays.fill(mBuckets, null);
|
mActiveBucketCount = 0;
|
}
|
|
public void clear(int index) {
|
if (mBuckets[index] != null) {
|
--mActiveBucketCount;
|
mBuckets[index] = null;
|
}
|
}
|
|
public Bucket getOrCreate(int index) {
|
Bucket bucket = mBuckets[index];
|
if (bucket == null) {
|
++mActiveBucketCount;
|
bucket = mBuckets[index] = new Bucket(PREDEFINED_BUCKET_PERIODS[index]);
|
}
|
return bucket;
|
}
|
|
public boolean isActive(int index) {
|
return mBuckets[index] != null;
|
}
|
|
public Bucket get(int index) {
|
return mBuckets[index];
|
}
|
|
public int size() {
|
return mBuckets.length;
|
}
|
|
public int getActiveCount() {
|
return mActiveBucketCount;
|
}
|
|
public int getActiveRegularBucketCount() {
|
if (isActive(EXPONENTIAL_BACK_OFF_BUCKET_IDX)) {
|
return mActiveBucketCount - 1;
|
} else {
|
return mActiveBucketCount;
|
}
|
}
|
|
/**
|
* Returns the active regular buckets sorted by their increasing time periods.
|
*/
|
public List<Bucket> getSortedActiveRegularBucketList() {
|
ArrayList<Bucket> activeBuckets = new ArrayList<>();
|
for (int i = 0; i < mBuckets.length; i++) {
|
if (mBuckets[i] != null && i != EXPONENTIAL_BACK_OFF_BUCKET_IDX) {
|
activeBuckets.add(mBuckets[i]);
|
}
|
}
|
Collections.sort(activeBuckets, mTimePeriodSortComparator);
|
return activeBuckets;
|
}
|
}
|
|
private int mMaxBuckets = DEFAULT_MAX_BUCKETS;
|
private int mMaxChannelsPerBucket = DEFAULT_MAX_CHANNELS_PER_BUCKET;
|
private int mMaxBatch = DEFAULT_MAX_SCANS_TO_BATCH;
|
private int mMaxApPerScan = DEFAULT_MAX_AP_PER_SCAN;
|
|
public int getMaxBuckets() {
|
return mMaxBuckets;
|
}
|
|
public void setMaxBuckets(int maxBuckets) {
|
mMaxBuckets = maxBuckets;
|
}
|
|
public int getMaxChannelsPerBucket() {
|
return mMaxChannelsPerBucket;
|
}
|
|
// TODO: find a way to get max channels
|
public void setMaxChannelsPerBucket(int maxChannels) {
|
mMaxChannelsPerBucket = maxChannels;
|
}
|
|
public int getMaxBatch() {
|
return mMaxBatch;
|
}
|
|
// TODO: find a way to get max batch size
|
public void setMaxBatch(int maxBatch) {
|
mMaxBatch = maxBatch;
|
}
|
|
public int getMaxApPerScan() {
|
return mMaxApPerScan;
|
}
|
|
public void setMaxApPerScan(int maxApPerScan) {
|
mMaxApPerScan = maxApPerScan;
|
}
|
|
private final BucketList mBuckets = new BucketList();
|
private final ChannelHelper mChannelHelper;
|
private WifiNative.ScanSettings mSchedule;
|
// This keeps track of the settings to the max time period bucket to which it was scheduled.
|
private final Map<ScanSettings, Bucket> mSettingsToScheduledBucket = new HashMap<>();
|
|
public BackgroundScanScheduler(ChannelHelper channelHelper) {
|
mChannelHelper = channelHelper;
|
createSchedule(new ArrayList<Bucket>(), getMaxChannelsPerBucket());
|
}
|
|
/**
|
* Updates the schedule from the given set of requests.
|
*/
|
public void updateSchedule(@NonNull Collection<ScanSettings> requests) {
|
// create initial schedule
|
mBuckets.clearAll();
|
for (ScanSettings request : requests) {
|
addScanToBuckets(request);
|
}
|
|
compactBuckets(getMaxBuckets());
|
|
List<Bucket> bucketList = optimizeBuckets();
|
|
List<Bucket> fixedBucketList =
|
fixBuckets(bucketList, getMaxBuckets(), getMaxChannelsPerBucket());
|
|
createSchedule(fixedBucketList, getMaxChannelsPerBucket());
|
}
|
|
/**
|
* Retrieves the current scanning schedule.
|
*/
|
public @NonNull WifiNative.ScanSettings getSchedule() {
|
return mSchedule;
|
}
|
|
/**
|
* Returns true if the given scan result should be reported to a listener with the given
|
* settings.
|
*/
|
public boolean shouldReportFullScanResultForSettings(@NonNull ScanResult result,
|
int bucketsScanned, @NonNull ScanSettings settings) {
|
return ScanScheduleUtil.shouldReportFullScanResultForSettings(mChannelHelper,
|
result, bucketsScanned, settings, getScheduledBucket(settings));
|
}
|
|
/**
|
* Returns a filtered version of the scan results from the chip that represents only the data
|
* requested in the settings. Will return null if the result should not be reported.
|
*/
|
public @Nullable ScanData[] filterResultsForSettings(@NonNull ScanData[] scanDatas,
|
@NonNull ScanSettings settings) {
|
return ScanScheduleUtil.filterResultsForSettings(mChannelHelper, scanDatas, settings,
|
getScheduledBucket(settings));
|
}
|
|
/**
|
* Retrieves the max time period bucket idx at which this setting was scheduled
|
*/
|
public int getScheduledBucket(ScanSettings settings) {
|
Bucket maxScheduledBucket = mSettingsToScheduledBucket.get(settings);
|
if (maxScheduledBucket != null) {
|
return maxScheduledBucket.bucketId;
|
} else {
|
Slog.wtf(TAG, "No bucket found for settings");
|
return -1;
|
}
|
}
|
|
/**
|
* creates a schedule for the current buckets
|
*/
|
private void createSchedule(List<Bucket> bucketList, int maxChannelsPerBucket) {
|
WifiNative.ScanSettings schedule = new WifiNative.ScanSettings();
|
schedule.num_buckets = bucketList.size();
|
schedule.buckets = new WifiNative.BucketSettings[bucketList.size()];
|
|
schedule.max_ap_per_scan = 0;
|
schedule.report_threshold_num_scans = getMaxBatch();
|
|
// set all buckets in schedule
|
int bucketId = 0;
|
for (Bucket bucket : bucketList) {
|
schedule.buckets[bucketId] =
|
bucket.createBucketSettings(bucketId, maxChannelsPerBucket);
|
for (ScanSettings settings : bucket.getSettingsList()) {
|
// set APs per scan
|
if (settings.numBssidsPerScan > schedule.max_ap_per_scan) {
|
schedule.max_ap_per_scan = settings.numBssidsPerScan;
|
}
|
// set batching
|
if (settings.maxScansToCache != 0
|
&& settings.maxScansToCache < schedule.report_threshold_num_scans) {
|
schedule.report_threshold_num_scans = settings.maxScansToCache;
|
}
|
}
|
bucketId++;
|
}
|
|
schedule.report_threshold_percent = DEFAULT_REPORT_THRESHOLD_PERCENTAGE;
|
|
if (schedule.max_ap_per_scan == 0 || schedule.max_ap_per_scan > getMaxApPerScan()) {
|
schedule.max_ap_per_scan = getMaxApPerScan();
|
}
|
|
// update base period as gcd of periods
|
if (schedule.num_buckets > 0) {
|
int gcd = schedule.buckets[0].period_ms;
|
for (int b = 1; b < schedule.num_buckets; b++) {
|
gcd = Rational.gcd(schedule.buckets[b].period_ms, gcd);
|
}
|
|
if (gcd < PERIOD_MIN_GCD_MS) {
|
Slog.wtf(TAG, "found gcd less than min gcd");
|
gcd = PERIOD_MIN_GCD_MS;
|
}
|
|
schedule.base_period_ms = gcd;
|
} else {
|
schedule.base_period_ms = DEFAULT_PERIOD_MS;
|
}
|
|
mSchedule = schedule;
|
}
|
|
/**
|
* Add a scan to the most appropriate bucket, creating the bucket if necessary.
|
*/
|
private void addScanToBuckets(ScanSettings settings) {
|
int bucketIndex;
|
|
if (settings.maxPeriodInMs != 0 && settings.maxPeriodInMs != settings.periodInMs) {
|
// exponential back off scan has a dedicated bucket
|
bucketIndex = EXPONENTIAL_BACK_OFF_BUCKET_IDX;
|
} else {
|
bucketIndex = findBestRegularBucketIndex(settings.periodInMs, NUM_OF_REGULAR_BUCKETS);
|
}
|
|
mBuckets.getOrCreate(bucketIndex).addSettings(settings);
|
}
|
|
/**
|
* find closest bucket period to the requested period in all predefined buckets
|
*/
|
private static int findBestRegularBucketIndex(int requestedPeriod, int maxNumBuckets) {
|
maxNumBuckets = Math.min(maxNumBuckets, NUM_OF_REGULAR_BUCKETS);
|
int index = -1;
|
int minDiff = Integer.MAX_VALUE;
|
for (int i = 0; i < maxNumBuckets; ++i) {
|
int diff = Math.abs(PREDEFINED_BUCKET_PERIODS[i] - requestedPeriod);
|
if (diff < minDiff) {
|
minDiff = diff;
|
index = i;
|
}
|
}
|
if (index == -1) {
|
Slog.wtf(TAG, "Could not find best bucket for period " + requestedPeriod + " in "
|
+ maxNumBuckets + " buckets");
|
}
|
return index;
|
}
|
|
/**
|
* Reduce the number of required buckets by reassigning lower priority buckets to the next
|
* closest period bucket.
|
*/
|
private void compactBuckets(int maxBuckets) {
|
int maxRegularBuckets = maxBuckets;
|
|
// reserve one bucket for exponential back off scan if there is
|
// such request(s)
|
if (mBuckets.isActive(EXPONENTIAL_BACK_OFF_BUCKET_IDX)) {
|
maxRegularBuckets--;
|
}
|
for (int i = NUM_OF_REGULAR_BUCKETS - 1;
|
i >= 0 && mBuckets.getActiveRegularBucketCount() > maxRegularBuckets; --i) {
|
if (mBuckets.isActive(i)) {
|
for (ScanSettings scanRequest : mBuckets.get(i).getSettingsList()) {
|
int newBucketIndex = findBestRegularBucketIndex(scanRequest.periodInMs, i);
|
mBuckets.getOrCreate(newBucketIndex).addSettings(scanRequest);
|
}
|
mBuckets.clear(i);
|
}
|
}
|
}
|
|
/**
|
* Clone the provided scan settings fields to a new ScanSettings object.
|
*/
|
private ScanSettings cloneScanSettings(ScanSettings originalSettings) {
|
ScanSettings settings = new ScanSettings();
|
settings.band = originalSettings.band;
|
settings.channels = originalSettings.channels;
|
settings.periodInMs = originalSettings.periodInMs;
|
settings.reportEvents = originalSettings.reportEvents;
|
settings.numBssidsPerScan = originalSettings.numBssidsPerScan;
|
settings.maxScansToCache = originalSettings.maxScansToCache;
|
settings.maxPeriodInMs = originalSettings.maxPeriodInMs;
|
settings.stepCount = originalSettings.stepCount;
|
settings.isPnoScan = originalSettings.isPnoScan;
|
return settings;
|
}
|
|
/**
|
* Creates a split scan setting that needs to be added back to the current bucket.
|
*/
|
private ScanSettings createCurrentBucketSplitSettings(ScanSettings originalSettings,
|
Set<Integer> currentBucketChannels) {
|
ScanSettings currentBucketSettings = cloneScanSettings(originalSettings);
|
// Let's create a new settings for the current bucket with the same flags, but the missing
|
// channels from the other bucket
|
currentBucketSettings.band = WifiScanner.WIFI_BAND_UNSPECIFIED;
|
currentBucketSettings.channels = new WifiScanner.ChannelSpec[currentBucketChannels.size()];
|
int chanIdx = 0;
|
for (Integer channel : currentBucketChannels) {
|
currentBucketSettings.channels[chanIdx++] = new WifiScanner.ChannelSpec(channel);
|
}
|
return currentBucketSettings;
|
}
|
|
/**
|
* Creates a split scan setting that needs to be added to the target lower time period bucket.
|
* The reportEvents field is modified to remove REPORT_EVENT_AFTER_EACH_SCAN because we
|
* need this flag only in the higher time period bucket.
|
*/
|
private ScanSettings createTargetBucketSplitSettings(ScanSettings originalSettings,
|
Set<Integer> targetBucketChannels) {
|
ScanSettings targetBucketSettings = cloneScanSettings(originalSettings);
|
// The new settings for the other bucket will have the channels that already in the that
|
// bucket. We'll need to do some migration of the |reportEvents| flags.
|
targetBucketSettings.band = WifiScanner.WIFI_BAND_UNSPECIFIED;
|
targetBucketSettings.channels = new WifiScanner.ChannelSpec[targetBucketChannels.size()];
|
int chanIdx = 0;
|
for (Integer channel : targetBucketChannels) {
|
targetBucketSettings.channels[chanIdx++] = new WifiScanner.ChannelSpec(channel);
|
}
|
targetBucketSettings.reportEvents =
|
originalSettings.reportEvents
|
& (WifiScanner.REPORT_EVENT_NO_BATCH
|
| WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT);
|
return targetBucketSettings;
|
}
|
|
/**
|
* Split the scan settings into 2 so that they can be put into 2 separate buckets.
|
* @return The first scan setting needs to be added back to the current bucket
|
* The second scan setting needs to be added to the other bucket
|
*/
|
private Pair<ScanSettings, ScanSettings> createSplitSettings(ScanSettings originalSettings,
|
ChannelCollection targetBucketChannelCol) {
|
Set<Integer> currentBucketChannels =
|
targetBucketChannelCol.getMissingChannelsFromSettings(originalSettings);
|
Set<Integer> targetBucketChannels =
|
targetBucketChannelCol.getContainingChannelsFromSettings(originalSettings);
|
// Two Copy of the original settings
|
ScanSettings currentBucketSettings =
|
createCurrentBucketSplitSettings(originalSettings, currentBucketChannels);
|
ScanSettings targetBucketSettings =
|
createTargetBucketSplitSettings(originalSettings, targetBucketChannels);
|
return Pair.create(currentBucketSettings, targetBucketSettings);
|
}
|
|
/**
|
* Try to merge the settings to lower buckets.
|
* Check if the channels in this settings is already covered by a lower time period
|
* bucket. If it's partially covered, the settings is split else the entire settings
|
* is moved to the lower time period bucket.
|
* This method updates the |mSettingsToScheduledBucket| mapping.
|
* @return Pair<wasMerged, remainingSplitSettings>
|
* wasMerged - boolean indicating whether the original setting was merged to lower time
|
* period buckets.
|
* remainingSplitSettings - Partial Scan Settings that need to be added back to the
|
* current bucket.
|
*/
|
private Pair<Boolean, ScanSettings> mergeSettingsToLowerBuckets(ScanSettings originalSettings,
|
Bucket currentBucket, ListIterator<Bucket> iterTargetBuckets) {
|
ScanSettings remainingSplitSettings = null;
|
boolean wasMerged = false;
|
Bucket maxScheduledBucket = currentBucket;
|
|
while (iterTargetBuckets.hasPrevious()) {
|
Bucket targetBucket = iterTargetBuckets.previous();
|
ChannelCollection targetBucketChannelCol = targetBucket.getChannelCollection();
|
if (targetBucketChannelCol.containsSettings(originalSettings)) {
|
targetBucket.addSettings(originalSettings);
|
// Update the max scheduled bucket for this setting
|
maxScheduledBucket = targetBucket;
|
wasMerged = true;
|
} else if (targetBucketChannelCol.partiallyContainsSettings(originalSettings)) {
|
Pair<ScanSettings, ScanSettings> splitSettings;
|
if (remainingSplitSettings == null) {
|
splitSettings = createSplitSettings(originalSettings, targetBucketChannelCol);
|
} else {
|
splitSettings =
|
createSplitSettings(remainingSplitSettings, targetBucketChannelCol);
|
}
|
targetBucket.addSettings(splitSettings.second);
|
// Update the |remainingSplitSettings| to keep track of the remaining scan settings.
|
// The original settings could be split across multiple buckets.
|
remainingSplitSettings = splitSettings.first;
|
wasMerged = true;
|
}
|
}
|
// Update the settings to scheduled bucket mapping. This is needed for event
|
// reporting lookup
|
mSettingsToScheduledBucket.put(originalSettings, maxScheduledBucket);
|
|
return Pair.create(wasMerged, remainingSplitSettings);
|
}
|
|
/**
|
* Optimize all the active buckets by removing duplicate channels in the buckets.
|
* This method tries to go through the settings in all the buckets and checks if the same
|
* channels for the setting is already being scanned by another bucked with lower time period.
|
* If yes, move the setting to the lower time period bucket. If all the settings from a higher
|
* period has been moved out, that bucket can be removed.
|
*
|
* We're trying to avoid cases where we have the same channels being scanned in different
|
* buckets. This is to workaround the fact that the HAL implementations have a max number of
|
* cumulative channel across buckets (b/28022609).
|
*/
|
private List<Bucket> optimizeBuckets() {
|
mSettingsToScheduledBucket.clear();
|
List<Bucket> sortedBuckets = mBuckets.getSortedActiveRegularBucketList();
|
ListIterator<Bucket> iterBuckets = sortedBuckets.listIterator();
|
// This is needed to keep track of split settings that need to be added back to the same
|
// bucket at the end of iterating thru all the settings. This has to be a separate temp list
|
// to prevent concurrent modification exceptions during iterations.
|
List<ScanSettings> currentBucketSplitSettingsList = new ArrayList<>();
|
|
// We need to go thru each setting starting from the lowest time period bucket and check
|
// if they're already contained in a lower time period bucket. If yes, delete the setting
|
// from the current bucket and move it to the other bucket. If the settings are only
|
// partially contained, split the settings into two and move the partial bucket back
|
// to the same bucket. Finally, if all the settings have been moved out, remove the current
|
// bucket altogether.
|
while (iterBuckets.hasNext()) {
|
Bucket currentBucket = iterBuckets.next();
|
Iterator<ScanSettings> iterSettings = currentBucket.getSettingsList().iterator();
|
|
currentBucketSplitSettingsList.clear();
|
|
while (iterSettings.hasNext()) {
|
ScanSettings currentSettings = iterSettings.next();
|
ListIterator<Bucket> iterTargetBuckets =
|
sortedBuckets.listIterator(iterBuckets.previousIndex());
|
|
Pair<Boolean, ScanSettings> mergeResult =
|
mergeSettingsToLowerBuckets(
|
currentSettings, currentBucket, iterTargetBuckets);
|
|
boolean wasMerged = mergeResult.first.booleanValue();
|
if (wasMerged) {
|
// Remove the original settings from the current bucket.
|
iterSettings.remove();
|
ScanSettings remainingSplitSettings = mergeResult.second;
|
if (remainingSplitSettings != null) {
|
// Add back the remaining split settings to the current bucket.
|
currentBucketSplitSettingsList.add(remainingSplitSettings);
|
}
|
}
|
}
|
|
for (ScanSettings splitSettings: currentBucketSplitSettingsList) {
|
currentBucket.addSettings(splitSettings);
|
}
|
if (currentBucket.getSettingsList().isEmpty()) {
|
iterBuckets.remove();
|
} else {
|
// Update the channel collection to account for the removed settings
|
currentBucket.updateChannelCollection();
|
}
|
}
|
|
// Update the settings to scheduled bucket map for all exponential scans.
|
if (mBuckets.isActive(EXPONENTIAL_BACK_OFF_BUCKET_IDX)) {
|
Bucket exponentialBucket = mBuckets.get(EXPONENTIAL_BACK_OFF_BUCKET_IDX);
|
for (ScanSettings settings : exponentialBucket.getSettingsList()) {
|
mSettingsToScheduledBucket.put(settings, exponentialBucket);
|
}
|
sortedBuckets.add(exponentialBucket);
|
}
|
|
return sortedBuckets;
|
}
|
|
/**
|
* Partition the channel set into 2 or more based on the max channels that can be specified for
|
* each bucket.
|
*/
|
private List<Set<Integer>> partitionChannelSet(Set<Integer> originalChannelSet,
|
int maxChannelsPerBucket) {
|
ArrayList<Set<Integer>> channelSetList = new ArrayList();
|
ArraySet<Integer> channelSet = new ArraySet<>();
|
Iterator<Integer> iterChannels = originalChannelSet.iterator();
|
|
while (iterChannels.hasNext()) {
|
channelSet.add(iterChannels.next());
|
if (channelSet.size() == maxChannelsPerBucket) {
|
channelSetList.add(channelSet);
|
channelSet = new ArraySet<>();
|
}
|
}
|
// Add the last partial set if any
|
if (!channelSet.isEmpty()) {
|
channelSetList.add(channelSet);
|
}
|
return channelSetList;
|
}
|
|
/**
|
* Creates a list of split buckets with the channel collection corrected to fit the
|
* max channel list size that can be specified. The original channel collection will be split
|
* into multiple buckets with the same scan settings.
|
* Note: This does not update the mSettingsToScheduledBucket map because this bucket is
|
* essentially a copy of the original bucket, so it should not affect the event reporting.
|
* This bucket results will come back the same time the original bucket results come back.
|
*/
|
private List<Bucket> createSplitBuckets(Bucket originalBucket, List<Set<Integer>> channelSets) {
|
List<Bucket> splitBucketList = new ArrayList<>();
|
int channelSetIdx = 0;
|
|
for (Set<Integer> channelSet : channelSets) {
|
Bucket splitBucket;
|
if (channelSetIdx == 0) {
|
// Need to keep the original bucket to keep track of the settings to scheduled
|
// bucket mapping.
|
splitBucket = originalBucket;
|
} else {
|
splitBucket = new Bucket(originalBucket);
|
}
|
ChannelCollection splitBucketChannelCollection = splitBucket.getChannelCollection();
|
splitBucketChannelCollection.clear();
|
for (Integer channel : channelSet) {
|
splitBucketChannelCollection.addChannel(channel);
|
}
|
channelSetIdx++;
|
splitBucketList.add(splitBucket);
|
}
|
return splitBucketList;
|
}
|
|
/**
|
* Check if any of the buckets don't fit into the bucket specification and fix it. This
|
* creates duplicate buckets to fit all the channels. So, the channels to be scanned
|
* will be split across 2 (or more) buckets.
|
* TODO: If we reach the max number of buckets, then this fix will be skipped!
|
*/
|
private List<Bucket> fixBuckets(List<Bucket> originalBucketList, int maxBuckets,
|
int maxChannelsPerBucket) {
|
List<Bucket> fixedBucketList = new ArrayList<>();
|
int totalNumBuckets = originalBucketList.size();
|
|
for (Bucket originalBucket : originalBucketList) {
|
ChannelCollection channelCollection = originalBucket.getChannelCollection();
|
Set<Integer> channelSet = channelCollection.getChannelSet();
|
if (channelSet.size() > maxChannelsPerBucket) {
|
List<Set<Integer>> channelSetList =
|
partitionChannelSet(channelSet, maxChannelsPerBucket);
|
int newTotalNumBuckets = totalNumBuckets + channelSetList.size() - 1;
|
if (newTotalNumBuckets <= maxBuckets) {
|
List<Bucket> splitBuckets = createSplitBuckets(originalBucket, channelSetList);
|
for (Bucket bucket : splitBuckets) {
|
fixedBucketList.add(bucket);
|
}
|
totalNumBuckets = newTotalNumBuckets;
|
} else {
|
fixedBucketList.add(originalBucket);
|
}
|
} else {
|
fixedBucketList.add(originalBucket);
|
}
|
}
|
return fixedBucketList;
|
}
|
}
|