/* * 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.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.bluetooth.Attributable; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.IBluetoothGatt; import android.bluetooth.annotations.RequiresBluetoothLocationPermission; import android.bluetooth.annotations.RequiresBluetoothScanPermission; import android.bluetooth.annotations.RequiresLegacyBluetoothAdminPermission; import android.content.AttributionSource; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; import android.util.Log; import java.util.IdentityHashMap; import java.util.Objects; /** * 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}. * * @hide */ 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 final BluetoothAdapter mBluetoothAdapter; private final AttributionSource mAttributionSource; /* maps callback, to callback wrapper and sync handle */ IdentityHashMap mCallbackWrappers; /** * Use {@link BluetoothAdapter#getBluetoothLeScanner()} instead. * * @hide */ public PeriodicAdvertisingManager(BluetoothAdapter bluetoothAdapter) { mBluetoothAdapter = Objects.requireNonNull(bluetoothAdapter); mAttributionSource = mBluetoothAdapter.getAttributionSource(); mCallbackWrappers = 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. */ @RequiresLegacyBluetoothAdminPermission @RequiresBluetoothScanPermission @RequiresBluetoothLocationPermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN) 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. */ @RequiresLegacyBluetoothAdminPermission @RequiresBluetoothScanPermission @RequiresBluetoothLocationPermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN) 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 = mBluetoothAdapter.getBluetoothGatt(); if (handler == null) { handler = new Handler(Looper.getMainLooper()); } IPeriodicAdvertisingCallback wrapped = wrap(callback, handler); mCallbackWrappers.put(callback, wrapped); try { gatt.registerSync(scanResult, skip, timeout, wrapped, mAttributionSource); } 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. */ @RequiresLegacyBluetoothAdminPermission @RequiresBluetoothScanPermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN) public void unregisterSync(PeriodicAdvertisingCallback callback) { if (callback == null) { throw new IllegalArgumentException("callback can't be null"); } IBluetoothGatt gatt = mBluetoothAdapter.getBluetoothGatt(); IPeriodicAdvertisingCallback wrapper = mCallbackWrappers.remove(callback); if (wrapper == null) { throw new IllegalArgumentException("callback was not properly registered"); } try { gatt.unregisterSync(wrapper, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "Failed to cancel sync creation - ", e); return; } } /** * Transfer periodic sync * * @hide */ public void transferSync(BluetoothDevice bda, int serviceData, int syncHandle) { IBluetoothGatt gatt = mBluetoothAdapter.getBluetoothGatt(); try { gatt.transferSync(bda, serviceData, syncHandle, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "Failed to register sync - ", e); return; } } /** * Transfer set info * * @hide */ public void transferSetInfo( BluetoothDevice bda, int serviceData, int advHandle, PeriodicAdvertisingCallback callback) { transferSetInfo(bda, serviceData, advHandle, callback, null); } /** * Transfer set info * * @hide */ public void transferSetInfo( BluetoothDevice bda, int serviceData, int advHandle, PeriodicAdvertisingCallback callback, @Nullable Handler handler) { if (callback == null) { throw new IllegalArgumentException("callback can't be null"); } IBluetoothGatt gatt = mBluetoothAdapter.getBluetoothGatt(); if (handler == null) { handler = new Handler(Looper.getMainLooper()); } IPeriodicAdvertisingCallback wrapper = wrap(callback, handler); if (wrapper == null) { throw new IllegalArgumentException("callback was not properly registered"); } try { gatt.transferSetInfo(bda, serviceData, advHandle, wrapper, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "Failed to register sync - ", e); return; } } @SuppressLint("AndroidFrameworkBluetoothPermission") 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) { Attributable.setAttributionSource(device, mAttributionSource); 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 notified. mCallbackWrappers.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 notified. mCallbackWrappers.remove(callback); } }); } public void onSyncTransferred(BluetoothDevice device, int status) { handler.post( new Runnable() { @Override public void run() { callback.onSyncTransferred(device, status); } }); } public void onBigInfoAdvertisingReport(int syncHandle, boolean encrypted) { handler.post( new Runnable() { @Override public void run() { callback.onBigInfoAdvertisingReport(syncHandle, encrypted); } }); } }; } }