| /* |
| * Copyright (C) 2014 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.bluetooth.gatt; |
| |
| import android.annotation.RequiresPermission; |
| import android.app.ActivityManager; |
| import android.app.AlarmManager; |
| import android.app.PendingIntent; |
| import android.bluetooth.BluetoothA2dp; |
| import android.bluetooth.BluetoothA2dpSink; |
| import android.bluetooth.BluetoothHeadset; |
| import android.bluetooth.BluetoothHeadsetClient; |
| import android.bluetooth.BluetoothHearingAid; |
| import android.bluetooth.BluetoothLeAudio; |
| import android.bluetooth.BluetoothProfile; |
| import android.bluetooth.le.ScanCallback; |
| import android.bluetooth.le.ScanFilter; |
| import android.bluetooth.le.ScanSettings; |
| import android.content.BroadcastReceiver; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.hardware.display.DisplayManager; |
| import android.location.LocationManager; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.RemoteException; |
| import android.os.SystemClock; |
| import android.provider.Settings; |
| import android.util.Log; |
| import android.util.SparseBooleanArray; |
| import android.util.SparseIntArray; |
| import android.view.Display; |
| |
| import com.android.bluetooth.Utils; |
| import com.android.bluetooth.btservice.AdapterService; |
| import com.android.bluetooth.btservice.BluetoothAdapterProxy; |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.util.ArrayDeque; |
| import java.util.Collections; |
| import java.util.Deque; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.UUID; |
| import java.util.concurrent.ConcurrentHashMap; |
| |
| /** |
| * Class that handles Bluetooth LE scan related operations. |
| * |
| * @hide |
| */ |
| public class ScanManager { |
| private static final boolean DBG = GattServiceConfig.DBG; |
| private static final String TAG = GattServiceConfig.TAG_PREFIX + "ScanManager"; |
| |
| /** |
| * Scan params corresponding to regular scan setting |
| */ |
| private static final int SCAN_MODE_LOW_POWER_WINDOW_MS = 140; |
| private static final int SCAN_MODE_LOW_POWER_INTERVAL_MS = 1400; |
| private static final int SCAN_MODE_BALANCED_WINDOW_MS = 183; |
| private static final int SCAN_MODE_BALANCED_INTERVAL_MS = 730; |
| private static final int SCAN_MODE_LOW_LATENCY_WINDOW_MS = 100; |
| private static final int SCAN_MODE_LOW_LATENCY_INTERVAL_MS = 100; |
| public static final int SCAN_MODE_SCREEN_OFF_LOW_POWER_WINDOW_MS = 512; |
| public static final int SCAN_MODE_SCREEN_OFF_LOW_POWER_INTERVAL_MS = 10240; |
| public static final int SCAN_MODE_SCREEN_OFF_BALANCED_WINDOW_MS = 183; |
| public static final int SCAN_MODE_SCREEN_OFF_BALANCED_INTERVAL_MS = 730; |
| |
| // Result type defined in bt stack. Need to be accessed by GattService. |
| static final int SCAN_RESULT_TYPE_TRUNCATED = 1; |
| static final int SCAN_RESULT_TYPE_FULL = 2; |
| static final int SCAN_RESULT_TYPE_BOTH = 3; |
| |
| // Messages for handling BLE scan operations. |
| @VisibleForTesting |
| static final int MSG_START_BLE_SCAN = 0; |
| static final int MSG_STOP_BLE_SCAN = 1; |
| static final int MSG_FLUSH_BATCH_RESULTS = 2; |
| static final int MSG_SCAN_TIMEOUT = 3; |
| static final int MSG_SUSPEND_SCANS = 4; |
| static final int MSG_RESUME_SCANS = 5; |
| static final int MSG_IMPORTANCE_CHANGE = 6; |
| static final int MSG_SCREEN_ON = 7; |
| static final int MSG_SCREEN_OFF = 8; |
| static final int MSG_REVERT_SCAN_MODE_UPGRADE = 9; |
| static final int MSG_START_CONNECTING = 10; |
| static final int MSG_STOP_CONNECTING = 11; |
| private static final String ACTION_REFRESH_BATCHED_SCAN = |
| "com.android.bluetooth.gatt.REFRESH_BATCHED_SCAN"; |
| |
| // Timeout for each controller operation. |
| private static final int OPERATION_TIME_OUT_MILLIS = 500; |
| private static final int MAX_IS_UID_FOREGROUND_MAP_SIZE = 500; |
| |
| private int mLastConfiguredScanSetting = Integer.MIN_VALUE; |
| // Scan parameters for batch scan. |
| private BatchScanParams mBatchScanParms; |
| |
| private final Object mCurUsedTrackableAdvertisementsLock = new Object(); |
| @GuardedBy("mCurUsedTrackableAdvertisementsLock") |
| private int mCurUsedTrackableAdvertisements = 0; |
| private final GattService mService; |
| private final AdapterService mAdapterService; |
| private BroadcastReceiver mBatchAlarmReceiver; |
| private boolean mBatchAlarmReceiverRegistered; |
| private ScanNative mScanNative; |
| private volatile ClientHandler mHandler; |
| private BluetoothAdapterProxy mBluetoothAdapterProxy; |
| |
| private Set<ScanClient> mRegularScanClients; |
| private Set<ScanClient> mBatchClients; |
| private Set<ScanClient> mSuspendedScanClients; |
| private SparseIntArray mPriorityMap = new SparseIntArray(); |
| |
| private DisplayManager mDm; |
| |
| private ActivityManager mActivityManager; |
| private LocationManager mLocationManager; |
| private static final int FOREGROUND_IMPORTANCE_CUTOFF = |
| ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE; |
| private static final boolean DEFAULT_UID_IS_FOREGROUND = true; |
| private static final int SCAN_MODE_APP_IN_BACKGROUND = ScanSettings.SCAN_MODE_LOW_POWER; |
| private static final int SCAN_MODE_FORCE_DOWNGRADED = ScanSettings.SCAN_MODE_LOW_POWER; |
| private static final int SCAN_MODE_MAX_IN_CONCURRENCY = ScanSettings.SCAN_MODE_BALANCED; |
| private final SparseBooleanArray mIsUidForegroundMap = new SparseBooleanArray(); |
| private boolean mScreenOn = false; |
| private boolean mIsConnecting; |
| private int mProfilesConnecting, mProfilesConnected, mProfilesDisconnecting; |
| |
| @VisibleForTesting |
| static class UidImportance { |
| public int uid; |
| public int importance; |
| |
| UidImportance(int uid, int importance) { |
| this.uid = uid; |
| this.importance = importance; |
| } |
| } |
| |
| ScanManager(GattService service, AdapterService adapterService, |
| BluetoothAdapterProxy bluetoothAdapterProxy) { |
| mRegularScanClients = |
| Collections.newSetFromMap(new ConcurrentHashMap<ScanClient, Boolean>()); |
| mBatchClients = Collections.newSetFromMap(new ConcurrentHashMap<ScanClient, Boolean>()); |
| mSuspendedScanClients = |
| Collections.newSetFromMap(new ConcurrentHashMap<ScanClient, Boolean>()); |
| mService = service; |
| mAdapterService = adapterService; |
| mScanNative = new ScanNative(); |
| mDm = mService.getSystemService(DisplayManager.class); |
| mActivityManager = mService.getSystemService(ActivityManager.class); |
| mLocationManager = mAdapterService.getSystemService(LocationManager.class); |
| mBluetoothAdapterProxy = bluetoothAdapterProxy; |
| mIsConnecting = false; |
| |
| mPriorityMap.put(ScanSettings.SCAN_MODE_OPPORTUNISTIC, 0); |
| mPriorityMap.put(ScanSettings.SCAN_MODE_SCREEN_OFF, 1); |
| mPriorityMap.put(ScanSettings.SCAN_MODE_LOW_POWER, 2); |
| mPriorityMap.put(ScanSettings.SCAN_MODE_SCREEN_OFF_BALANCED, 3); |
| // BALANCED and AMBIENT_DISCOVERY now have the same settings and priority. |
| mPriorityMap.put(ScanSettings.SCAN_MODE_BALANCED, 4); |
| mPriorityMap.put(ScanSettings.SCAN_MODE_AMBIENT_DISCOVERY, 4); |
| mPriorityMap.put(ScanSettings.SCAN_MODE_LOW_LATENCY, 5); |
| } |
| |
| void start() { |
| HandlerThread thread = new HandlerThread("BluetoothScanManager"); |
| thread.start(); |
| mHandler = new ClientHandler(thread.getLooper()); |
| if (mDm != null) { |
| mDm.registerDisplayListener(mDisplayListener, null); |
| } |
| mScreenOn = isScreenOn(); |
| AppScanStats.initScanRadioState(); |
| AppScanStats.setScreenState(mScreenOn); |
| if (mActivityManager != null) { |
| mActivityManager.addOnUidImportanceListener(mUidImportanceListener, |
| FOREGROUND_IMPORTANCE_CUTOFF); |
| } |
| IntentFilter locationIntentFilter = new IntentFilter(LocationManager.MODE_CHANGED_ACTION); |
| locationIntentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); |
| mService.registerReceiver(mLocationReceiver, locationIntentFilter); |
| mService.registerReceiver(mBluetoothConnectionReceiver, |
| getBluetoothConnectionIntentFilter()); |
| } |
| |
| void cleanup() { |
| mRegularScanClients.clear(); |
| mBatchClients.clear(); |
| mSuspendedScanClients.clear(); |
| mScanNative.cleanup(); |
| |
| if (mActivityManager != null) { |
| try { |
| mActivityManager.removeOnUidImportanceListener(mUidImportanceListener); |
| } catch (IllegalArgumentException e) { |
| Log.w(TAG, "exception when invoking removeOnUidImportanceListener", e); |
| } |
| } |
| |
| if (mDm != null) { |
| mDm.unregisterDisplayListener(mDisplayListener); |
| } |
| |
| if (mHandler != null) { |
| // Shut down the thread |
| mHandler.removeCallbacksAndMessages(null); |
| Looper looper = mHandler.getLooper(); |
| if (looper != null) { |
| looper.quitSafely(); |
| } |
| mHandler = null; |
| } |
| |
| try { |
| mService.unregisterReceiver(mLocationReceiver); |
| } catch (IllegalArgumentException e) { |
| Log.w(TAG, "exception when invoking unregisterReceiver(mLocationReceiver)", e); |
| } |
| |
| try { |
| mService.unregisterReceiver(mBluetoothConnectionReceiver); |
| } catch (IllegalArgumentException e) { |
| Log.w(TAG, "exception when invoking unregisterReceiver(mBluetoothConnectionReceiver)", |
| e); |
| } |
| } |
| |
| void registerScanner(UUID uuid) { |
| mScanNative.registerScanner(uuid.getLeastSignificantBits(), |
| uuid.getMostSignificantBits()); |
| } |
| |
| void unregisterScanner(int scannerId) { |
| mScanNative.unregisterScanner(scannerId); |
| } |
| |
| /** |
| * Returns the regular scan queue. |
| */ |
| Set<ScanClient> getRegularScanQueue() { |
| return mRegularScanClients; |
| } |
| |
| /** |
| * Returns the suspended scan queue. |
| */ |
| Set<ScanClient> getSuspendedScanQueue() { |
| return mSuspendedScanClients; |
| } |
| |
| /** |
| * Returns batch scan queue. |
| */ |
| Set<ScanClient> getBatchScanQueue() { |
| return mBatchClients; |
| } |
| |
| /** |
| * Returns a set of full batch scan clients. |
| */ |
| Set<ScanClient> getFullBatchScanQueue() { |
| // TODO: split full batch scan clients and truncated batch clients so we don't need to |
| // construct this every time. |
| Set<ScanClient> fullBatchClients = new HashSet<ScanClient>(); |
| for (ScanClient client : mBatchClients) { |
| if (client.settings.getScanResultType() == ScanSettings.SCAN_RESULT_TYPE_FULL) { |
| fullBatchClients.add(client); |
| } |
| } |
| return fullBatchClients; |
| } |
| |
| void startScan(ScanClient client) { |
| if (DBG) { |
| Log.d(TAG, "startScan() " + client); |
| } |
| sendMessage(MSG_START_BLE_SCAN, client); |
| } |
| |
| void stopScan(int scannerId) { |
| ScanClient client = mScanNative.getBatchScanClient(scannerId); |
| if (client == null) { |
| client = mScanNative.getRegularScanClient(scannerId); |
| } |
| if (client == null) { |
| client = mScanNative.getSuspendedScanClient(scannerId); |
| } |
| sendMessage(MSG_STOP_BLE_SCAN, client); |
| } |
| |
| void flushBatchScanResults(ScanClient client) { |
| sendMessage(MSG_FLUSH_BATCH_RESULTS, client); |
| } |
| |
| void callbackDone(int scannerId, int status) { |
| mScanNative.callbackDone(scannerId, status); |
| } |
| |
| void onConnectingState(boolean isConnecting) { |
| if (isConnecting) { |
| sendMessage(MSG_START_CONNECTING, null); |
| } else { |
| sendMessage(MSG_STOP_CONNECTING, null); |
| } |
| } |
| |
| private IntentFilter getBluetoothConnectionIntentFilter() { |
| IntentFilter filter = new IntentFilter(); |
| filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); |
| filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); |
| filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED); |
| filter.addAction(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED); |
| filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); |
| filter.addAction(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED); |
| filter.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED); |
| return filter; |
| } |
| |
| private void sendMessage(int what, ScanClient client) { |
| final ClientHandler handler = mHandler; |
| if (handler == null) { |
| Log.d(TAG, "sendMessage: mHandler is null."); |
| return; |
| } |
| Message message = new Message(); |
| message.what = what; |
| message.obj = client; |
| handler.sendMessage(message); |
| } |
| |
| private boolean isFilteringSupported() { |
| if (mBluetoothAdapterProxy == null) { |
| Log.e(TAG, "mBluetoothAdapterProxy is null"); |
| return false; |
| } |
| return mBluetoothAdapterProxy.isOffloadedScanFilteringSupported(); |
| } |
| |
| boolean isAutoBatchScanClientEnabled(ScanClient client) { |
| return mScanNative.isAutoBatchScanClientEnabled(client); |
| } |
| |
| // Handler class that handles BLE scan operations. |
| private class ClientHandler extends Handler { |
| |
| ClientHandler(Looper looper) { |
| super(looper); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MSG_START_BLE_SCAN: |
| handleStartScan((ScanClient) msg.obj); |
| break; |
| case MSG_STOP_BLE_SCAN: |
| handleStopScan((ScanClient) msg.obj); |
| break; |
| case MSG_FLUSH_BATCH_RESULTS: |
| handleFlushBatchResults((ScanClient) msg.obj); |
| break; |
| case MSG_SCAN_TIMEOUT: |
| mScanNative.regularScanTimeout((ScanClient) msg.obj); |
| break; |
| case MSG_SUSPEND_SCANS: |
| handleSuspendScans(); |
| break; |
| case MSG_RESUME_SCANS: |
| handleResumeScans(); |
| break; |
| case MSG_SCREEN_OFF: |
| handleScreenOff(); |
| break; |
| case MSG_SCREEN_ON: |
| handleScreenOn(); |
| break; |
| case MSG_REVERT_SCAN_MODE_UPGRADE: |
| revertScanModeUpgrade((ScanClient) msg.obj); |
| break; |
| case MSG_IMPORTANCE_CHANGE: |
| handleImportanceChange((UidImportance) msg.obj); |
| break; |
| case MSG_START_CONNECTING: |
| handleConnectingState(); |
| break; |
| case MSG_STOP_CONNECTING: |
| handleClearConnectingState(); |
| break; |
| default: |
| // Shouldn't happen. |
| Log.e(TAG, "received an unkown message : " + msg.what); |
| } |
| } |
| |
| void handleStartScan(ScanClient client) { |
| if (DBG) { |
| Log.d(TAG, "handling starting scan"); |
| } |
| |
| if (!isScanSupported(client)) { |
| Log.e(TAG, "Scan settings not supported"); |
| return; |
| } |
| |
| if (mRegularScanClients.contains(client) || mBatchClients.contains(client)) { |
| Log.e(TAG, "Scan already started"); |
| return; |
| } |
| |
| if (requiresScreenOn(client) && !mScreenOn) { |
| Log.w(TAG, "Cannot start unfiltered scan in screen-off. This scan will be resumed " |
| + "later: " + client.scannerId); |
| mSuspendedScanClients.add(client); |
| if (client.stats != null) { |
| client.stats.recordScanSuspend(client.scannerId); |
| } |
| return; |
| } |
| |
| final boolean locationEnabled = mLocationManager.isLocationEnabled(); |
| if (requiresLocationOn(client) && !locationEnabled) { |
| Log.i(TAG, "Cannot start unfiltered scan in location-off. This scan will be" |
| + " resumed when location is on: " + client.scannerId); |
| mSuspendedScanClients.add(client); |
| if (client.stats != null) { |
| client.stats.recordScanSuspend(client.scannerId); |
| } |
| return; |
| } |
| |
| if (!mScanNative.isExemptFromAutoBatchScanUpdate(client)) { |
| if (mScreenOn) { |
| clearAutoBatchScanClient(client); |
| } else { |
| setAutoBatchScanClient(client); |
| } |
| } |
| |
| // Begin scan operations. |
| if (isBatchClient(client) || isAutoBatchScanClientEnabled(client)) { |
| mBatchClients.add(client); |
| mScanNative.startBatchScan(client); |
| } else { |
| updateScanModeBeforeStart(client); |
| updateScanModeConcurrency(client); |
| mRegularScanClients.add(client); |
| mScanNative.startRegularScan(client); |
| if (!mScanNative.isOpportunisticScanClient(client)) { |
| mScanNative.configureRegularScanParams(); |
| |
| if (!mScanNative.isExemptFromScanTimeout(client)) { |
| Message msg = obtainMessage(MSG_SCAN_TIMEOUT); |
| msg.obj = client; |
| // Only one timeout message should exist at any time |
| sendMessageDelayed(msg, mAdapterService.getScanTimeoutMillis()); |
| if (DBG) { |
| Log.d(TAG, |
| "apply scan timeout (" + mAdapterService.getScanTimeoutMillis() |
| + ")" + "to scannerId " + client.scannerId); |
| } |
| } |
| } |
| } |
| client.started = true; |
| } |
| |
| private boolean requiresScreenOn(ScanClient client) { |
| boolean isFiltered = isFilteredScan(client); |
| return !mScanNative.isOpportunisticScanClient(client) && !isFiltered; |
| } |
| |
| private boolean requiresLocationOn(ScanClient client) { |
| boolean isFiltered = isFilteredScan(client); |
| return !client.hasDisavowedLocation && !isFiltered; |
| } |
| |
| private boolean isFilteredScan(ScanClient client) { |
| if ((client.filters == null) || client.filters.isEmpty()) { |
| return false; |
| } |
| |
| boolean atLeastOneValidFilter = false; |
| for (ScanFilter filter : client.filters) { |
| // A valid filter need at least one field not empty |
| if (!filter.isAllFieldsEmpty()) { |
| atLeastOneValidFilter = true; |
| break; |
| } |
| } |
| return atLeastOneValidFilter; |
| } |
| |
| @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN) |
| void handleStopScan(ScanClient client) { |
| if (client == null) { |
| return; |
| } |
| if (DBG) { |
| Log.d(TAG, "handling stopping scan " + client); |
| } |
| |
| if (mSuspendedScanClients.contains(client)) { |
| mSuspendedScanClients.remove(client); |
| } |
| removeMessages(MSG_REVERT_SCAN_MODE_UPGRADE, client); |
| removeMessages(MSG_SCAN_TIMEOUT, client); |
| if (mRegularScanClients.contains(client)) { |
| mScanNative.stopRegularScan(client); |
| |
| if (!mScanNative.isOpportunisticScanClient(client)) { |
| mScanNative.configureRegularScanParams(); |
| } |
| } else { |
| if (isAutoBatchScanClientEnabled(client)) { |
| handleFlushBatchResults(client); |
| } |
| mScanNative.stopBatchScan(client); |
| } |
| if (client.appDied) { |
| if (DBG) { |
| Log.d(TAG, "app died, unregister scanner - " + client.scannerId); |
| } |
| mService.unregisterScanner(client.scannerId, mService.getAttributionSource()); |
| } |
| } |
| |
| void handleFlushBatchResults(ScanClient client) { |
| if (DBG) { |
| Log.d(TAG, "handleFlushBatchResults() " + client); |
| } |
| if (!mBatchClients.contains(client)) { |
| if (DBG) { |
| Log.d(TAG, "There is no batch scan client to flush " + client); |
| } |
| return; |
| } |
| mScanNative.flushBatchResults(client.scannerId); |
| } |
| |
| private boolean isBatchClient(ScanClient client) { |
| if (client == null || client.settings == null) { |
| return false; |
| } |
| ScanSettings settings = client.settings; |
| return settings.getCallbackType() == ScanSettings.CALLBACK_TYPE_ALL_MATCHES |
| && settings.getReportDelayMillis() != 0; |
| } |
| |
| private boolean isScanSupported(ScanClient client) { |
| if (client == null || client.settings == null) { |
| return true; |
| } |
| ScanSettings settings = client.settings; |
| if (isFilteringSupported()) { |
| return true; |
| } |
| return settings.getCallbackType() == ScanSettings.CALLBACK_TYPE_ALL_MATCHES |
| && settings.getReportDelayMillis() == 0; |
| } |
| |
| void handleScreenOff() { |
| AppScanStats.setScreenState(false); |
| if (!mScreenOn) { |
| return; |
| } |
| mScreenOn = false; |
| if (DBG) { |
| Log.d(TAG, "handleScreenOff()"); |
| } |
| handleSuspendScans(); |
| updateRegularScanClientsScreenOff(); |
| updateRegularScanToBatchScanClients(); |
| } |
| |
| void handleConnectingState() { |
| if (mAdapterService.getScanDowngradeDurationMillis() == 0) { |
| return; |
| } |
| boolean updatedScanParams = false; |
| mIsConnecting = true; |
| if (DBG) { |
| Log.d(TAG, "handleConnectingState()"); |
| } |
| for (ScanClient client : mRegularScanClients) { |
| if (downgradeScanModeFromMaxDuty(client)) { |
| updatedScanParams = true; |
| if (DBG) { |
| Log.d(TAG, "scanMode is downgraded by connecting for " + client); |
| } |
| } |
| } |
| if (updatedScanParams) { |
| mScanNative.configureRegularScanParams(); |
| } |
| removeMessages(MSG_STOP_CONNECTING); |
| Message msg = obtainMessage(MSG_STOP_CONNECTING); |
| sendMessageDelayed(msg, mAdapterService.getScanDowngradeDurationMillis()); |
| } |
| |
| void handleClearConnectingState() { |
| if (!mIsConnecting) { |
| Log.e(TAG, "handleClearConnectingState() - not connecting state"); |
| return; |
| } |
| if (DBG) { |
| Log.d(TAG, "handleClearConnectingState()"); |
| } |
| boolean updatedScanParams = false; |
| for (ScanClient client : mRegularScanClients) { |
| if (revertDowngradeScanModeFromMaxDuty(client)) { |
| updatedScanParams = true; |
| if (DBG) { |
| Log.d(TAG, "downgraded scanMode is reverted for " + client); |
| } |
| } |
| } |
| if (updatedScanParams) { |
| mScanNative.configureRegularScanParams(); |
| } |
| removeMessages(MSG_STOP_CONNECTING); |
| mIsConnecting = false; |
| } |
| |
| @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN) |
| void handleSuspendScans() { |
| for (ScanClient client : mRegularScanClients) { |
| if ((requiresScreenOn(client) && !mScreenOn) |
| || (requiresLocationOn(client) && !mLocationManager.isLocationEnabled())) { |
| /*Suspend unfiltered scans*/ |
| if (client.stats != null) { |
| client.stats.recordScanSuspend(client.scannerId); |
| } |
| if (DBG) { |
| Log.d(TAG, "suspend scan " + client); |
| } |
| handleStopScan(client); |
| mSuspendedScanClients.add(client); |
| } |
| } |
| } |
| |
| private void updateRegularScanToBatchScanClients() { |
| boolean updatedScanParams = false; |
| for (ScanClient client : mRegularScanClients) { |
| if (!mScanNative.isExemptFromAutoBatchScanUpdate(client)) { |
| if (DBG) { |
| Log.d(TAG, "Updating regular scan to batch scan" + client); |
| } |
| handleStopScan(client); |
| setAutoBatchScanClient(client); |
| handleStartScan(client); |
| updatedScanParams = true; |
| } |
| } |
| if (updatedScanParams) { |
| mScanNative.configureRegularScanParams(); |
| } |
| } |
| |
| private void updateBatchScanToRegularScanClients() { |
| boolean updatedScanParams = false; |
| for (ScanClient client : mBatchClients) { |
| if (!mScanNative.isExemptFromAutoBatchScanUpdate(client)) { |
| if (DBG) { |
| Log.d(TAG, "Updating batch scan to regular scan" + client); |
| } |
| handleStopScan(client); |
| clearAutoBatchScanClient(client); |
| handleStartScan(client); |
| updatedScanParams = true; |
| } |
| } |
| if (updatedScanParams) { |
| mScanNative.configureRegularScanParams(); |
| } |
| } |
| |
| private void setAutoBatchScanClient(ScanClient client) { |
| if (isAutoBatchScanClientEnabled(client)) { |
| return; |
| } |
| client.updateScanMode(ScanSettings.SCAN_MODE_SCREEN_OFF); |
| if (client.stats != null) { |
| client.stats.setAutoBatchScan(client.scannerId, true); |
| } |
| } |
| |
| private void clearAutoBatchScanClient(ScanClient client) { |
| if (!isAutoBatchScanClientEnabled(client)) { |
| return; |
| } |
| client.updateScanMode(client.scanModeApp); |
| if (client.stats != null) { |
| client.stats.setAutoBatchScan(client.scannerId, false); |
| } |
| } |
| |
| private void updateRegularScanClientsScreenOff() { |
| boolean updatedScanParams = false; |
| for (ScanClient client : mRegularScanClients) { |
| if (updateScanModeScreenOff(client)) { |
| updatedScanParams = true; |
| if (DBG) { |
| Log.d(TAG, "Scan mode update during screen off" + client); |
| } |
| } |
| } |
| if (updatedScanParams) { |
| mScanNative.configureRegularScanParams(); |
| } |
| } |
| |
| private boolean updateScanModeScreenOff(ScanClient client) { |
| if (mScanNative.isOpportunisticScanClient(client)) { |
| return false; |
| } |
| if (!isAppForeground(client) || mScanNative.isForceDowngradedScanClient(client)) { |
| return client.updateScanMode(ScanSettings.SCAN_MODE_SCREEN_OFF); |
| } |
| |
| // The following codes are effectively only for services |
| // Apps are either already or will be soon handled by handleImportanceChange(). |
| switch (client.scanModeApp) { |
| case ScanSettings.SCAN_MODE_LOW_POWER: |
| return client.updateScanMode(ScanSettings.SCAN_MODE_SCREEN_OFF); |
| case ScanSettings.SCAN_MODE_BALANCED: |
| case ScanSettings.SCAN_MODE_AMBIENT_DISCOVERY: |
| return client.updateScanMode(ScanSettings.SCAN_MODE_SCREEN_OFF_BALANCED); |
| case ScanSettings.SCAN_MODE_LOW_LATENCY: |
| return client.updateScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY); |
| case ScanSettings.SCAN_MODE_OPPORTUNISTIC: |
| default: |
| return false; |
| } |
| } |
| |
| /** |
| * Services and Apps are assumed to be in the foreground by default unless it changes to the |
| * background triggering onUidImportance(). |
| */ |
| private boolean isAppForeground(ScanClient client) { |
| return mIsUidForegroundMap.get(client.appUid, DEFAULT_UID_IS_FOREGROUND); |
| } |
| |
| private boolean updateScanModeBeforeStart(ScanClient client) { |
| if (upgradeScanModeBeforeStart(client)) { |
| return true; |
| } |
| if (mScreenOn) { |
| return updateScanModeScreenOn(client); |
| } else { |
| return updateScanModeScreenOff(client); |
| } |
| } |
| |
| private boolean updateScanModeConcurrency(ScanClient client) { |
| if (mIsConnecting) { |
| return downgradeScanModeFromMaxDuty(client); |
| } |
| return false; |
| } |
| |
| private boolean upgradeScanModeBeforeStart(ScanClient client) { |
| if (client.started || mAdapterService.getScanUpgradeDurationMillis() == 0) { |
| return false; |
| } |
| if (client.stats == null || client.stats.hasRecentScan()) { |
| return false; |
| } |
| if (!isAppForeground(client) || isBatchClient(client)) { |
| return false; |
| } |
| |
| if (upgradeScanModeByOneLevel(client)) { |
| Message msg = obtainMessage(MSG_REVERT_SCAN_MODE_UPGRADE); |
| msg.obj = client; |
| if (DBG) { |
| Log.d(TAG, "scanMode is upgraded for " + client); |
| } |
| sendMessageDelayed(msg, mAdapterService.getScanUpgradeDurationMillis()); |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean upgradeScanModeByOneLevel(ScanClient client) { |
| switch (client.scanModeApp) { |
| case ScanSettings.SCAN_MODE_LOW_POWER: |
| return client.updateScanMode(ScanSettings.SCAN_MODE_BALANCED); |
| case ScanSettings.SCAN_MODE_BALANCED: |
| case ScanSettings.SCAN_MODE_AMBIENT_DISCOVERY: |
| return client.updateScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY); |
| case ScanSettings.SCAN_MODE_OPPORTUNISTIC: |
| case ScanSettings.SCAN_MODE_LOW_LATENCY: |
| default: |
| return false; |
| } |
| } |
| |
| void revertScanModeUpgrade(ScanClient client) { |
| if (mPriorityMap.get(client.settings.getScanMode()) |
| <= mPriorityMap.get(client.scanModeApp)) { |
| return; |
| } |
| if (client.updateScanMode(client.scanModeApp)) { |
| if (DBG) { |
| Log.d(TAG, "scanMode upgrade is reverted for " + client); |
| } |
| mScanNative.configureRegularScanParams(); |
| } |
| } |
| |
| private boolean updateScanModeScreenOn(ScanClient client) { |
| if (mScanNative.isOpportunisticScanClient(client)) { |
| return false; |
| } |
| int scanMode = |
| isAppForeground(client) ? client.scanModeApp : SCAN_MODE_APP_IN_BACKGROUND; |
| int maxScanMode = |
| mScanNative.isForceDowngradedScanClient(client) |
| ? SCAN_MODE_FORCE_DOWNGRADED |
| : scanMode; |
| return client.updateScanMode(getMinScanMode(scanMode, maxScanMode)); |
| } |
| |
| private boolean downgradeScanModeFromMaxDuty(ScanClient client) { |
| if ((client.stats == null) || mAdapterService.getScanDowngradeDurationMillis() == 0) { |
| return false; |
| } |
| int scanMode = client.settings.getScanMode(); |
| int maxScanMode = SCAN_MODE_MAX_IN_CONCURRENCY; |
| if (client.updateScanMode(getMinScanMode(scanMode, maxScanMode))) { |
| client.stats.setScanDowngrade(client.scannerId, true); |
| if (DBG) { |
| Log.d(TAG, "downgradeScanModeFromMaxDuty() for " + client); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean revertDowngradeScanModeFromMaxDuty(ScanClient client) { |
| if (!mScanNative.isDowngradedScanClient(client)) { |
| return false; |
| } |
| client.stats.setScanDowngrade(client.scannerId, false); |
| if (DBG) { |
| Log.d(TAG, "revertDowngradeScanModeFromMaxDuty() for " + client); |
| } |
| if (mScreenOn) { |
| return updateScanModeScreenOn(client); |
| } else { |
| return updateScanModeScreenOff(client); |
| } |
| } |
| |
| void handleScreenOn() { |
| AppScanStats.setScreenState(true); |
| if (mScreenOn) { |
| return; |
| } |
| mScreenOn = true; |
| if (DBG) { |
| Log.d(TAG, "handleScreenOn()"); |
| } |
| updateBatchScanToRegularScanClients(); |
| handleResumeScans(); |
| updateRegularScanClientsScreenOn(); |
| } |
| |
| void handleResumeScans() { |
| Iterator<ScanClient> iterator = mSuspendedScanClients.iterator(); |
| while (iterator.hasNext()) { |
| ScanClient client = iterator.next(); |
| if ((!requiresScreenOn(client) || mScreenOn) |
| && (!requiresLocationOn(client) || mLocationManager.isLocationEnabled())) { |
| if (client.stats != null) { |
| client.stats.recordScanResume(client.scannerId); |
| } |
| if (DBG) { |
| Log.d(TAG, "resume scan " + client); |
| } |
| handleStartScan(client); |
| iterator.remove(); |
| } |
| } |
| } |
| |
| private void updateRegularScanClientsScreenOn() { |
| boolean updatedScanParams = false; |
| for (ScanClient client : mRegularScanClients) { |
| if (updateScanModeScreenOn(client)) { |
| updatedScanParams = true; |
| } |
| } |
| if (updatedScanParams) { |
| mScanNative.configureRegularScanParams(); |
| } |
| } |
| } |
| |
| /** |
| * Parameters for batch scans. |
| */ |
| class BatchScanParams { |
| public int scanMode; |
| public int fullScanscannerId; |
| public int truncatedScanscannerId; |
| |
| BatchScanParams() { |
| scanMode = -1; |
| fullScanscannerId = -1; |
| truncatedScanscannerId = -1; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (obj == null || getClass() != obj.getClass()) { |
| return false; |
| } |
| BatchScanParams other = (BatchScanParams) obj; |
| return scanMode == other.scanMode && fullScanscannerId == other.fullScanscannerId |
| && truncatedScanscannerId == other.truncatedScanscannerId; |
| |
| } |
| } |
| |
| public int getCurrentUsedTrackingAdvertisement() { |
| synchronized (mCurUsedTrackableAdvertisementsLock) { |
| return mCurUsedTrackableAdvertisements; |
| } |
| } |
| |
| private class ScanNative { |
| |
| // Delivery mode defined in bt stack. |
| private static final int DELIVERY_MODE_IMMEDIATE = 0; |
| private static final int DELIVERY_MODE_ON_FOUND_LOST = 1; |
| private static final int DELIVERY_MODE_BATCH = 2; |
| |
| private static final int ONFOUND_SIGHTINGS_AGGRESSIVE = 1; |
| private static final int ONFOUND_SIGHTINGS_STICKY = 4; |
| |
| private static final int ALL_PASS_FILTER_INDEX_REGULAR_SCAN = 1; |
| private static final int ALL_PASS_FILTER_INDEX_BATCH_SCAN = 2; |
| private static final int ALL_PASS_FILTER_SELECTION = 0; |
| |
| private static final int DISCARD_OLDEST_WHEN_BUFFER_FULL = 0; |
| |
| |
| /** |
| * Onfound/onlost for scan settings |
| */ |
| private static final int MATCH_MODE_AGGRESSIVE_TIMEOUT_FACTOR = (1); |
| private static final int MATCH_MODE_STICKY_TIMEOUT_FACTOR = (3); |
| private static final int ONLOST_FACTOR = 2; |
| private static final int ONLOST_ONFOUND_BASE_TIMEOUT_MS = 500; |
| |
| // The logic is AND for each filter field. |
| private static final int LIST_LOGIC_TYPE = 0x1111111; |
| private static final int FILTER_LOGIC_TYPE = 1; |
| // Filter indices that are available to user. It's sad we need to maintain filter index. |
| private final Deque<Integer> mFilterIndexStack; |
| // Map of scannerId and Filter indices used by client. |
| private final Map<Integer, Deque<Integer>> mClientFilterIndexMap; |
| // Keep track of the clients that uses ALL_PASS filters. |
| private final Set<Integer> mAllPassRegularClients = new HashSet<>(); |
| private final Set<Integer> mAllPassBatchClients = new HashSet<>(); |
| |
| private AlarmManager mAlarmManager; |
| private PendingIntent mBatchScanIntervalIntent; |
| private ScanNativeInterface mNativeInterface; |
| |
| ScanNative() { |
| mNativeInterface = GattObjectsFactory.getInstance().getScanNativeInterface(); |
| mFilterIndexStack = new ArrayDeque<Integer>(); |
| mClientFilterIndexMap = new HashMap<Integer, Deque<Integer>>(); |
| |
| mAlarmManager = mService.getSystemService(AlarmManager.class); |
| Intent batchIntent = new Intent(ACTION_REFRESH_BATCHED_SCAN, null); |
| mBatchScanIntervalIntent = PendingIntent.getBroadcast(mService, 0, batchIntent, |
| PendingIntent.FLAG_IMMUTABLE); |
| IntentFilter filter = new IntentFilter(); |
| filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); |
| filter.addAction(ACTION_REFRESH_BATCHED_SCAN); |
| mBatchAlarmReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| Log.d(TAG, "awakened up at time " + SystemClock.elapsedRealtime()); |
| String action = intent.getAction(); |
| |
| if (action.equals(ACTION_REFRESH_BATCHED_SCAN)) { |
| if (mBatchClients.isEmpty()) { |
| return; |
| } |
| // Note this actually flushes all pending batch data. |
| if (mBatchClients.iterator().hasNext()) { |
| flushBatchScanResults(mBatchClients.iterator().next()); |
| } |
| } |
| } |
| }; |
| mService.registerReceiver(mBatchAlarmReceiver, filter); |
| mBatchAlarmReceiverRegistered = true; |
| } |
| |
| private void callbackDone(int scannerId, int status) { |
| if (DBG) { |
| Log.d(TAG, "callback done for scannerId - " + scannerId + " status - " + status); |
| } |
| if (status == 0) { |
| mNativeInterface.callbackDone(); |
| } |
| // TODO: add a callback for scan failure. |
| } |
| |
| private void resetCountDownLatch() { |
| mNativeInterface.resetCountDownLatch(); |
| } |
| |
| private boolean waitForCallback() { |
| return mNativeInterface.waitForCallback(OPERATION_TIME_OUT_MILLIS); |
| } |
| |
| void configureRegularScanParams() { |
| if (DBG) { |
| Log.d(TAG, "configureRegularScanParams() - queue=" + mRegularScanClients.size()); |
| } |
| int curScanSetting = Integer.MIN_VALUE; |
| ScanClient client = getAggressiveClient(mRegularScanClients); |
| if (client != null) { |
| curScanSetting = client.settings.getScanMode(); |
| } |
| |
| if (curScanSetting != Integer.MIN_VALUE |
| && curScanSetting != ScanSettings.SCAN_MODE_OPPORTUNISTIC) { |
| if (curScanSetting != mLastConfiguredScanSetting) { |
| int scanWindowMs = getScanWindowMillis(client.settings); |
| int scanIntervalMs = getScanIntervalMillis(client.settings); |
| |
| // convert scanWindow and scanInterval from ms to LE scan units(0.625ms) |
| int scanWindow = Utils.millsToUnit(scanWindowMs); |
| int scanInterval = Utils.millsToUnit(scanIntervalMs); |
| mNativeInterface.gattClientScan(false); |
| if (!AppScanStats.recordScanRadioStop()) { |
| Log.w(TAG, "There is no scan radio to stop"); |
| } |
| if (DBG) { |
| Log.d(TAG, "Start gattClientScanNative with" |
| + " old scanMode " + mLastConfiguredScanSetting |
| + " new scanMode " + curScanSetting |
| + " ( in MS: " + scanIntervalMs + " / " + scanWindowMs |
| + ", in scan unit: " + scanInterval + " / " + scanWindow + " )" |
| + client); |
| } |
| mNativeInterface.gattSetScanParameters(client.scannerId, scanInterval, |
| scanWindow); |
| mNativeInterface.gattClientScan(true); |
| if (!AppScanStats.recordScanRadioStart(curScanSetting)) { |
| Log.w(TAG, "Scan radio already started"); |
| } |
| mLastConfiguredScanSetting = curScanSetting; |
| } |
| } else { |
| mLastConfiguredScanSetting = curScanSetting; |
| if (DBG) { |
| Log.d(TAG, "configureRegularScanParams() - queue empty, scan stopped"); |
| } |
| } |
| } |
| |
| ScanClient getAggressiveClient(Set<ScanClient> cList) { |
| ScanClient result = null; |
| int currentScanModePriority = Integer.MIN_VALUE; |
| for (ScanClient client : cList) { |
| int priority = mPriorityMap.get(client.settings.getScanMode()); |
| if (priority > currentScanModePriority) { |
| result = client; |
| currentScanModePriority = priority; |
| } |
| } |
| return result; |
| } |
| |
| void startRegularScan(ScanClient client) { |
| if (isFilteringSupported() && mFilterIndexStack.isEmpty() |
| && mClientFilterIndexMap.isEmpty()) { |
| initFilterIndexStack(); |
| } |
| if (isFilteringSupported()) { |
| configureScanFilters(client); |
| } |
| // Start scan native only for the first client. |
| if (numRegularScanClients() == 1 |
| && client.settings != null |
| && client.settings.getScanMode() != ScanSettings.SCAN_MODE_OPPORTUNISTIC) { |
| if (DBG) { |
| Log.d(TAG, "start gattClientScanNative from startRegularScan()"); |
| } |
| mNativeInterface.gattClientScan(true); |
| if (!AppScanStats.recordScanRadioStart(client.settings.getScanMode())) { |
| Log.w(TAG, "Scan radio already started"); |
| } |
| } |
| } |
| |
| private int numRegularScanClients() { |
| int num = 0; |
| for (ScanClient client : mRegularScanClients) { |
| if (client.settings.getScanMode() != ScanSettings.SCAN_MODE_OPPORTUNISTIC) { |
| num++; |
| } |
| } |
| return num; |
| } |
| |
| void startBatchScan(ScanClient client) { |
| if (mFilterIndexStack.isEmpty() && isFilteringSupported()) { |
| initFilterIndexStack(); |
| } |
| configureScanFilters(client); |
| if (!isOpportunisticScanClient(client)) { |
| // Reset batch scan. May need to stop the existing batch scan and update scan |
| // params. |
| resetBatchScan(client); |
| } |
| } |
| |
| private boolean isExemptFromScanTimeout(ScanClient client) { |
| return isOpportunisticScanClient(client) || isFirstMatchScanClient(client); |
| } |
| |
| private boolean isExemptFromAutoBatchScanUpdate(ScanClient client) { |
| return isOpportunisticScanClient(client) || !isAllMatchesAutoBatchScanClient(client); |
| } |
| |
| private boolean isAutoBatchScanClientEnabled(ScanClient client) { |
| return client.stats != null && client.stats.isAutoBatchScan(client.scannerId); |
| } |
| |
| private boolean isAllMatchesAutoBatchScanClient(ScanClient client) { |
| return client.settings.getCallbackType() |
| == ScanSettings.CALLBACK_TYPE_ALL_MATCHES_AUTO_BATCH; |
| } |
| |
| private boolean isOpportunisticScanClient(ScanClient client) { |
| return client.settings.getScanMode() == ScanSettings.SCAN_MODE_OPPORTUNISTIC; |
| } |
| |
| private boolean isTimeoutScanClient(ScanClient client) { |
| return (client.stats != null) && client.stats.isScanTimeout(client.scannerId); |
| } |
| |
| private boolean isDowngradedScanClient(ScanClient client) { |
| return (client.stats != null) && client.stats.isScanDowngraded(client.scannerId); |
| } |
| |
| private boolean isForceDowngradedScanClient(ScanClient client) { |
| return isTimeoutScanClient(client) || isDowngradedScanClient(client); |
| } |
| |
| private boolean isFirstMatchScanClient(ScanClient client) { |
| return (client.settings.getCallbackType() & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) |
| != 0; |
| } |
| |
| private void resetBatchScan(ScanClient client) { |
| int scannerId = client.scannerId; |
| BatchScanParams batchScanParams = getBatchScanParams(); |
| // Stop batch if batch scan params changed and previous params is not null. |
| if (mBatchScanParms != null && (!mBatchScanParms.equals(batchScanParams))) { |
| if (DBG) { |
| Log.d(TAG, "stopping BLe Batch"); |
| } |
| resetCountDownLatch(); |
| mNativeInterface.gattClientStopBatchScan(scannerId); |
| waitForCallback(); |
| // Clear pending results as it's illegal to config storage if there are still |
| // pending results. |
| flushBatchResults(scannerId); |
| } |
| // Start batch if batchScanParams changed and current params is not null. |
| if (batchScanParams != null && (!batchScanParams.equals(mBatchScanParms))) { |
| int notifyThreshold = 95; |
| if (DBG) { |
| Log.d(TAG, "Starting BLE batch scan"); |
| } |
| int resultType = getResultType(batchScanParams); |
| int fullScanPercent = getFullScanStoragePercent(resultType); |
| resetCountDownLatch(); |
| if (DBG) { |
| Log.d(TAG, "configuring batch scan storage, appIf " + client.scannerId); |
| } |
| mNativeInterface.gattClientConfigBatchScanStorage(client.scannerId, fullScanPercent, |
| 100 - fullScanPercent, notifyThreshold); |
| waitForCallback(); |
| resetCountDownLatch(); |
| int scanInterval = |
| Utils.millsToUnit(getBatchScanIntervalMillis(batchScanParams.scanMode)); |
| int scanWindow = |
| Utils.millsToUnit(getBatchScanWindowMillis(batchScanParams.scanMode)); |
| mNativeInterface.gattClientStartBatchScan(scannerId, resultType, scanInterval, |
| scanWindow, 0, DISCARD_OLDEST_WHEN_BUFFER_FULL); |
| waitForCallback(); |
| } |
| mBatchScanParms = batchScanParams; |
| setBatchAlarm(); |
| } |
| |
| private int getFullScanStoragePercent(int resultType) { |
| switch (resultType) { |
| case SCAN_RESULT_TYPE_FULL: |
| return 100; |
| case SCAN_RESULT_TYPE_TRUNCATED: |
| return 0; |
| case SCAN_RESULT_TYPE_BOTH: |
| return 50; |
| default: |
| return 50; |
| } |
| } |
| |
| private BatchScanParams getBatchScanParams() { |
| if (mBatchClients.isEmpty()) { |
| return null; |
| } |
| BatchScanParams params = new BatchScanParams(); |
| ScanClient winner = getAggressiveClient(mBatchClients); |
| if (winner != null) { |
| params.scanMode = winner.settings.getScanMode(); |
| } |
| // TODO: split full batch scan results and truncated batch scan results to different |
| // collections. |
| for (ScanClient client : mBatchClients) { |
| if (client.settings.getScanResultType() == ScanSettings.SCAN_RESULT_TYPE_FULL) { |
| params.fullScanscannerId = client.scannerId; |
| } else { |
| params.truncatedScanscannerId = client.scannerId; |
| } |
| } |
| return params; |
| } |
| |
| // Batched scan doesn't require high duty cycle scan because scan result is reported |
| // infrequently anyway. To avoid redefining paramete sets, map to the low duty cycle |
| // parameter set as follows. |
| private int getBatchScanWindowMillis(int scanMode) { |
| ContentResolver resolver = mService.getContentResolver(); |
| switch (scanMode) { |
| case ScanSettings.SCAN_MODE_LOW_LATENCY: |
| return Settings.Global.getInt( |
| resolver, |
| Settings.Global.BLE_SCAN_BALANCED_WINDOW_MS, |
| SCAN_MODE_BALANCED_WINDOW_MS); |
| case ScanSettings.SCAN_MODE_SCREEN_OFF: |
| return mAdapterService.getScreenOffLowPowerWindowMillis(); |
| default: |
| return Settings.Global.getInt( |
| resolver, |
| Settings.Global.BLE_SCAN_LOW_POWER_WINDOW_MS, |
| SCAN_MODE_LOW_POWER_WINDOW_MS); |
| } |
| } |
| |
| private int getBatchScanIntervalMillis(int scanMode) { |
| ContentResolver resolver = mService.getContentResolver(); |
| switch (scanMode) { |
| case ScanSettings.SCAN_MODE_LOW_LATENCY: |
| return Settings.Global.getInt( |
| resolver, |
| Settings.Global.BLE_SCAN_BALANCED_INTERVAL_MS, |
| SCAN_MODE_BALANCED_INTERVAL_MS); |
| case ScanSettings.SCAN_MODE_SCREEN_OFF: |
| return mAdapterService.getScreenOffLowPowerIntervalMillis(); |
| default: |
| return Settings.Global.getInt( |
| resolver, |
| Settings.Global.BLE_SCAN_LOW_POWER_INTERVAL_MS, |
| SCAN_MODE_LOW_POWER_INTERVAL_MS); |
| } |
| } |
| |
| // Set the batch alarm to be triggered within a short window after batch interval. This |
| // allows system to optimize wake up time while still allows a degree of precise control. |
| private void setBatchAlarm() { |
| // Cancel any pending alarm just in case. |
| mAlarmManager.cancel(mBatchScanIntervalIntent); |
| if (mBatchClients.isEmpty()) { |
| return; |
| } |
| long batchTriggerIntervalMillis = getBatchTriggerIntervalMillis(); |
| // Allows the alarm to be triggered within |
| // [batchTriggerIntervalMillis, 1.1 * batchTriggerIntervalMillis] |
| long windowLengthMillis = batchTriggerIntervalMillis / 10; |
| long windowStartMillis = SystemClock.elapsedRealtime() + batchTriggerIntervalMillis; |
| mAlarmManager.setWindow(AlarmManager.ELAPSED_REALTIME_WAKEUP, windowStartMillis, |
| windowLengthMillis, mBatchScanIntervalIntent); |
| } |
| |
| void stopRegularScan(ScanClient client) { |
| // Remove scan filters and recycle filter indices. |
| if (client == null) { |
| return; |
| } |
| int deliveryMode = getDeliveryMode(client); |
| if (deliveryMode == DELIVERY_MODE_ON_FOUND_LOST) { |
| for (ScanFilter filter : client.filters) { |
| int entriesToFree = getNumOfTrackingAdvertisements(client.settings); |
| if (!manageAllocationOfTrackingAdvertisement(entriesToFree, false)) { |
| Log.e(TAG, "Error freeing for onfound/onlost filter resources " |
| + entriesToFree); |
| try { |
| mService.onScanManagerErrorCallback(client.scannerId, |
| ScanCallback.SCAN_FAILED_INTERNAL_ERROR); |
| } catch (RemoteException e) { |
| Log.e(TAG, "failed on onScanManagerCallback at freeing", e); |
| } |
| } |
| } |
| } |
| mRegularScanClients.remove(client); |
| if (numRegularScanClients() == 0) { |
| if (DBG) { |
| Log.d(TAG, "stop gattClientScanNative"); |
| } |
| mNativeInterface.gattClientScan(false); |
| if (!AppScanStats.recordScanRadioStop()) { |
| Log.w(TAG, "There is no scan radio to stop"); |
| } |
| } |
| removeScanFilters(client.scannerId); |
| } |
| |
| void regularScanTimeout(ScanClient client) { |
| if (!isExemptFromScanTimeout(client) && client.stats.isScanningTooLong()) { |
| if (DBG) { |
| Log.d(TAG, "regularScanTimeout - client scan time was too long"); |
| } |
| if (client.filters == null || client.filters.isEmpty()) { |
| Log.w(TAG, |
| "Moving unfiltered scan client to opportunistic scan (scannerId " |
| + client.scannerId + ")"); |
| setOpportunisticScanClient(client); |
| removeScanFilters(client.scannerId); |
| |
| } else { |
| Log.w( |
| TAG, |
| "Moving filtered scan client to downgraded scan (scannerId " |
| + client.scannerId |
| + ")"); |
| int scanMode = client.settings.getScanMode(); |
| int maxScanMode = SCAN_MODE_FORCE_DOWNGRADED; |
| client.updateScanMode(getMinScanMode(scanMode, maxScanMode)); |
| } |
| client.stats.setScanTimeout(client.scannerId); |
| client.stats.recordScanTimeoutCountMetrics(); |
| } |
| |
| // The scan should continue for background scans |
| configureRegularScanParams(); |
| if (numRegularScanClients() == 0) { |
| if (DBG) { |
| Log.d(TAG, "stop gattClientScanNative"); |
| } |
| mNativeInterface.gattClientScan(false); |
| if (!AppScanStats.recordScanRadioStop()) { |
| Log.w(TAG, "There is no scan radio to stop"); |
| } |
| } |
| } |
| |
| void setOpportunisticScanClient(ScanClient client) { |
| // TODO: Add constructor to ScanSettings.Builder |
| // that can copy values from an existing ScanSettings object |
| ScanSettings.Builder builder = new ScanSettings.Builder(); |
| ScanSettings settings = client.settings; |
| builder.setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC); |
| builder.setCallbackType(settings.getCallbackType()); |
| builder.setScanResultType(settings.getScanResultType()); |
| builder.setReportDelay(settings.getReportDelayMillis()); |
| builder.setNumOfMatches(settings.getNumOfMatches()); |
| client.settings = builder.build(); |
| } |
| |
| // Find the regular scan client information. |
| ScanClient getRegularScanClient(int scannerId) { |
| for (ScanClient client : mRegularScanClients) { |
| if (client.scannerId == scannerId) { |
| return client; |
| } |
| } |
| return null; |
| } |
| |
| ScanClient getSuspendedScanClient(int scannerId) { |
| for (ScanClient client : mSuspendedScanClients) { |
| if (client.scannerId == scannerId) { |
| return client; |
| } |
| } |
| return null; |
| } |
| |
| void stopBatchScan(ScanClient client) { |
| mBatchClients.remove(client); |
| removeScanFilters(client.scannerId); |
| if (!isOpportunisticScanClient(client)) { |
| resetBatchScan(client); |
| } |
| } |
| |
| void flushBatchResults(int scannerId) { |
| if (DBG) { |
| Log.d(TAG, "flushPendingBatchResults - scannerId = " + scannerId); |
| } |
| if (mBatchScanParms.fullScanscannerId != -1) { |
| resetCountDownLatch(); |
| mNativeInterface.gattClientReadScanReports(mBatchScanParms.fullScanscannerId, |
| SCAN_RESULT_TYPE_FULL); |
| waitForCallback(); |
| } |
| if (mBatchScanParms.truncatedScanscannerId != -1) { |
| resetCountDownLatch(); |
| mNativeInterface.gattClientReadScanReports(mBatchScanParms.truncatedScanscannerId, |
| SCAN_RESULT_TYPE_TRUNCATED); |
| waitForCallback(); |
| } |
| setBatchAlarm(); |
| } |
| |
| void cleanup() { |
| mAlarmManager.cancel(mBatchScanIntervalIntent); |
| // Protect against multiple calls of cleanup. |
| if (mBatchAlarmReceiverRegistered) { |
| mService.unregisterReceiver(mBatchAlarmReceiver); |
| } |
| mBatchAlarmReceiverRegistered = false; |
| } |
| |
| private long getBatchTriggerIntervalMillis() { |
| long intervalMillis = Long.MAX_VALUE; |
| for (ScanClient client : mBatchClients) { |
| if (client.settings != null && client.settings.getReportDelayMillis() > 0) { |
| intervalMillis = |
| Math.min(intervalMillis, client.settings.getReportDelayMillis()); |
| } |
| } |
| return intervalMillis; |
| } |
| |
| // Add scan filters. The logic is: |
| // If no offload filter can/needs to be set, set ALL_PASS filter. |
| // Otherwise offload all filters to hardware and enable all filters. |
| private void configureScanFilters(ScanClient client) { |
| int scannerId = client.scannerId; |
| int deliveryMode = getDeliveryMode(client); |
| int trackEntries = 0; |
| |
| // Do not add any filters set by opportunistic scan clients |
| if (isOpportunisticScanClient(client)) { |
| return; |
| } |
| |
| if (!shouldAddAllPassFilterToController(client, deliveryMode)) { |
| return; |
| } |
| |
| resetCountDownLatch(); |
| mNativeInterface.gattClientScanFilterEnable(scannerId, true); |
| waitForCallback(); |
| |
| if (shouldUseAllPassFilter(client)) { |
| int filterIndex = |
| (deliveryMode == DELIVERY_MODE_BATCH) ? ALL_PASS_FILTER_INDEX_BATCH_SCAN |
| : ALL_PASS_FILTER_INDEX_REGULAR_SCAN; |
| resetCountDownLatch(); |
| // Don't allow Onfound/onlost with all pass |
| configureFilterParamter(scannerId, client, ALL_PASS_FILTER_SELECTION, filterIndex, |
| 0); |
| waitForCallback(); |
| } else { |
| Deque<Integer> clientFilterIndices = new ArrayDeque<Integer>(); |
| for (ScanFilter filter : client.filters) { |
| ScanFilterQueue queue = new ScanFilterQueue(); |
| queue.addScanFilter(filter); |
| int featureSelection = queue.getFeatureSelection(); |
| int filterIndex = mFilterIndexStack.pop(); |
| |
| resetCountDownLatch(); |
| mNativeInterface.gattClientScanFilterAdd(scannerId, queue.toArray(), |
| filterIndex); |
| waitForCallback(); |
| |
| resetCountDownLatch(); |
| if (deliveryMode == DELIVERY_MODE_ON_FOUND_LOST) { |
| trackEntries = getNumOfTrackingAdvertisements(client.settings); |
| if (!manageAllocationOfTrackingAdvertisement(trackEntries, true)) { |
| Log.e(TAG, "No hardware resources for onfound/onlost filter " |
| + trackEntries); |
| client.stats.recordTrackingHwFilterNotAvailableCountMetrics(); |
| try { |
| mService.onScanManagerErrorCallback(scannerId, |
| ScanCallback.SCAN_FAILED_INTERNAL_ERROR); |
| } catch (RemoteException e) { |
| Log.e(TAG, "failed on onScanManagerCallback", e); |
| } |
| } |
| } |
| configureFilterParamter(scannerId, client, featureSelection, filterIndex, |
| trackEntries); |
| waitForCallback(); |
| clientFilterIndices.add(filterIndex); |
| } |
| mClientFilterIndexMap.put(scannerId, clientFilterIndices); |
| } |
| } |
| |
| // Check whether the filter should be added to controller. |
| // Note only on ALL_PASS filter should be added. |
| private boolean shouldAddAllPassFilterToController(ScanClient client, int deliveryMode) { |
| // Not an ALL_PASS client, need to add filter. |
| if (!shouldUseAllPassFilter(client)) { |
| return true; |
| } |
| |
| if (deliveryMode == DELIVERY_MODE_BATCH) { |
| mAllPassBatchClients.add(client.scannerId); |
| return mAllPassBatchClients.size() == 1; |
| } else { |
| mAllPassRegularClients.add(client.scannerId); |
| return mAllPassRegularClients.size() == 1; |
| } |
| } |
| |
| private void removeScanFilters(int scannerId) { |
| Deque<Integer> filterIndices = mClientFilterIndexMap.remove(scannerId); |
| if (filterIndices != null) { |
| mFilterIndexStack.addAll(filterIndices); |
| for (Integer filterIndex : filterIndices) { |
| resetCountDownLatch(); |
| mNativeInterface.gattClientScanFilterParamDelete(scannerId, filterIndex); |
| waitForCallback(); |
| } |
| } |
| // Remove if ALL_PASS filters are used. |
| removeFilterIfExisits(mAllPassRegularClients, scannerId, |
| ALL_PASS_FILTER_INDEX_REGULAR_SCAN); |
| removeFilterIfExisits(mAllPassBatchClients, scannerId, |
| ALL_PASS_FILTER_INDEX_BATCH_SCAN); |
| } |
| |
| private void removeFilterIfExisits(Set<Integer> clients, int scannerId, int filterIndex) { |
| if (!clients.contains(scannerId)) { |
| return; |
| } |
| clients.remove(scannerId); |
| // Remove ALL_PASS filter iff no app is using it. |
| if (clients.isEmpty()) { |
| resetCountDownLatch(); |
| mNativeInterface.gattClientScanFilterParamDelete(scannerId, filterIndex); |
| waitForCallback(); |
| } |
| } |
| |
| private ScanClient getBatchScanClient(int scannerId) { |
| for (ScanClient client : mBatchClients) { |
| if (client.scannerId == scannerId) { |
| return client; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Return batch scan result type value defined in bt stack. |
| */ |
| private int getResultType(BatchScanParams params) { |
| if (params.fullScanscannerId != -1 && params.truncatedScanscannerId != -1) { |
| return SCAN_RESULT_TYPE_BOTH; |
| } |
| if (params.truncatedScanscannerId != -1) { |
| return SCAN_RESULT_TYPE_TRUNCATED; |
| } |
| if (params.fullScanscannerId != -1) { |
| return SCAN_RESULT_TYPE_FULL; |
| } |
| return -1; |
| } |
| |
| // Check if ALL_PASS filter should be used for the client. |
| private boolean shouldUseAllPassFilter(ScanClient client) { |
| if (client == null) { |
| return true; |
| } |
| if (client.filters == null || client.filters.isEmpty()) { |
| return true; |
| } |
| if (client.filters.size() > mFilterIndexStack.size()) { |
| client.stats.recordHwFilterNotAvailableCountMetrics(); |
| return true; |
| } |
| return false; |
| } |
| |
| private void initFilterIndexStack() { |
| int maxFiltersSupported = |
| AdapterService.getAdapterService().getNumOfOffloadedScanFilterSupported(); |
| // Start from index 4 as: |
| // index 0 is reserved for ALL_PASS filter in Settings app. |
| // index 1 is reserved for ALL_PASS filter for regular scan apps. |
| // index 2 is reserved for ALL_PASS filter for batch scan apps. |
| // index 3 is reserved for BAP/CAP Announcements |
| for (int i = 4; i < maxFiltersSupported; ++i) { |
| mFilterIndexStack.add(i); |
| } |
| } |
| |
| // Configure filter parameters. |
| private void configureFilterParamter(int scannerId, ScanClient client, int featureSelection, |
| int filterIndex, int numOfTrackingEntries) { |
| int deliveryMode = getDeliveryMode(client); |
| int rssiThreshold = Byte.MIN_VALUE; |
| ScanSettings settings = client.settings; |
| int onFoundTimeout = getOnFoundOnLostTimeoutMillis(settings, true); |
| int onLostTimeout = getOnFoundOnLostTimeoutMillis(settings, false); |
| int onFoundCount = getOnFoundOnLostSightings(settings); |
| onLostTimeout = 10000; |
| if (DBG) { |
| Log.d(TAG, "configureFilterParamter " + onFoundTimeout + " " + onLostTimeout + " " |
| + onFoundCount + " " + numOfTrackingEntries); |
| } |
| FilterParams filtValue = |
| new FilterParams(scannerId, filterIndex, featureSelection, LIST_LOGIC_TYPE, |
| FILTER_LOGIC_TYPE, rssiThreshold, rssiThreshold, deliveryMode, |
| onFoundTimeout, onLostTimeout, onFoundCount, numOfTrackingEntries); |
| mNativeInterface.gattClientScanFilterParamAdd(filtValue); |
| } |
| |
| // Get delivery mode based on scan settings. |
| private int getDeliveryMode(ScanClient client) { |
| if (client == null) { |
| return DELIVERY_MODE_IMMEDIATE; |
| } |
| ScanSettings settings = client.settings; |
| if (settings == null) { |
| return DELIVERY_MODE_IMMEDIATE; |
| } |
| if ((settings.getCallbackType() & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0 |
| || (settings.getCallbackType() & ScanSettings.CALLBACK_TYPE_MATCH_LOST) != 0) { |
| return DELIVERY_MODE_ON_FOUND_LOST; |
| } |
| if (isAllMatchesAutoBatchScanClient(client)) { |
| return isAutoBatchScanClientEnabled(client) ? DELIVERY_MODE_BATCH |
| : DELIVERY_MODE_IMMEDIATE; |
| } |
| return settings.getReportDelayMillis() == 0 ? DELIVERY_MODE_IMMEDIATE |
| : DELIVERY_MODE_BATCH; |
| } |
| |
| private int getScanWindowMillis(ScanSettings settings) { |
| ContentResolver resolver = mService.getContentResolver(); |
| if (settings == null) { |
| return Settings.Global.getInt( |
| resolver, |
| Settings.Global.BLE_SCAN_LOW_POWER_WINDOW_MS, |
| SCAN_MODE_LOW_POWER_WINDOW_MS); |
| } |
| |
| switch (settings.getScanMode()) { |
| case ScanSettings.SCAN_MODE_LOW_LATENCY: |
| return Settings.Global.getInt( |
| resolver, |
| Settings.Global.BLE_SCAN_LOW_LATENCY_WINDOW_MS, |
| SCAN_MODE_LOW_LATENCY_WINDOW_MS); |
| case ScanSettings.SCAN_MODE_BALANCED: |
| case ScanSettings.SCAN_MODE_AMBIENT_DISCOVERY: |
| return Settings.Global.getInt( |
| resolver, |
| Settings.Global.BLE_SCAN_BALANCED_WINDOW_MS, |
| SCAN_MODE_BALANCED_WINDOW_MS); |
| case ScanSettings.SCAN_MODE_LOW_POWER: |
| return Settings.Global.getInt( |
| resolver, |
| Settings.Global.BLE_SCAN_LOW_POWER_WINDOW_MS, |
| SCAN_MODE_LOW_POWER_WINDOW_MS); |
| case ScanSettings.SCAN_MODE_SCREEN_OFF: |
| return mAdapterService.getScreenOffLowPowerWindowMillis(); |
| case ScanSettings.SCAN_MODE_SCREEN_OFF_BALANCED: |
| return mAdapterService.getScreenOffBalancedWindowMillis(); |
| default: |
| return Settings.Global.getInt( |
| resolver, |
| Settings.Global.BLE_SCAN_LOW_POWER_WINDOW_MS, |
| SCAN_MODE_LOW_POWER_WINDOW_MS); |
| } |
| } |
| |
| private int getScanIntervalMillis(ScanSettings settings) { |
| ContentResolver resolver = mService.getContentResolver(); |
| if (settings == null) { |
| return Settings.Global.getInt( |
| resolver, |
| Settings.Global.BLE_SCAN_LOW_POWER_INTERVAL_MS, |
| SCAN_MODE_LOW_POWER_INTERVAL_MS); |
| } |
| switch (settings.getScanMode()) { |
| case ScanSettings.SCAN_MODE_LOW_LATENCY: |
| return Settings.Global.getInt( |
| resolver, |
| Settings.Global.BLE_SCAN_LOW_LATENCY_INTERVAL_MS, |
| SCAN_MODE_LOW_LATENCY_INTERVAL_MS); |
| case ScanSettings.SCAN_MODE_BALANCED: |
| case ScanSettings.SCAN_MODE_AMBIENT_DISCOVERY: |
| return Settings.Global.getInt( |
| resolver, |
| Settings.Global.BLE_SCAN_BALANCED_INTERVAL_MS, |
| SCAN_MODE_BALANCED_INTERVAL_MS); |
| case ScanSettings.SCAN_MODE_LOW_POWER: |
| return Settings.Global.getInt( |
| resolver, |
| Settings.Global.BLE_SCAN_LOW_POWER_INTERVAL_MS, |
| SCAN_MODE_LOW_POWER_INTERVAL_MS); |
| case ScanSettings.SCAN_MODE_SCREEN_OFF: |
| return mAdapterService.getScreenOffLowPowerIntervalMillis(); |
| case ScanSettings.SCAN_MODE_SCREEN_OFF_BALANCED: |
| return mAdapterService.getScreenOffBalancedIntervalMillis(); |
| default: |
| return Settings.Global.getInt( |
| resolver, |
| Settings.Global.BLE_SCAN_LOW_POWER_INTERVAL_MS, |
| SCAN_MODE_LOW_POWER_INTERVAL_MS); |
| } |
| } |
| |
| private int getOnFoundOnLostTimeoutMillis(ScanSettings settings, boolean onFound) { |
| int factor; |
| int timeout = ONLOST_ONFOUND_BASE_TIMEOUT_MS; |
| |
| if (settings.getMatchMode() == ScanSettings.MATCH_MODE_AGGRESSIVE) { |
| factor = MATCH_MODE_AGGRESSIVE_TIMEOUT_FACTOR; |
| } else { |
| factor = MATCH_MODE_STICKY_TIMEOUT_FACTOR; |
| } |
| if (!onFound) { |
| factor = factor * ONLOST_FACTOR; |
| } |
| return (timeout * factor); |
| } |
| |
| private int getOnFoundOnLostSightings(ScanSettings settings) { |
| if (settings == null) { |
| return ONFOUND_SIGHTINGS_AGGRESSIVE; |
| } |
| if (settings.getMatchMode() == ScanSettings.MATCH_MODE_AGGRESSIVE) { |
| return ONFOUND_SIGHTINGS_AGGRESSIVE; |
| } else { |
| return ONFOUND_SIGHTINGS_STICKY; |
| } |
| } |
| |
| private int getNumOfTrackingAdvertisements(ScanSettings settings) { |
| if (settings == null) { |
| return 0; |
| } |
| int val = 0; |
| int maxTotalTrackableAdvertisements = |
| AdapterService.getAdapterService().getTotalNumOfTrackableAdvertisements(); |
| // controller based onfound onlost resources are scarce commodity; the |
| // assignment of filters to num of beacons to track is configurable based |
| // on hw capabilities. Apps give an intent and allocation of onfound |
| // resources or failure there of is done based on availability - FCFS model |
| switch (settings.getNumOfMatches()) { |
| case ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT: |
| val = 1; |
| break; |
| case ScanSettings.MATCH_NUM_FEW_ADVERTISEMENT: |
| val = 2; |
| break; |
| case ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT: |
| val = maxTotalTrackableAdvertisements / 2; |
| break; |
| default: |
| val = 1; |
| if (DBG) { |
| Log.d(TAG, "Invalid setting for getNumOfMatches() " |
| + settings.getNumOfMatches()); |
| } |
| } |
| return val; |
| } |
| |
| private boolean manageAllocationOfTrackingAdvertisement(int numOfTrackableAdvertisement, |
| boolean allocate) { |
| int maxTotalTrackableAdvertisements = |
| AdapterService.getAdapterService().getTotalNumOfTrackableAdvertisements(); |
| synchronized (mCurUsedTrackableAdvertisementsLock) { |
| int availableEntries = |
| maxTotalTrackableAdvertisements - mCurUsedTrackableAdvertisements; |
| if (allocate) { |
| if (availableEntries >= numOfTrackableAdvertisement) { |
| mCurUsedTrackableAdvertisements += numOfTrackableAdvertisement; |
| return true; |
| } else { |
| return false; |
| } |
| } else { |
| if (numOfTrackableAdvertisement > mCurUsedTrackableAdvertisements) { |
| return false; |
| } else { |
| mCurUsedTrackableAdvertisements -= numOfTrackableAdvertisement; |
| return true; |
| } |
| } |
| } |
| } |
| |
| private void registerScanner(long appUuidLsb, long appUuidMsb) { |
| mNativeInterface.registerScanner(appUuidLsb, appUuidMsb); |
| } |
| |
| private void unregisterScanner(int scannerId) { |
| mNativeInterface.unregisterScanner(scannerId); |
| } |
| } |
| |
| @VisibleForTesting |
| ClientHandler getClientHandler() { |
| return mHandler; |
| } |
| |
| @VisibleForTesting |
| BatchScanParams getBatchScanParams() { |
| return mBatchScanParms; |
| } |
| |
| private boolean isScreenOn() { |
| Display[] displays = mDm.getDisplays(); |
| |
| if (displays == null) { |
| return false; |
| } |
| |
| for (Display display : displays) { |
| if (display.getState() == Display.STATE_ON) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| private final DisplayManager.DisplayListener mDisplayListener = |
| new DisplayManager.DisplayListener() { |
| @Override |
| public void onDisplayAdded(int displayId) {} |
| |
| @Override |
| public void onDisplayRemoved(int displayId) {} |
| |
| @Override |
| public void onDisplayChanged(int displayId) { |
| if (isScreenOn()) { |
| sendMessage(MSG_SCREEN_ON, null); |
| } else { |
| sendMessage(MSG_SCREEN_OFF, null); |
| } |
| } |
| }; |
| |
| private ActivityManager.OnUidImportanceListener mUidImportanceListener = |
| new ActivityManager.OnUidImportanceListener() { |
| @Override |
| public void onUidImportance(final int uid, final int importance) { |
| if (mService.mScannerMap.getAppScanStatsByUid(uid) != null) { |
| Message message = new Message(); |
| message.what = MSG_IMPORTANCE_CHANGE; |
| message.obj = new UidImportance(uid, importance); |
| mHandler.sendMessage(message); |
| } |
| } |
| }; |
| |
| private BroadcastReceiver mLocationReceiver = |
| new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| String action = intent.getAction(); |
| if (LocationManager.MODE_CHANGED_ACTION.equals(action)) { |
| final boolean locationEnabled = mLocationManager.isLocationEnabled(); |
| if (locationEnabled) { |
| sendMessage(MSG_RESUME_SCANS, null); |
| } else { |
| sendMessage(MSG_SUSPEND_SCANS, null); |
| } |
| } |
| } |
| }; |
| |
| private BroadcastReceiver mBluetoothConnectionReceiver = |
| new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| String action = intent.getAction(); |
| if (action == null) { |
| Log.w(TAG, "Received intent with null action"); |
| return; |
| } |
| switch (action) { |
| case BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED: |
| case BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED: |
| case BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED: |
| case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED: |
| case BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED: |
| case BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED: |
| int prevState = intent.getIntExtra( |
| BluetoothProfile.EXTRA_PREVIOUS_STATE, -1); |
| int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); |
| if (DBG) { |
| Log.d(TAG, "PROFILE_CONNECTION_STATE_CHANGE: action=" |
| + action + ", prevState=" + prevState + ", state=" + state); |
| } |
| boolean updatedConnectingState = |
| updateCountersAndCheckForConnectingState(state, prevState); |
| if (DBG) { |
| Log.d(TAG, "updatedConnectingState = " + updatedConnectingState); |
| } |
| if (updatedConnectingState) { |
| if (!mIsConnecting) { |
| onConnectingState(true); |
| } |
| } else { |
| if (mIsConnecting) { |
| onConnectingState(false); |
| } |
| } |
| break; |
| default: |
| Log.w(TAG, "Received unknown intent " + intent); |
| break; |
| } |
| } |
| }; |
| |
| private boolean updateCountersAndCheckForConnectingState(int state, int prevState) { |
| switch (prevState) { |
| case BluetoothProfile.STATE_CONNECTING: |
| if (mProfilesConnecting > 0) { |
| mProfilesConnecting--; |
| } else { |
| Log.e(TAG, "mProfilesConnecting " + mProfilesConnecting); |
| throw new IllegalStateException( |
| "Invalid state transition, " + prevState + " -> " + state); |
| } |
| break; |
| case BluetoothProfile.STATE_CONNECTED: |
| if (mProfilesConnected > 0) { |
| mProfilesConnected--; |
| } else { |
| Log.e(TAG, "mProfilesConnected " + mProfilesConnected); |
| throw new IllegalStateException( |
| "Invalid state transition, " + prevState + " -> " + state); |
| } |
| break; |
| case BluetoothProfile.STATE_DISCONNECTING: |
| if (mProfilesDisconnecting > 0) { |
| mProfilesDisconnecting--; |
| } else { |
| Log.e(TAG, "mProfilesDisconnecting " + mProfilesDisconnecting); |
| throw new IllegalStateException( |
| "Invalid state transition, " + prevState + " -> " + state); |
| } |
| break; |
| } |
| switch (state) { |
| case BluetoothProfile.STATE_CONNECTING: |
| mProfilesConnecting++; |
| break; |
| case BluetoothProfile.STATE_CONNECTED: |
| mProfilesConnected++; |
| break; |
| case BluetoothProfile.STATE_DISCONNECTING: |
| mProfilesDisconnecting++; |
| break; |
| case BluetoothProfile.STATE_DISCONNECTED: |
| break; |
| default: |
| } |
| if (DBG) { |
| Log.d(TAG, "mProfilesConnecting " + mProfilesConnecting + ", mProfilesConnected " |
| + mProfilesConnected + ", mProfilesDisconnecting " + mProfilesDisconnecting); |
| } |
| return (mProfilesConnecting > 0); |
| } |
| |
| private void handleImportanceChange(UidImportance imp) { |
| if (imp == null) { |
| return; |
| } |
| int uid = imp.uid; |
| int importance = imp.importance; |
| boolean updatedScanParams = false; |
| boolean isForeground = importance <= ActivityManager.RunningAppProcessInfo |
| .IMPORTANCE_FOREGROUND_SERVICE; |
| |
| if (mIsUidForegroundMap.size() < MAX_IS_UID_FOREGROUND_MAP_SIZE) { |
| mIsUidForegroundMap.put(uid, isForeground); |
| } |
| |
| for (ScanClient client : mRegularScanClients) { |
| if (client.appUid != uid || mScanNative.isOpportunisticScanClient(client)) { |
| continue; |
| } |
| if (isForeground) { |
| int scanMode = client.scanModeApp; |
| int maxScanMode = |
| mScanNative.isForceDowngradedScanClient(client) |
| ? SCAN_MODE_FORCE_DOWNGRADED |
| : scanMode; |
| if (client.updateScanMode(getMinScanMode(scanMode, maxScanMode))) { |
| updatedScanParams = true; |
| } |
| } else { |
| int scanMode = client.settings.getScanMode(); |
| int maxScanMode = |
| mScreenOn ? SCAN_MODE_APP_IN_BACKGROUND : ScanSettings.SCAN_MODE_SCREEN_OFF; |
| if (client.updateScanMode(getMinScanMode(scanMode, maxScanMode))) { |
| updatedScanParams = true; |
| } |
| } |
| if (DBG) { |
| Log.d(TAG, "uid " + uid + " isForeground " + isForeground |
| + " scanMode " + client.settings.getScanMode()); |
| } |
| } |
| |
| if (updatedScanParams) { |
| mScanNative.configureRegularScanParams(); |
| } |
| } |
| |
| private int getMinScanMode(int oldScanMode, int newScanMode) { |
| return mPriorityMap.get(oldScanMode) <= mPriorityMap.get(newScanMode) |
| ? oldScanMode |
| : newScanMode; |
| } |
| } |