From ac7fcfa36160f1ce8b1ba8e0a3a90338a99a02a4 Mon Sep 17 00:00:00 2001 From: Jakub Pawlowski Date: Mon, 16 Jan 2017 07:21:01 -0800 Subject: [PATCH] Bluetooth 5 periodc scan API (1/2) Bug: 30622771 Test: manual Change-Id: I61853bc71f6013e9406d1d151bb51ea4484bb92c (cherry picked from commit a48e03745becc96181c676dc3d194d0572f11c10) --- Android.mk | 1 + api/current.txt | 31 +++ api/system-current.txt | 31 +++ api/test-current.txt | 31 +++ .../android/bluetooth/BluetoothAdapter.java | 26 ++ .../android/bluetooth/IBluetoothGatt.aidl | 5 + .../le/IPeriodicAdvertisingCallback.aidl | 31 +++ .../le/PeriodicAdvertisingCallback.java | 77 ++++++ .../le/PeriodicAdvertisingManager.java | 237 ++++++++++++++++++ .../le/PeriodicAdvertisingReport.aidl | 19 ++ .../le/PeriodicAdvertisingReport.java | 184 ++++++++++++++ 11 files changed, 673 insertions(+) create mode 100644 core/java/android/bluetooth/le/IPeriodicAdvertisingCallback.aidl create mode 100644 core/java/android/bluetooth/le/PeriodicAdvertisingCallback.java create mode 100644 core/java/android/bluetooth/le/PeriodicAdvertisingManager.java create mode 100644 core/java/android/bluetooth/le/PeriodicAdvertisingReport.aidl create mode 100644 core/java/android/bluetooth/le/PeriodicAdvertisingReport.java diff --git a/Android.mk b/Android.mk index 4e1479a4d2dea..a49a587586d91 100644 --- a/Android.mk +++ b/Android.mk @@ -131,6 +131,7 @@ LOCAL_SRC_FILES += \ core/java/android/bluetooth/IBluetoothGattCallback.aidl \ core/java/android/bluetooth/IBluetoothGattServerCallback.aidl \ core/java/android/bluetooth/le/IAdvertiserCallback.aidl \ + core/java/android/bluetooth/le/IPeriodicAdvertisingCallback.aidl \ core/java/android/bluetooth/le/IScannerCallback.aidl \ core/java/android/content/IClipboard.aidl \ core/java/android/content/IContentService.aidl \ diff --git a/api/current.txt b/api/current.txt index 3e99316ed6553..f6250bfb48075 100644 --- a/api/current.txt +++ b/api/current.txt @@ -6695,6 +6695,7 @@ package android.bluetooth { method public java.util.Set getBondedDevices(); method public static synchronized android.bluetooth.BluetoothAdapter getDefaultAdapter(); method public java.lang.String getName(); + method public android.bluetooth.le.PeriodicAdvertisingManager getPeriodicAdvertisingManager(); method public int getProfileConnectionState(int); method public boolean getProfileProxy(android.content.Context, android.bluetooth.BluetoothProfile.ServiceListener, int); method public android.bluetooth.BluetoothDevice getRemoteDevice(java.lang.String); @@ -7490,6 +7491,36 @@ package android.bluetooth.le { method public void stopScan(android.bluetooth.le.ScanCallback); } + public abstract class PeriodicAdvertisingCallback { + ctor public PeriodicAdvertisingCallback(); + method public void onPeriodicAdvertisingReport(android.bluetooth.le.PeriodicAdvertisingReport); + method public void onSyncEstablished(int, android.bluetooth.BluetoothDevice, int, int, int, int); + method public void onSyncLost(int); + field public static final int SYNC_NO_RESOURCES = 2; // 0x2 + field public static final int SYNC_NO_RESPONSE = 1; // 0x1 + } + + public final class PeriodicAdvertisingManager { + method public void registerSync(android.bluetooth.le.ScanResult, int, int, android.bluetooth.le.PeriodicAdvertisingCallback); + method public void registerSync(android.bluetooth.le.ScanResult, int, int, android.bluetooth.le.PeriodicAdvertisingCallback, android.os.Handler); + method public void unregisterSync(android.bluetooth.le.PeriodicAdvertisingCallback); + } + + public final class PeriodicAdvertisingReport implements android.os.Parcelable { + ctor public PeriodicAdvertisingReport(int, int, int, int, android.bluetooth.le.ScanRecord); + method public int describeContents(); + method public android.bluetooth.le.ScanRecord getData(); + method public int getDataStatus(); + method public int getRssi(); + method public int getSyncHandle(); + method public long getTimestampNanos(); + method public int getTxPower(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + field public static final int DATA_COMPLETE = 0; // 0x0 + field public static final int DATA_INCOMPLETE_TRUNCATED = 2; // 0x2 + } + public abstract class ScanCallback { ctor public ScanCallback(); method public void onBatchScanResults(java.util.List); diff --git a/api/system-current.txt b/api/system-current.txt index 078a988eb4c54..36b16f387d849 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -6993,6 +6993,7 @@ package android.bluetooth { method public java.util.Set getBondedDevices(); method public static synchronized android.bluetooth.BluetoothAdapter getDefaultAdapter(); method public java.lang.String getName(); + method public android.bluetooth.le.PeriodicAdvertisingManager getPeriodicAdvertisingManager(); method public int getProfileConnectionState(int); method public boolean getProfileProxy(android.content.Context, android.bluetooth.BluetoothProfile.ServiceListener, int); method public android.bluetooth.BluetoothDevice getRemoteDevice(java.lang.String); @@ -7797,6 +7798,36 @@ package android.bluetooth.le { method public void stopScan(android.bluetooth.le.ScanCallback); } + public abstract class PeriodicAdvertisingCallback { + ctor public PeriodicAdvertisingCallback(); + method public void onPeriodicAdvertisingReport(android.bluetooth.le.PeriodicAdvertisingReport); + method public void onSyncEstablished(int, android.bluetooth.BluetoothDevice, int, int, int, int); + method public void onSyncLost(int); + field public static final int SYNC_NO_RESOURCES = 2; // 0x2 + field public static final int SYNC_NO_RESPONSE = 1; // 0x1 + } + + public final class PeriodicAdvertisingManager { + method public void registerSync(android.bluetooth.le.ScanResult, int, int, android.bluetooth.le.PeriodicAdvertisingCallback); + method public void registerSync(android.bluetooth.le.ScanResult, int, int, android.bluetooth.le.PeriodicAdvertisingCallback, android.os.Handler); + method public void unregisterSync(android.bluetooth.le.PeriodicAdvertisingCallback); + } + + public final class PeriodicAdvertisingReport implements android.os.Parcelable { + ctor public PeriodicAdvertisingReport(int, int, int, int, android.bluetooth.le.ScanRecord); + method public int describeContents(); + method public android.bluetooth.le.ScanRecord getData(); + method public int getDataStatus(); + method public int getRssi(); + method public int getSyncHandle(); + method public long getTimestampNanos(); + method public int getTxPower(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + field public static final int DATA_COMPLETE = 0; // 0x0 + field public static final int DATA_INCOMPLETE_TRUNCATED = 2; // 0x2 + } + public final class ResultStorageDescriptor implements android.os.Parcelable { ctor public ResultStorageDescriptor(int, int, int); method public int describeContents(); diff --git a/api/test-current.txt b/api/test-current.txt index b7c9acefab3e6..95372a49e0c47 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -6704,6 +6704,7 @@ package android.bluetooth { method public java.util.Set getBondedDevices(); method public static synchronized android.bluetooth.BluetoothAdapter getDefaultAdapter(); method public java.lang.String getName(); + method public android.bluetooth.le.PeriodicAdvertisingManager getPeriodicAdvertisingManager(); method public int getProfileConnectionState(int); method public boolean getProfileProxy(android.content.Context, android.bluetooth.BluetoothProfile.ServiceListener, int); method public android.bluetooth.BluetoothDevice getRemoteDevice(java.lang.String); @@ -7499,6 +7500,36 @@ package android.bluetooth.le { method public void stopScan(android.bluetooth.le.ScanCallback); } + public abstract class PeriodicAdvertisingCallback { + ctor public PeriodicAdvertisingCallback(); + method public void onPeriodicAdvertisingReport(android.bluetooth.le.PeriodicAdvertisingReport); + method public void onSyncEstablished(int, android.bluetooth.BluetoothDevice, int, int, int, int); + method public void onSyncLost(int); + field public static final int SYNC_NO_RESOURCES = 2; // 0x2 + field public static final int SYNC_NO_RESPONSE = 1; // 0x1 + } + + public final class PeriodicAdvertisingManager { + method public void registerSync(android.bluetooth.le.ScanResult, int, int, android.bluetooth.le.PeriodicAdvertisingCallback); + method public void registerSync(android.bluetooth.le.ScanResult, int, int, android.bluetooth.le.PeriodicAdvertisingCallback, android.os.Handler); + method public void unregisterSync(android.bluetooth.le.PeriodicAdvertisingCallback); + } + + public final class PeriodicAdvertisingReport implements android.os.Parcelable { + ctor public PeriodicAdvertisingReport(int, int, int, int, android.bluetooth.le.ScanRecord); + method public int describeContents(); + method public android.bluetooth.le.ScanRecord getData(); + method public int getDataStatus(); + method public int getRssi(); + method public int getSyncHandle(); + method public long getTimestampNanos(); + method public int getTxPower(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + field public static final int DATA_COMPLETE = 0; // 0x0 + field public static final int DATA_INCOMPLETE_TRUNCATED = 2; // 0x2 + } + public abstract class ScanCallback { ctor public ScanCallback(); method public void onBatchScanResults(java.util.List); diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java index 4ecf1249d18c4..818693773a0ff 100644 --- a/core/java/android/bluetooth/BluetoothAdapter.java +++ b/core/java/android/bluetooth/BluetoothAdapter.java @@ -26,6 +26,7 @@ import android.annotation.SystemApi; import android.app.ActivityThread; import android.bluetooth.le.BluetoothLeAdvertiser; import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.PeriodicAdvertisingManager; import android.bluetooth.le.ScanCallback; import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanRecord; @@ -525,6 +526,7 @@ public final class BluetoothAdapter { private static BluetoothLeScanner sBluetoothLeScanner; private static BluetoothLeAdvertiser sBluetoothLeAdvertiser; + private static PeriodicAdvertisingManager sPeriodicAdvertisingManager; private final IBluetoothManager mManagerService; private IBluetooth mService; @@ -629,6 +631,30 @@ public final class BluetoothAdapter { return sBluetoothLeAdvertiser; } + /** + * Returns a {@link PeriodicAdvertisingManager} object for Bluetooth LE Periodic Advertising + * operations. Will return null if Bluetooth is turned off or if Bluetooth LE Periodic + * Advertising is not supported on this device. + *

+ * Use {@link #isLePeriodicAdvertisingSupported()} to check whether LE Periodic Advertising is + * supported on this device before calling this method. + */ + public PeriodicAdvertisingManager getPeriodicAdvertisingManager() { + if (!getLeAccess()) + return null; + + if (!isLePeriodicAdvertisingSupported()) + return null; + + synchronized (mLock) { + if (sPeriodicAdvertisingManager == null) { + sPeriodicAdvertisingManager = + new PeriodicAdvertisingManager(mManagerService); + } + } + return sPeriodicAdvertisingManager; + } + /** * Returns a {@link BluetoothLeScanner} object for Bluetooth LE scan operations. */ diff --git a/core/java/android/bluetooth/IBluetoothGatt.aidl b/core/java/android/bluetooth/IBluetoothGatt.aidl index aa2291e072dd4..69563cb3e6a4d 100644 --- a/core/java/android/bluetooth/IBluetoothGatt.aidl +++ b/core/java/android/bluetooth/IBluetoothGatt.aidl @@ -21,6 +21,7 @@ import android.bluetooth.BluetoothGattService; import android.bluetooth.le.AdvertiseSettings; import android.bluetooth.le.AdvertiseData; import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanResult; import android.bluetooth.le.ScanSettings; import android.bluetooth.le.ResultStorageDescriptor; import android.os.ParcelUuid; @@ -29,6 +30,7 @@ import android.os.WorkSource; import android.bluetooth.IBluetoothGattCallback; import android.bluetooth.IBluetoothGattServerCallback; import android.bluetooth.le.IAdvertiserCallback; +import android.bluetooth.le.IPeriodicAdvertisingCallback; import android.bluetooth.le.IScannerCallback; /** @@ -53,6 +55,9 @@ interface IBluetoothGatt { in AdvertiseSettings settings); void stopMultiAdvertising(in int advertiserId); + void registerSync(in ScanResult scanResult, in int skip, in int timeout, in IPeriodicAdvertisingCallback callback); + void unregisterSync(in IPeriodicAdvertisingCallback callback); + void registerClient(in ParcelUuid appId, in IBluetoothGattCallback callback); void unregisterClient(in int clientIf); void clientConnect(in int clientIf, in String address, in boolean isDirect, in int transport); diff --git a/core/java/android/bluetooth/le/IPeriodicAdvertisingCallback.aidl b/core/java/android/bluetooth/le/IPeriodicAdvertisingCallback.aidl new file mode 100644 index 0000000000000..a76c54d4ab492 --- /dev/null +++ b/core/java/android/bluetooth/le/IPeriodicAdvertisingCallback.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2017 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 android.bluetooth.le; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.le.PeriodicAdvertisingReport; + +/** + * Callback definitions for interacting with Periodic Advertising + * @hide + */ +oneway interface IPeriodicAdvertisingCallback { + + void onSyncEstablished(in int syncHandle, in BluetoothDevice device, in int advertisingSid, + in int skip, in int timeout, in int status); + void onPeriodicAdvertisingReport(in PeriodicAdvertisingReport report); + void onSyncLost(in int syncHandle); +} diff --git a/core/java/android/bluetooth/le/PeriodicAdvertisingCallback.java b/core/java/android/bluetooth/le/PeriodicAdvertisingCallback.java new file mode 100644 index 0000000000000..6616231bcdf8b --- /dev/null +++ b/core/java/android/bluetooth/le/PeriodicAdvertisingCallback.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2017 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 android.bluetooth.le; + +import android.bluetooth.BluetoothDevice; + +/** + * Bluetooth LE periodic advertising callbacks, used to deliver periodic + * advertising operation status. + * + * @see PeriodicAdvertisingManager#createSync + */ +public abstract class PeriodicAdvertisingCallback { + + /** + * The requested operation was successful. + * + * @hide + */ + public static final int SYNC_SUCCESS = 0; + + /** + * Sync failed to be established because remote device did not respond. + */ + public static final int SYNC_NO_RESPONSE = 1; + + /** + * Sync failed to be established because controller can't support more syncs. + */ + public static final int SYNC_NO_RESOURCES = 2; + + + /** + * Callback when synchronization was established. + * + * @param syncHandle handle used to identify this synchronization. + * @param device remote device. + * @param advertisingSid synchronized advertising set id. + * @param skip The number of periodic advertising packets that can be skipped + * after a successful receive in force. @see PeriodicAdvertisingManager#createSync + * @param timeout Synchronization timeout for the periodic advertising in force. One + * unit is 10ms. @see PeriodicAdvertisingManager#createSync + * @param timeout + * @param status operation status. + */ + public void onSyncEstablished(int syncHandle, BluetoothDevice device, + int advertisingSid, int skip, int timeout, + int status) {} + + /** + * Callback when periodic advertising report is received. + * + * @param report periodic advertising report. + */ + public void onPeriodicAdvertisingReport(PeriodicAdvertisingReport report) {} + + /** + * Callback when periodic advertising synchronization was lost. + * + * @param syncHandle handle used to identify this synchronization. + */ + public void onSyncLost(int syncHandle) {} +} diff --git a/core/java/android/bluetooth/le/PeriodicAdvertisingManager.java b/core/java/android/bluetooth/le/PeriodicAdvertisingManager.java new file mode 100644 index 0000000000000..12c8a8c8ffd4f --- /dev/null +++ b/core/java/android/bluetooth/le/PeriodicAdvertisingManager.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2017 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 android.bluetooth.le; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.IBluetoothGatt; +import android.bluetooth.IBluetoothManager; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.util.Log; +import java.util.IdentityHashMap; +import java.util.Map; + +/** + * This class provides methods to perform periodic advertising related + * operations. An application can register for periodic advertisements using + * {@link PeriodicAdvertisingManager#registerSync}. + *

+ * Use {@link BluetoothAdapter#getPeriodicAdvertisingManager()} to get an + * instance of {@link PeriodicAdvertisingManager}. + *

+ * Note: Most of the methods here require + * {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission. + */ +public final class PeriodicAdvertisingManager { + + private static final String TAG = "PeriodicAdvertisingManager"; + + private static final int SKIP_MIN = 0; + private static final int SKIP_MAX = 499; + private static final int TIMEOUT_MIN = 10; + private static final int TIMEOUT_MAX = 16384; + + private static final int SYNC_STARTING = -1; + + private final IBluetoothManager mBluetoothManager; + private BluetoothAdapter mBluetoothAdapter; + + /* maps callback, to callback wrapper and sync handle */ + Map callbackWrappers; + + /** + * Use {@link BluetoothAdapter#getBluetoothLeScanner()} instead. + * + * @param bluetoothManager BluetoothManager that conducts overall Bluetooth Management. + * @hide + */ + public PeriodicAdvertisingManager(IBluetoothManager bluetoothManager) { + mBluetoothManager = bluetoothManager; + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + callbackWrappers = new IdentityHashMap<>(); + } + + /** + * Synchronize with periodic advertising pointed to by the {@code scanResult}. + * The {@code scanResult} used must contain a valid advertisingSid. First + * call to registerSync will use the {@code skip} and {@code timeout} provided. + * Subsequent calls from other apps, trying to sync with same set will reuse + * existing sync, thus {@code skip} and {@code timeout} values will not take + * effect. The values in effect will be returned in + * {@link PeriodicAdvertisingCallback#onSyncEstablished}. + * + * @param scanResult Scan result containing advertisingSid. + * @param skip The number of periodic advertising packets that can be skipped + * after a successful receive. Must be between 0 and 499. + * @param timeout Synchronization timeout for the periodic advertising. One + * unit is 10ms. Must be between 10 (100ms) and 16384 (163.84s). + * @param callback Callback used to deliver all operations status. + * @throws IllegalArgumentException if {@code scanResult} is null or {@code + * skip} is invalid or {@code timeout} is invalid or {@code callback} is null. + */ + public void registerSync(ScanResult scanResult, int skip, int timeout, + PeriodicAdvertisingCallback callback) { + registerSync(scanResult, skip, timeout, callback, null); + } + + /** + * Synchronize with periodic advertising pointed to by the {@code scanResult}. + * The {@code scanResult} used must contain a valid advertisingSid. First + * call to registerSync will use the {@code skip} and {@code timeout} provided. + * Subsequent calls from other apps, trying to sync with same set will reuse + * existing sync, thus {@code skip} and {@code timeout} values will not take + * effect. The values in effect will be returned in + * {@link PeriodicAdvertisingCallback#onSyncEstablished}. + * + * @param scanResult Scan result containing advertisingSid. + * @param skip The number of periodic advertising packets that can be skipped + * after a successful receive. Must be between 0 and 499. + * @param timeout Synchronization timeout for the periodic advertising. One + * unit is 10ms. Must be between 10 (100ms) and 16384 (163.84s). + * @param callback Callback used to deliver all operations status. + * @param handler thread upon which the callbacks will be invoked. + * @throws IllegalArgumentException if {@code scanResult} is null or {@code + * skip} is invalid or {@code timeout} is invalid or {@code callback} is null. + */ + public void registerSync(ScanResult scanResult, int skip, int timeout, + PeriodicAdvertisingCallback callback, Handler handler) { + if (callback == null) { + throw new IllegalArgumentException("callback can't be null"); + } + + if (scanResult == null) { + throw new IllegalArgumentException("scanResult can't be null"); + } + + if (scanResult.getAdvertisingSid() == ScanResult.SID_NOT_PRESENT) { + throw new IllegalArgumentException("scanResult must contain a valid sid"); + } + + if (skip < SKIP_MIN || skip > SKIP_MAX) { + throw new IllegalArgumentException( + "timeout must be between " + TIMEOUT_MIN + " and " + TIMEOUT_MAX); + } + + if (timeout < TIMEOUT_MIN || timeout > TIMEOUT_MAX) { + throw new IllegalArgumentException( + "timeout must be between " + TIMEOUT_MIN + " and " + TIMEOUT_MAX); + } + + IBluetoothGatt gatt; + try { + gatt = mBluetoothManager.getBluetoothGatt(); + } catch (RemoteException e) { + Log.e(TAG, "Failed to get Bluetooth gatt - ", e); + callback.onSyncEstablished(0, scanResult.getDevice(), scanResult.getAdvertisingSid(), + skip, timeout, + PeriodicAdvertisingCallback.SYNC_NO_RESOURCES); + return; + } + + if (handler == null) + handler = new Handler(Looper.getMainLooper()); + + IPeriodicAdvertisingCallback wrapped = wrap(callback, handler); + callbackWrappers.put(callback, wrapped); + + try { + gatt.registerSync(scanResult, skip, timeout, wrapped); + } catch (RemoteException e) { + Log.e(TAG, "Failed to register sync - ", e); + return; + } + } + + /** + * Cancel pending attempt to create sync, or terminate existing sync. + * + * @param callback Callback used to deliver all operations status. + * @throws IllegalArgumentException if {@code callback} is null, or not a properly + * registered callback. + */ + public void unregisterSync(PeriodicAdvertisingCallback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback can't be null"); + } + + IBluetoothGatt gatt; + try { + gatt = mBluetoothManager.getBluetoothGatt(); + } catch (RemoteException e) { + Log.e(TAG, "Failed to get Bluetooth gatt - ", e); + return; + } + + IPeriodicAdvertisingCallback wrapper = callbackWrappers.remove(callback); + if (wrapper == null) { + throw new IllegalArgumentException("callback was not properly registered"); + } + + try { + gatt.unregisterSync(wrapper); + } catch (RemoteException e) { + Log.e(TAG, "Failed to cancel sync creation - ", e); + return; + } + } + + private IPeriodicAdvertisingCallback wrap(PeriodicAdvertisingCallback callback, Handler handler) { + return new IPeriodicAdvertisingCallback.Stub() { + public void onSyncEstablished(int syncHandle, BluetoothDevice device, + int advertisingSid, int skip, int timeout, int status) { + + handler.post(new Runnable() { + @Override + public void run() { + callback.onSyncEstablished(syncHandle, device, advertisingSid, skip, timeout, + status); + + if (status != PeriodicAdvertisingCallback.SYNC_SUCCESS) { + // App can still unregister the sync until notified it failed. Remove callback + // after app was notifed. + callbackWrappers.remove(callback); + } + } + }); + } + + public void onPeriodicAdvertisingReport(PeriodicAdvertisingReport report) { + handler.post(new Runnable() { + @Override + public void run() { + callback.onPeriodicAdvertisingReport(report); + } + }); + } + + public void onSyncLost(int syncHandle) { + handler.post(new Runnable() { + @Override + public void run() { + callback.onSyncLost(syncHandle); + // App can still unregister the sync until notified it's lost. Remove callback after + // app was notifed. + callbackWrappers.remove(callback); + } + }); + } + }; + } +} diff --git a/core/java/android/bluetooth/le/PeriodicAdvertisingReport.aidl b/core/java/android/bluetooth/le/PeriodicAdvertisingReport.aidl new file mode 100644 index 0000000000000..547d09611fdb5 --- /dev/null +++ b/core/java/android/bluetooth/le/PeriodicAdvertisingReport.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2017 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 android.bluetooth.le; + +parcelable PeriodicAdvertisingReport; diff --git a/core/java/android/bluetooth/le/PeriodicAdvertisingReport.java b/core/java/android/bluetooth/le/PeriodicAdvertisingReport.java new file mode 100644 index 0000000000000..3ff4ca580069f --- /dev/null +++ b/core/java/android/bluetooth/le/PeriodicAdvertisingReport.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2017 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 android.bluetooth.le; + +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * PeriodicAdvertisingReport for Bluetooth LE synchronized advertising. + */ +public final class PeriodicAdvertisingReport implements Parcelable { + + /** + * The data returned is complete + */ + public static final int DATA_COMPLETE = 0; + + /** + * The data returned is incomplete. The controller was unsuccessfull to + * receive all chained packets, returning only partial data. + */ + public static final int DATA_INCOMPLETE_TRUNCATED = 2; + + private int syncHandle; + private int txPower; + private int rssi; + private int dataStatus; + + // periodic advertising data. + @Nullable + private ScanRecord data; + + // Device timestamp when the result was last seen. + private long timestampNanos; + + /** + * Constructor of periodic advertising result. + * + */ + public PeriodicAdvertisingReport(int syncHandle, int txPower, int rssi, + int dataStatus, ScanRecord data) { + this.syncHandle = syncHandle; + this.txPower = txPower; + this.rssi = rssi; + this.dataStatus = dataStatus; + this.data = data; + } + + private PeriodicAdvertisingReport(Parcel in) { + readFromParcel(in); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(syncHandle); + dest.writeLong(txPower); + dest.writeInt(rssi); + dest.writeInt(dataStatus); + if (data != null) { + dest.writeInt(1); + dest.writeByteArray(data.getBytes()); + } else { + dest.writeInt(0); + } + } + + private void readFromParcel(Parcel in) { + syncHandle = in.readInt(); + txPower = in.readInt(); + rssi = in.readInt(); + dataStatus = in.readInt(); + if (in.readInt() == 1) { + data = ScanRecord.parseFromBytes(in.createByteArray()); + } + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Returns the synchronization handle. + */ + public int getSyncHandle() { + return syncHandle; + } + + /** + * Returns the transmit power in dBm. The valid range is [-127, 126]. Value + * of 127 means information was not available. + */ + public int getTxPower() { + return txPower; + } + + /** + * Returns the received signal strength in dBm. The valid range is [-127, 20]. + */ + public int getRssi() { + return rssi; + } + + /** + * Returns the data status. Can be one of {@link PeriodicAdvertisingReport#DATA_COMPLETE} + * or {@link PeriodicAdvertisingReport#DATA_INCOMPLETE_TRUNCATED}. + */ + public int getDataStatus() { + return dataStatus; + } + + /** + * Returns the data contained in this periodic advertising report. + */ + @Nullable + public ScanRecord getData() { + return data; + } + + /** + * Returns timestamp since boot when the scan record was observed. + */ + public long getTimestampNanos() { + return timestampNanos; + } + + @Override + public int hashCode() { + return Objects.hash(syncHandle, txPower, rssi, dataStatus, data, timestampNanos); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + PeriodicAdvertisingReport other = (PeriodicAdvertisingReport) obj; + return (syncHandle == other.syncHandle) && + (txPower == other.txPower) && + (rssi == other.rssi) && + (dataStatus == other.dataStatus) && + Objects.equals(data, other.data) && + (timestampNanos == other.timestampNanos); + } + + @Override + public String toString() { + return "PeriodicAdvertisingReport{syncHandle=" + syncHandle + + ", txPower=" + txPower + ", rssi=" + rssi + ", dataStatus=" + dataStatus + + ", data=" + Objects.toString(data) + ", timestampNanos=" + timestampNanos + '}'; + } + + public static final Parcelable.Creator CREATOR = new Creator() { + @Override + public PeriodicAdvertisingReport createFromParcel(Parcel source) { + return new PeriodicAdvertisingReport(source); + } + + @Override + public PeriodicAdvertisingReport[] newArray(int size) { + return new PeriodicAdvertisingReport[size]; + } + }; +}