1 /*
2  * Copyright (C) 2017 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package com.google.android.mobly.snippet.bundled;
18 
19 import android.annotation.TargetApi;
20 import android.bluetooth.BluetoothAdapter;
21 import android.bluetooth.le.BluetoothLeScanner;
22 import android.bluetooth.le.ScanCallback;
23 import android.bluetooth.le.ScanFilter;
24 import android.bluetooth.le.ScanResult;
25 import android.bluetooth.le.ScanSettings;
26 import android.os.Build;
27 import android.os.Bundle;
28 import com.google.android.mobly.snippet.Snippet;
29 import com.google.android.mobly.snippet.bundled.utils.JsonDeserializer;
30 import com.google.android.mobly.snippet.bundled.utils.JsonSerializer;
31 import com.google.android.mobly.snippet.bundled.utils.MbsEnums;
32 import com.google.android.mobly.snippet.event.EventCache;
33 import com.google.android.mobly.snippet.event.SnippetEvent;
34 import com.google.android.mobly.snippet.rpc.AsyncRpc;
35 import com.google.android.mobly.snippet.rpc.Rpc;
36 import com.google.android.mobly.snippet.rpc.RpcMinSdk;
37 import com.google.android.mobly.snippet.rpc.RpcOptional;
38 import com.google.android.mobly.snippet.util.Log;
39 import java.util.ArrayList;
40 import java.util.HashMap;
41 import java.util.List;
42 import org.json.JSONArray;
43 import org.json.JSONException;
44 import org.json.JSONObject;
45 
46 /** Snippet class exposing Android APIs in WifiManager. */
47 @TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1)
48 public class BluetoothLeScannerSnippet implements Snippet {
49     private static class BluetoothLeScanSnippetException extends Exception {
50         private static final long serialVersionUID = 1;
51 
BluetoothLeScanSnippetException(String msg)52         public BluetoothLeScanSnippetException(String msg) {
53             super(msg);
54         }
55     }
56 
57     private final BluetoothLeScanner mScanner;
58     private final EventCache mEventCache = EventCache.getInstance();
59     private final HashMap<String, ScanCallback> mScanCallbacks = new HashMap<>();
60     private final JsonSerializer mJsonSerializer = new JsonSerializer();
61     private long bleScanStartTime = 0;
62 
BluetoothLeScannerSnippet()63     public BluetoothLeScannerSnippet() {
64         mScanner = BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner();
65     }
66 
67     /**
68      * Start a BLE scan.
69      *
70      * @param callbackId
71      * @param scanFilters A JSONArray representing a list of {@link ScanFilter} object for finding
72      *     exact BLE devices. E.g.
73      *     <pre>
74      *          [
75      *            {
76      *              "ServiceUuid": (A string representation of {@link ParcelUuid}),
77      *            },
78      *          ]
79      *     </pre>
80      *
81      * @param scanSettings A JSONObject representing a {@link ScanSettings} object which is the
82      *     Settings for the scan. E.g.
83      *     <pre>
84      *          {
85      *            'ScanMode': 'SCAN_MODE_LOW_LATENCY',
86      *          }
87      *     </pre>
88      *
89      * @throws BluetoothLeScanSnippetException
90      */
91     @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1)
92     @AsyncRpc(description = "Start BLE scan.")
bleStartScan( String callbackId, @RpcOptional JSONArray scanFilters, @RpcOptional JSONObject scanSettings)93     public void bleStartScan(
94             String callbackId,
95             @RpcOptional JSONArray scanFilters,
96             @RpcOptional JSONObject scanSettings)
97             throws BluetoothLeScanSnippetException, JSONException {
98         if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) {
99             throw new BluetoothLeScanSnippetException(
100                     "Bluetooth is disabled, cannot start BLE scan.");
101         }
102         DefaultScanCallback callback = new DefaultScanCallback(callbackId);
103         if (scanFilters == null && scanSettings == null) {
104             mScanner.startScan(callback);
105         } else {
106             ArrayList<ScanFilter> filters = new ArrayList<>();
107             for (int i = 0; i < scanFilters.length(); i++) {
108                 filters.add(JsonDeserializer.jsonToScanFilter(scanFilters.getJSONObject(i)));
109             }
110             ScanSettings settings = JsonDeserializer.jsonToScanSettings(scanSettings);
111             mScanner.startScan(filters, settings, callback);
112         }
113         bleScanStartTime = System.currentTimeMillis();
114         mScanCallbacks.put(callbackId, callback);
115     }
116 
117     /**
118      * Stop a BLE scan.
119      *
120      * @param callbackId The callbackId corresponding to the {@link
121      *     BluetoothLeScannerSnippet#bleStartScan} call that started the scan.
122      * @throws BluetoothLeScanSnippetException
123      */
124     @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1)
125     @Rpc(description = "Stop a BLE scan.")
bleStopScan(String callbackId)126     public void bleStopScan(String callbackId) throws BluetoothLeScanSnippetException {
127         ScanCallback callback = mScanCallbacks.remove(callbackId);
128         if (callback == null) {
129             throw new BluetoothLeScanSnippetException("No ongoing scan with ID: " + callbackId);
130         }
131         mScanner.stopScan(callback);
132     }
133 
134     @Override
shutdown()135     public void shutdown() {
136         for (ScanCallback callback : mScanCallbacks.values()) {
137             mScanner.stopScan(callback);
138         }
139         mScanCallbacks.clear();
140     }
141 
142     private class DefaultScanCallback extends ScanCallback {
143         private final String mCallbackId;
144 
DefaultScanCallback(String callbackId)145         public DefaultScanCallback(String callbackId) {
146             mCallbackId = callbackId;
147         }
148 
149         @Override
onScanResult(int callbackType, ScanResult result)150         public void onScanResult(int callbackType, ScanResult result) {
151             Log.i("Got Bluetooth LE scan result.");
152             long bleScanOnResultTime = System.currentTimeMillis();
153             SnippetEvent event = new SnippetEvent(mCallbackId, "onScanResult");
154             String callbackTypeString =
155                     MbsEnums.BLE_SCAN_RESULT_CALLBACK_TYPE.getString(callbackType);
156             event.getData().putString("CallbackType", callbackTypeString);
157             event.getData().putBundle("result", mJsonSerializer.serializeBleScanResult(result));
158             event.getData()
159                     .putLong("StartToResultTimeDeltaMs", bleScanOnResultTime - bleScanStartTime);
160             mEventCache.postEvent(event);
161         }
162 
163         @Override
onBatchScanResults(List<ScanResult> results)164         public void onBatchScanResults(List<ScanResult> results) {
165             Log.i("Got Bluetooth LE batch scan results.");
166             SnippetEvent event = new SnippetEvent(mCallbackId, "onBatchScanResult");
167             ArrayList<Bundle> resultList = new ArrayList<>(results.size());
168             for (ScanResult result : results) {
169                 resultList.add(mJsonSerializer.serializeBleScanResult(result));
170             }
171             event.getData().putParcelableArrayList("results", resultList);
172             mEventCache.postEvent(event);
173         }
174 
175         @Override
onScanFailed(int errorCode)176         public void onScanFailed(int errorCode) {
177             Log.e("Bluetooth LE scan failed with error code: " + errorCode);
178             SnippetEvent event = new SnippetEvent(mCallbackId, "onScanFailed");
179             String errorCodeString = MbsEnums.BLE_SCAN_FAILED_ERROR_CODE.getString(errorCode);
180             event.getData().putString("ErrorCode", errorCodeString);
181             mEventCache.postEvent(event);
182         }
183     }
184 }
185