From 8a7fe9abd925d8ee471424fcd901a4767aaabf90 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Tue, 23 May 2017 16:24:46 -0700 Subject: Add basic Bluetooth LE advertising and scan support. (#47) * Add basic Bluetooth LE advertising and scan support. * Introduce `RpcEnum`, a standard way to handle String-int enum conversion. --- .../snippet/bundled/BluetoothAdapterSnippet.java | 7 +- .../bundled/BluetoothLeAdvertiserSnippet.java | 178 +++++++++++++++++++++ .../snippet/bundled/BluetoothLeScannerSnippet.java | 138 ++++++++++++++++ .../snippet/bundled/utils/JsonDeserializer.java | 61 +++++++ .../snippet/bundled/utils/JsonSerializer.java | 110 ++++++------- .../mobly/snippet/bundled/utils/MbsEnums.java | 91 +++++++++++ .../mobly/snippet/bundled/utils/RpcEnum.java | 89 +++++++++++ .../android/mobly/snippet/bundled/utils/Utils.java | 1 + 8 files changed, 618 insertions(+), 57 deletions(-) create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeAdvertiserSnippet.java create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeScannerSnippet.java create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/utils/RpcEnum.java (limited to 'src/main/java/com/google/android') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java index 67df97b..0f4d3e6 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Build; +import android.os.Bundle; import android.support.test.InstrumentationRegistry; import com.google.android.mobly.snippet.Snippet; import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; @@ -173,11 +174,11 @@ public class BluetoothAdapterSnippet implements Snippet { } @Rpc(description = "Get the list of paired bluetooth devices.") - public JSONArray btGetPairedDevices() + public ArrayList btGetPairedDevices() throws BluetoothAdapterSnippetException, InterruptedException, JSONException { - JSONArray pairedDevices = new JSONArray(); + ArrayList pairedDevices = new ArrayList<>(); for (BluetoothDevice device : mBluetoothAdapter.getBondedDevices()) { - pairedDevices.put(mJsonSerializer.toJson(device)); + pairedDevices.add(mJsonSerializer.serializeBluetoothDevice(device)); } return pairedDevices; } diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeAdvertiserSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeAdvertiserSnippet.java new file mode 100644 index 0000000..e161a5b --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeAdvertiserSnippet.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * 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.google.android.mobly.snippet.bundled; + +import android.annotation.TargetApi; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.le.AdvertiseCallback; +import android.bluetooth.le.AdvertiseData; +import android.bluetooth.le.AdvertiseSettings; +import android.bluetooth.le.BluetoothLeAdvertiser; +import android.os.Build; +import android.os.Bundle; +import android.os.ParcelUuid; +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.bundled.utils.JsonDeserializer; +import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; +import com.google.android.mobly.snippet.bundled.utils.RpcEnum; +import com.google.android.mobly.snippet.event.EventCache; +import com.google.android.mobly.snippet.event.SnippetEvent; +import com.google.android.mobly.snippet.rpc.AsyncRpc; +import com.google.android.mobly.snippet.rpc.Rpc; +import com.google.android.mobly.snippet.rpc.RpcMinSdk; +import com.google.android.mobly.snippet.util.Log; +import java.util.HashMap; +import org.json.JSONException; +import org.json.JSONObject; + +/** Snippet class exposing Android APIs in WifiManager. */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1) +public class BluetoothLeAdvertiserSnippet implements Snippet { + private static class BluetoothLeAdvertiserSnippetException extends Exception { + private static final long serialVersionUID = 1; + + public BluetoothLeAdvertiserSnippetException(String msg) { + super(msg); + } + } + + private final BluetoothLeAdvertiser mAdvertiser; + private static final EventCache sEventCache = EventCache.getInstance(); + + private final HashMap mAdvertiseCallbacks = new HashMap<>(); + + public BluetoothLeAdvertiserSnippet() { + mAdvertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser(); + } + + /** + * Start Bluetooth LE advertising. + * + *

This can be called multiple times, and each call is associated with a {@link + * AdvertiseCallback} object, which is used to stop the advertising. + * + * @param callbackId + * @param advertiseSettings A JSONObject representing a {@link AdvertiseSettings object}. E.g. + *

+     *          {
+     *            "AdvertiseMode": "ADVERTISE_MODE_BALANCED",
+     *            "Timeout": (int, milliseconds),
+     *            "Connectable": (bool),
+     *            "TxPowerLevel": "ADVERTISE_TX_POWER_LOW"
+     *          }
+     *     
+ * + * @param advertiseData A JSONObject representing a {@link AdvertiseData} object. E.g. + *
+     *          {
+     *            "IncludeDeviceName": (bool),
+     *            # JSON list, each element representing a set of service data, which is composed of
+     *            # a UUID, and an optional string.
+     *            "ServiceData": [
+     *                      {
+     *                        "UUID": (A string representation of {@link ParcelUuid}),
+     *                        "Data": (Optional, The string representation of what you want to
+     *                                 advertise, base64 encoded)
+     *                        # If you want to add a UUID without data, simply omit the "Data"
+     *                        # field.
+     *                      }
+     *                ]
+     *          }
+     *     
+ * + * @throws BluetoothLeAdvertiserSnippetException + * @throws JSONException + */ + @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1) + @AsyncRpc(description = "Start BLE advertising.") + public void bleStartAdvertising( + String callbackId, JSONObject advertiseSettings, JSONObject advertiseData) + throws BluetoothLeAdvertiserSnippetException, JSONException { + if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) { + throw new BluetoothLeAdvertiserSnippetException( + "Bluetooth is disabled, cannot start BLE advertising."); + } + AdvertiseSettings settings = JsonDeserializer.jsonToBleAdvertiseSettings(advertiseSettings); + AdvertiseData data = JsonDeserializer.jsonToBleAdvertiseData(advertiseData); + AdvertiseCallback advertiseCallback = new DefaultAdvertiseCallback(callbackId); + mAdvertiser.startAdvertising(settings, data, advertiseCallback); + mAdvertiseCallbacks.put(callbackId, advertiseCallback); + } + + /** + * Stop a BLE advertising. + * + * @param callbackId The callbackId corresponding to the {@link + * BluetoothLeAdvertiserSnippet#bleStartAdvertising} call that started the advertising. + * @throws BluetoothLeScannerSnippet.BluetoothLeScanSnippetException + */ + @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1) + @Rpc(description = "Stop BLE advertising.") + public void bleStopAdvertising(String callbackId) throws BluetoothLeAdvertiserSnippetException { + AdvertiseCallback callback = mAdvertiseCallbacks.remove(callbackId); + if (callback == null) { + throw new BluetoothLeAdvertiserSnippetException( + "No advertising session found for ID " + callbackId); + } + mAdvertiser.stopAdvertising(callback); + } + + private static class DefaultAdvertiseCallback extends AdvertiseCallback { + private final String mCallbackId; + public static RpcEnum ADVERTISE_FAILURE_ERROR_CODE = + new RpcEnum.Builder() + .add("ADVERTISE_FAILED_ALREADY_STARTED", ADVERTISE_FAILED_ALREADY_STARTED) + .add("ADVERTISE_FAILED_DATA_TOO_LARGE", ADVERTISE_FAILED_DATA_TOO_LARGE) + .add( + "ADVERTISE_FAILED_FEATURE_UNSUPPORTED", + ADVERTISE_FAILED_FEATURE_UNSUPPORTED) + .add("ADVERTISE_FAILED_INTERNAL_ERROR", ADVERTISE_FAILED_INTERNAL_ERROR) + .add( + "ADVERTISE_FAILED_TOO_MANY_ADVERTISERS", + ADVERTISE_FAILED_TOO_MANY_ADVERTISERS) + .build(); + + public DefaultAdvertiseCallback(String callbackId) { + mCallbackId = callbackId; + } + + public void onStartSuccess(AdvertiseSettings settingsInEffect) { + Log.e("Bluetooth LE advertising started with settings: " + settingsInEffect.toString()); + SnippetEvent event = new SnippetEvent(mCallbackId, "onStartSuccess"); + Bundle advertiseSettings = + JsonSerializer.serializeBleAdvertisingSettings(settingsInEffect); + event.getData().putBundle("SettingsInEffect", advertiseSettings); + sEventCache.postEvent(event); + } + + public void onStartFailure(int errorCode) { + Log.e("Bluetooth LE advertising failed to start with error code: " + errorCode); + SnippetEvent event = new SnippetEvent(mCallbackId, "onStartFailure"); + final String errorCodeString = ADVERTISE_FAILURE_ERROR_CODE.getString(errorCode); + event.getData().putString("ErrorCode", errorCodeString); + sEventCache.postEvent(event); + } + } + + @Override + public void shutdown() { + for (AdvertiseCallback callback : mAdvertiseCallbacks.values()) { + mAdvertiser.stopAdvertising(callback); + } + mAdvertiseCallbacks.clear(); + } +} diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeScannerSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeScannerSnippet.java new file mode 100644 index 0000000..7e133d1 --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeScannerSnippet.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * 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.google.android.mobly.snippet.bundled; + +import android.annotation.TargetApi; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanResult; +import android.os.Build; +import android.os.Bundle; +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; +import com.google.android.mobly.snippet.bundled.utils.MbsEnums; +import com.google.android.mobly.snippet.event.EventCache; +import com.google.android.mobly.snippet.event.SnippetEvent; +import com.google.android.mobly.snippet.rpc.AsyncRpc; +import com.google.android.mobly.snippet.rpc.Rpc; +import com.google.android.mobly.snippet.rpc.RpcMinSdk; +import com.google.android.mobly.snippet.util.Log; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** Snippet class exposing Android APIs in WifiManager. */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1) +public class BluetoothLeScannerSnippet implements Snippet { + private static class BluetoothLeScanSnippetException extends Exception { + private static final long serialVersionUID = 1; + + public BluetoothLeScanSnippetException(String msg) { + super(msg); + } + } + + private final BluetoothLeScanner mScanner; + private final EventCache mEventCache = EventCache.getInstance(); + private final HashMap mScanCallbacks = new HashMap<>(); + private final JsonSerializer mJsonSerializer = new JsonSerializer(); + + public BluetoothLeScannerSnippet() { + mScanner = BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner(); + } + + /** + * Start a BLE scan. + * + * @param callbackId + * @throws BluetoothLeScanSnippetException + */ + @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1) + @AsyncRpc(description = "Start BLE scan.") + public void bleStartScan(String callbackId) throws BluetoothLeScanSnippetException { + if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) { + throw new BluetoothLeScanSnippetException( + "Bluetooth is disabled, cannot start BLE scan."); + } + DefaultScanCallback callback = new DefaultScanCallback(callbackId); + mScanner.startScan(callback); + mScanCallbacks.put(callbackId, callback); + } + + /** + * Stop a BLE scan. + * + * @param callbackId The callbackId corresponding to the {@link + * BluetoothLeScannerSnippet#bleStartScan} call that started the scan. + * @throws BluetoothLeScanSnippetException + */ + @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1) + @Rpc(description = "Stop a BLE scan.") + public void bleStopScan(String callbackId) throws BluetoothLeScanSnippetException { + ScanCallback callback = mScanCallbacks.remove(callbackId); + if (callback == null) { + throw new BluetoothLeScanSnippetException("No ongoing scan with ID: " + callbackId); + } + mScanner.stopScan(callback); + } + + @Override + public void shutdown() { + for (ScanCallback callback : mScanCallbacks.values()) { + mScanner.stopScan(callback); + } + mScanCallbacks.clear(); + } + + private class DefaultScanCallback extends ScanCallback { + private final String mCallbackId; + + public DefaultScanCallback(String callbackId) { + mCallbackId = callbackId; + } + + public void onScanResult(int callbackType, ScanResult result) { + Log.i("Got Bluetooth LE scan result."); + SnippetEvent event = new SnippetEvent(mCallbackId, "onScanResult"); + String callbackTypeString = + MbsEnums.BLE_SCAN_RESULT_CALLBACK_TYPE.getString(callbackType); + event.getData().putString("CallbackType", callbackTypeString); + event.getData().putBundle("result", mJsonSerializer.serializeBleScanResult(result)); + mEventCache.postEvent(event); + } + + public void onBatchScanResults(List results) { + Log.i("Got Bluetooth LE batch scan results."); + SnippetEvent event = new SnippetEvent(mCallbackId, "onBatchScanResult"); + ArrayList resultList = new ArrayList<>(results.size()); + for (ScanResult result : results) { + resultList.add(mJsonSerializer.serializeBleScanResult(result)); + } + event.getData().putParcelableArrayList("results", resultList); + mEventCache.postEvent(event); + } + + public void onScanFailed(int errorCode) { + Log.e("Bluetooth LE scan failed with error code: " + errorCode); + SnippetEvent event = new SnippetEvent(mCallbackId, "onScanFailed"); + String errorCodeString = MbsEnums.BLE_SCAN_FAILED_ERROR_CODE.getString(errorCode); + event.getData().putString("ErrorCode", errorCodeString); + mEventCache.postEvent(event); + } + } +} diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java index 4fc6b82..2f943e0 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java @@ -16,7 +16,14 @@ package com.google.android.mobly.snippet.bundled.utils; +import android.annotation.TargetApi; +import android.bluetooth.le.AdvertiseData; +import android.bluetooth.le.AdvertiseSettings; import android.net.wifi.WifiConfiguration; +import android.os.Build; +import android.os.ParcelUuid; +import android.util.Base64; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -40,4 +47,58 @@ public class JsonDeserializer { } return config; } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static AdvertiseSettings jsonToBleAdvertiseSettings(JSONObject jsonObject) + throws JSONException { + AdvertiseSettings.Builder builder = new AdvertiseSettings.Builder(); + if (jsonObject.has("AdvertiseMode")) { + int mode = MbsEnums.BLE_ADVERTISE_MODE.getInt(jsonObject.getString("AdvertiseMode")); + builder.setAdvertiseMode(mode); + } + // Timeout in milliseconds. + if (jsonObject.has("Timeout")) { + builder.setTimeout(jsonObject.getInt("Timeout")); + } + if (jsonObject.has("Connectable")) { + builder.setConnectable(jsonObject.getBoolean("Connectable")); + } + if (jsonObject.has("TxPowerLevel")) { + int txPowerLevel = + MbsEnums.BLE_ADVERTISE_TX_POWER.getInt(jsonObject.getString("TxPowerLevel")); + builder.setTxPowerLevel(txPowerLevel); + } + return builder.build(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static AdvertiseData jsonToBleAdvertiseData(JSONObject jsonObject) throws JSONException { + AdvertiseData.Builder builder = new AdvertiseData.Builder(); + if (jsonObject.has("IncludeDeviceName")) { + builder.setIncludeDeviceName(jsonObject.getBoolean("IncludeDeviceName")); + } + if (jsonObject.has("IncludeTxPowerLevel")) { + builder.setIncludeTxPowerLevel(jsonObject.getBoolean("IncludeTxPowerLevel")); + } + if (jsonObject.has("ServiceData")) { + JSONArray serviceData = jsonObject.getJSONArray("ServiceData"); + for (int i = 0; i < serviceData.length(); i++) { + JSONObject dataSet = serviceData.getJSONObject(i); + ParcelUuid parcelUuid = ParcelUuid.fromString(dataSet.getString("UUID")); + builder.addServiceUuid(parcelUuid); + if (dataSet.has("Data")) { + byte[] data = Base64.decode(dataSet.getString("Data"), Base64.DEFAULT); + builder.addServiceData(parcelUuid, data); + } + } + } + if (jsonObject.has("ManufacturerData")) { + JSONObject manufacturerData = jsonObject.getJSONObject("ManufacturerData"); + int manufacturerId = manufacturerData.getInt("ManufacturerId"); + byte[] manufacturerSpecificData = + Base64.decode(jsonObject.getString("ManufacturerSpecificData"), Base64.DEFAULT); + builder.addManufacturerData(manufacturerId, manufacturerSpecificData); + } + return builder.build(); + } } diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java index bd00f45..c555b92 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java @@ -16,12 +16,16 @@ package com.google.android.mobly.snippet.bundled.utils; +import android.annotation.TargetApi; import android.bluetooth.BluetoothDevice; +import android.bluetooth.le.AdvertiseSettings; +import android.bluetooth.le.ScanRecord; import android.net.DhcpInfo; import android.net.wifi.SupplicantState; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; import android.os.Build; +import android.os.Bundle; import android.os.ParcelUuid; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -65,9 +69,7 @@ public class JsonSerializer { } public JSONObject toJson(Object object) throws JSONException { - if (object instanceof BluetoothDevice) { - return serializeBluetoothDevice((BluetoothDevice) object); - } else if (object instanceof DhcpInfo) { + if (object instanceof DhcpInfo) { return serializeDhcpInfo((DhcpInfo) object); } else if (object instanceof WifiConfiguration) { return serializeWifiConfiguration((WifiConfiguration) object); @@ -109,13 +111,13 @@ public class JsonSerializer { private JSONObject serializeWifiConfiguration(WifiConfiguration data) throws JSONException { JSONObject result = new JSONObject(mGson.toJson(data)); result.put("Status", WifiConfiguration.Status.strings[data.status]); - guaranteedPut(result, "SSID", trimQuotationMarks(data.SSID)); + result.put("SSID", trimQuotationMarks(data.SSID)); return result; } private JSONObject serializeWifiInfo(WifiInfo data) throws JSONException { JSONObject result = new JSONObject(mGson.toJson(data)); - guaranteedPut(result, "SSID", trimQuotationMarks(data.getSSID())); + result.put("SSID", trimQuotationMarks(data.getSSID())); for (SupplicantState state : SupplicantState.values()) { if (data.getSupplicantState().equals(state)) { result.put("SupplicantState", state.name()); @@ -124,71 +126,71 @@ public class JsonSerializer { return result; } - private JSONObject serializeBluetoothDevice(BluetoothDevice data) throws JSONException { - JSONObject result = new JSONObject(); - guaranteedPut(result, "Address", data.getAddress()); - final String bondStateFieldName = "BondState"; - switch (data.getBondState()) { - case BluetoothDevice.BOND_NONE: - result.put(bondStateFieldName, "BOND_NONE"); - break; - case BluetoothDevice.BOND_BONDING: - result.put(bondStateFieldName, "BOND_BONDING"); - break; - case BluetoothDevice.BOND_BONDED: - result.put(bondStateFieldName, "BOND_BONDED"); - break; - } - guaranteedPut(result, "Name", data.getName()); + public Bundle serializeBluetoothDevice(BluetoothDevice data) { + Bundle result = new Bundle(); + result.putString("Address", data.getAddress()); + final String bondState = + MbsEnums.BLUETOOTH_DEVICE_BOND_STATE.getString(data.getBondState()); + result.putString("BondState", bondState); + result.putString("Name", data.getName()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - final String deviceTypeFieldName = "DeviceType"; - switch (data.getType()) { - case BluetoothDevice.DEVICE_TYPE_CLASSIC: - result.put(deviceTypeFieldName, "DEVICE_TYPE_CLASSIC"); - break; - case BluetoothDevice.DEVICE_TYPE_LE: - result.put(deviceTypeFieldName, "DEVICE_TYPE_LE"); - break; - case BluetoothDevice.DEVICE_TYPE_DUAL: - result.put(deviceTypeFieldName, "DEVICE_TYPE_DUAL"); - break; - case BluetoothDevice.DEVICE_TYPE_UNKNOWN: - result.put(deviceTypeFieldName, "DEVICE_TYPE_UNKNOWN"); - break; - } + String deviceType = MbsEnums.BLUETOOTH_DEVICE_TYPE.getString(data.getType()); + result.putString("DeviceType", deviceType); ParcelUuid[] parcelUuids = data.getUuids(); if (parcelUuids != null) { ArrayList uuidStrings = new ArrayList<>(parcelUuids.length); for (ParcelUuid parcelUuid : parcelUuids) { uuidStrings.add(parcelUuid.getUuid().toString()); } - result.put("UUIDs", uuidStrings); + result.putStringArrayList("UUIDs", uuidStrings); } } return result; } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public Bundle serializeBleScanResult(android.bluetooth.le.ScanResult scanResult) { + Bundle result = new Bundle(); + result.putBundle("Device", serializeBluetoothDevice(scanResult.getDevice())); + result.putInt("Rssi", scanResult.getRssi()); + result.putBundle("ScanRecord", serializeBleScanRecord(scanResult.getScanRecord())); + result.putLong("TimestampNanos", scanResult.getTimestampNanos()); + return result; + } + /** - * Guarantees a field is put into a JSONObject even if it's null. + * Serialize ScanRecord for Bluetooth LE. * - *

By default, if the object of {@link JSONObject#put(String, Object)} is null, the `put` - * method would either remove the field or do nothing, causing serialized objects to have - * inconsistent fields. + *

Not all fields are serialized here. Will add more as we need. * - *

Use this method to put objects that may be null into the serialized JSONObject so the - * serialized objects have a consistent set of critical fields, like the SSID field in - * serialized WifiConfiguration objects. + *

The returned {@link Bundle} has the following info:
+     *          "DeviceName", String
+     *          "TxPowerLevel", String
+     * 
* - * @param data - * @param name - * @param object - * @throws JSONException + * @param record A {@link ScanRecord} object. + * @return A {@link Bundle} object. */ - private void guaranteedPut(JSONObject data, String name, Object object) throws JSONException { - if (object == null) { - data.put(name, JSONObject.NULL); - } else { - data.put(name, object); - } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private Bundle serializeBleScanRecord(ScanRecord record) { + Bundle result = new Bundle(); + result.putString("DeviceName", record.getDeviceName()); + result.putString( + "TxPowerLevel", + MbsEnums.BLE_ADVERTISE_TX_POWER.getString(record.getTxPowerLevel())); + return result; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static Bundle serializeBleAdvertisingSettings(AdvertiseSettings advertiseSettings) { + Bundle result = new Bundle(); + result.putString( + "TxPowerLevel", + MbsEnums.BLE_ADVERTISE_TX_POWER.getString(advertiseSettings.getTxPowerLevel())); + result.putString( + "Mode", MbsEnums.BLE_ADVERTISE_MODE.getString(advertiseSettings.getMode())); + result.putInt("Timeout", advertiseSettings.getTimeout()); + result.putBoolean("IsConnectable", advertiseSettings.isConnectable()); + return result; } } diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java new file mode 100644 index 0000000..33c425c --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java @@ -0,0 +1,91 @@ +package com.google.android.mobly.snippet.bundled.utils; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.le.AdvertiseSettings; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanSettings; +import android.os.Build; + +/** Mobly Bundled Snippets (MBS)'s {@link RpcEnum} objects representing enums in Android APIs. */ +public class MbsEnums { + static final RpcEnum BLE_ADVERTISE_MODE = buildBleAdvertiseModeEnum(); + static final RpcEnum BLE_ADVERTISE_TX_POWER = buildBleAdvertiseTxPowerEnum(); + public static final RpcEnum BLE_SCAN_FAILED_ERROR_CODE = buildBleScanFailedErrorCodeEnum(); + public static final RpcEnum BLE_SCAN_RESULT_CALLBACK_TYPE = + buildBleScanResultCallbackTypeEnum(); + static final RpcEnum BLUETOOTH_DEVICE_BOND_STATE = buildBluetoothDeviceBondState(); + static final RpcEnum BLUETOOTH_DEVICE_TYPE = buildBluetoothDeviceTypeEnum(); + + private static RpcEnum buildBluetoothDeviceBondState() { + RpcEnum.Builder builder = new RpcEnum.Builder(); + return builder.add("BOND_NONE", BluetoothDevice.BOND_NONE) + .add("BOND_BONDING", BluetoothDevice.BOND_BONDING) + .add("BOND_BONDED", BluetoothDevice.BOND_BONDED) + .build(); + } + + private static RpcEnum buildBluetoothDeviceTypeEnum() { + RpcEnum.Builder builder = new RpcEnum.Builder(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + return builder.build(); + } + return builder.add("DEVICE_TYPE_CLASSIC", BluetoothDevice.DEVICE_TYPE_CLASSIC) + .add("DEVICE_TYPE_LE", BluetoothDevice.DEVICE_TYPE_LE) + .add("DEVICE_TYPE_DUAL", BluetoothDevice.DEVICE_TYPE_DUAL) + .build(); + } + + private static RpcEnum buildBleAdvertiseTxPowerEnum() { + RpcEnum.Builder builder = new RpcEnum.Builder(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return builder.build(); + } + return builder.add( + "ADVERTISE_TX_POWER_ULTRA_LOW", + AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW) + .add("ADVERTISE_TX_POWER_LOW", AdvertiseSettings.ADVERTISE_TX_POWER_LOW) + .add("ADVERTISE_TX_POWER_MEDIUM", AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM) + .add("ADVERTISE_TX_POWER_HIGH", AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) + .build(); + } + + private static RpcEnum buildBleAdvertiseModeEnum() { + RpcEnum.Builder builder = new RpcEnum.Builder(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return builder.build(); + } + return builder.add("ADVERTISE_MODE_BALANCED", AdvertiseSettings.ADVERTISE_MODE_BALANCED) + .add("ADVERTISE_MODE_LOW_LATENCY", AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) + .add("ADVERTISE_MODE_LOW_POWER", AdvertiseSettings.ADVERTISE_MODE_LOW_POWER) + .build(); + } + + private static RpcEnum buildBleScanFailedErrorCodeEnum() { + RpcEnum.Builder builder = new RpcEnum.Builder(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return builder.build(); + } + return builder.add("SCAN_FAILED_ALREADY_STARTED", ScanCallback.SCAN_FAILED_ALREADY_STARTED) + .add( + "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED", + ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED) + .add( + "SCAN_FAILED_FEATURE_UNSUPPORTED", + ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED) + .add("SCAN_FAILED_INTERNAL_ERROR", ScanCallback.SCAN_FAILED_INTERNAL_ERROR) + .build(); + } + + private static RpcEnum buildBleScanResultCallbackTypeEnum() { + RpcEnum.Builder builder = new RpcEnum.Builder(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return builder.build(); + } + builder.add("CALLBACK_TYPE_ALL_MATCHES", ScanSettings.CALLBACK_TYPE_ALL_MATCHES); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + builder.add("CALLBACK_TYPE_FIRST_MATCH", ScanSettings.CALLBACK_TYPE_FIRST_MATCH); + builder.add("CALLBACK_TYPE_MATCH_LOST", ScanSettings.CALLBACK_TYPE_MATCH_LOST); + } + return builder.build(); + } +} diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/RpcEnum.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/RpcEnum.java new file mode 100644 index 0000000..d3d95ae --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/RpcEnum.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * 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.google.android.mobly.snippet.bundled.utils; + +import com.google.common.collect.ImmutableBiMap; + +/** + * A container type for handling String-Integer enum conversion in Rpc protocol. + * + *

In Serializing/Deserializing Android API enums, we often need to convert an enum value from + * one form to another. This container class makes it easier to do so. + * + *

Once built, an RpcEnum object is immutable. + */ +public class RpcEnum { + private final ImmutableBiMap mEnums; + + private RpcEnum(ImmutableBiMap.Builder builder, int minSdk) { + mEnums = builder.build(); + } + + /** + * Get the int value of an enum based on its String value. + * + * @param enumString + * @return + */ + public int getInt(String enumString) { + Integer result = mEnums.get(enumString); + if (result == null) { + throw new NoSuchFieldError("No int value found for: " + enumString); + } + return result; + } + + /** + * Get the String value of an enum based on its int value. + * + * @param enumInt + * @return + */ + public String getString(int enumInt) { + String result = mEnums.inverse().get(enumInt); + if (result == null) { + throw new NoSuchFieldError("No String value found for: " + enumInt); + } + return result; + } + + /** Builder for RpcEnum. */ + public static class Builder { + private final ImmutableBiMap.Builder builder; + public int minSdk = 0; + + public Builder() { + builder = new ImmutableBiMap.Builder<>(); + } + + /** + * Add an enum String-Integer pair. + * + * @param enumString + * @param enumInt + * @return + */ + public Builder add(String enumString, int enumInt) { + builder.put(enumString, enumInt); + return this; + } + + public RpcEnum build() { + return new RpcEnum(builder, minSdk); + } + } +} diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/Utils.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/Utils.java index 8736d85..8f4f7d6 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/utils/Utils.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/Utils.java @@ -22,6 +22,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public final class Utils { + private Utils() {} /** -- cgit v1.2.3