1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.connecteddevice.ble;
18 
19 import static com.android.car.connecteddevice.util.SafeLog.logd;
20 import static com.android.car.connecteddevice.util.SafeLog.loge;
21 import static com.android.car.connecteddevice.util.SafeLog.logw;
22 
23 import android.annotation.IntDef;
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.bluetooth.BluetoothAdapter;
27 import android.bluetooth.le.BluetoothLeScanner;
28 import android.bluetooth.le.ScanCallback;
29 import android.bluetooth.le.ScanFilter;
30 import android.bluetooth.le.ScanResult;
31 import android.bluetooth.le.ScanSettings;
32 import android.content.Context;
33 import android.content.pm.PackageManager;
34 import android.os.Handler;
35 
36 import java.lang.annotation.Retention;
37 import java.lang.annotation.RetentionPolicy;
38 import java.util.List;
39 import java.util.concurrent.atomic.AtomicInteger;
40 
41 /**
42  * Class that manages BLE scanning operations.
43  */
44 public class BleCentralManager {
45 
46     private static final String TAG = "BleCentralManager";
47 
48     private static final int RETRY_LIMIT = 5;
49 
50     private static final int RETRY_INTERVAL_MS = 1000;
51 
52     private final Context mContext;
53 
54     private final Handler mHandler;
55 
56     private List<ScanFilter> mScanFilters;
57 
58     private ScanSettings mScanSettings;
59 
60     private ScanCallback mScanCallback;
61 
62     private BluetoothLeScanner mScanner;
63 
64     private int mScannerStartCount = 0;
65 
66     private AtomicInteger mScannerState = new AtomicInteger(STOPPED);
67     @Retention(RetentionPolicy.SOURCE)
68     @IntDef({
69             STOPPED,
70             STARTED,
71             SCANNING
72     })
73     private @interface ScannerState {}
74     private static final int STOPPED = 0;
75     private static final int STARTED = 1;
76     private static final int SCANNING = 2;
77 
BleCentralManager(@onNull Context context)78     public BleCentralManager(@NonNull Context context) {
79         mContext = context;
80         mHandler = new Handler(context.getMainLooper());
81     }
82 
83     /**
84      * Start the BLE scanning process.
85      *
86      * @param filters Optional list of {@link ScanFilter}s to apply to scan results.
87      * @param settings {@link ScanSettings} to apply to scanner.
88      * @param callback {@link ScanCallback} for scan events.
89      */
startScanning(@ullable List<ScanFilter> filters, @NonNull ScanSettings settings, @NonNull ScanCallback callback)90     public void startScanning(@Nullable List<ScanFilter> filters, @NonNull ScanSettings settings,
91             @NonNull ScanCallback callback) {
92         if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
93             loge(TAG, "Attempted start scanning, but system does not support BLE. Ignoring");
94             return;
95         }
96         logd(TAG, "Request received to start scanning.");
97         mScannerStartCount = 0;
98         mScanFilters = filters;
99         mScanSettings = settings;
100         mScanCallback = callback;
101         updateScannerState(STARTED);
102         startScanningInternally();
103     }
104 
105     /** Stop the scanner */
stopScanning()106     public void stopScanning() {
107         logd(TAG, "Attempting to stop scanning");
108         if (mScanner != null) {
109             mScanner.stopScan(mInternalScanCallback);
110         }
111         mScanCallback = null;
112         updateScannerState(STOPPED);
113     }
114 
115     /** Returns {@code true} if currently scanning, {@code false} otherwise. */
isScanning()116     public boolean isScanning() {
117         return mScannerState.get() == SCANNING;
118     }
119 
120     /** Clean up the scanning process. */
cleanup()121     public void cleanup() {
122         if (isScanning()) {
123             stopScanning();
124         }
125     }
126 
startScanningInternally()127     private void startScanningInternally() {
128         logd(TAG, "Attempting to start scanning");
129         if (mScanner == null && BluetoothAdapter.getDefaultAdapter() != null) {
130             mScanner = BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner();
131         }
132         if (mScanner != null) {
133             mScanner.startScan(mScanFilters, mScanSettings, mInternalScanCallback);
134             updateScannerState(SCANNING);
135         } else {
136             mHandler.postDelayed(() -> {
137                 // Keep trying
138                 logd(TAG, "Scanner unavailable. Trying again.");
139                 startScanningInternally();
140             }, RETRY_INTERVAL_MS);
141         }
142     }
143 
updateScannerState(@cannerState int newState)144     private void updateScannerState(@ScannerState int newState) {
145         mScannerState.set(newState);
146     }
147 
148     private final ScanCallback mInternalScanCallback = new ScanCallback() {
149         @Override
150         public void onScanResult(int callbackType, ScanResult result) {
151             if (mScanCallback != null) {
152                 mScanCallback.onScanResult(callbackType, result);
153             }
154         }
155 
156         @Override
157         public void onBatchScanResults(List<ScanResult> results) {
158             logd(TAG, "Batch scan found " + results.size() + " results.");
159             if (mScanCallback != null) {
160                 mScanCallback.onBatchScanResults(results);
161             }
162         }
163 
164         @Override
165         public void onScanFailed(int errorCode) {
166             if (mScannerStartCount >= RETRY_LIMIT) {
167                 loge(TAG, "Cannot start BLE Scanner. Scanning Retry count: "
168                         + mScannerStartCount);
169                 if (mScanCallback != null) {
170                     mScanCallback.onScanFailed(errorCode);
171                 }
172                 return;
173             }
174 
175             mScannerStartCount++;
176             logw(TAG, "BLE Scanner failed to start. Error: "
177                     + errorCode
178                     + " Retry: "
179                     + mScannerStartCount);
180             switch(errorCode) {
181                 case SCAN_FAILED_ALREADY_STARTED:
182                     // Scanner already started. Do nothing.
183                     break;
184                 case SCAN_FAILED_APPLICATION_REGISTRATION_FAILED:
185                 case SCAN_FAILED_INTERNAL_ERROR:
186                     mHandler.postDelayed(BleCentralManager.this::startScanningInternally,
187                             RETRY_INTERVAL_MS);
188                     break;
189                 default:
190                     // Ignore other codes.
191             }
192         }
193     };
194 }
195