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