package org.altbeacon.beacon; import android.annotation.TargetApi; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothManager; import android.bluetooth.le.AdvertiseCallback; import android.bluetooth.le.AdvertiseData; import android.bluetooth.le.AdvertiseSettings; import android.bluetooth.le.BluetoothLeAdvertiser; import android.content.Context; import android.content.pm.PackageManager; import android.os.ParcelUuid; import org.altbeacon.beacon.logging.LogManager; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.UUID; /** * Provides a mechanism for transmitting as a beacon. Requires Android 5.0 */ @TargetApi(21) public class BeaconTransmitter { public static final int SUPPORTED = 0; public static final int NOT_SUPPORTED_MIN_SDK = 1; public static final int NOT_SUPPORTED_BLE = 2; // isMultipleAdvertisementSupported returning false no longer indicates that transmission is not // possible @Deprecated public static final int NOT_SUPPORTED_MULTIPLE_ADVERTISEMENTS = 3; public static final int NOT_SUPPORTED_CANNOT_GET_ADVERTISER = 4; public static final int NOT_SUPPORTED_CANNOT_GET_ADVERTISER_MULTIPLE_ADVERTISEMENTS = 5; private static final String TAG = "BeaconTransmitter"; private BluetoothAdapter mBluetoothAdapter; private BluetoothLeAdvertiser mBluetoothLeAdvertiser; private int mAdvertiseMode = AdvertiseSettings.ADVERTISE_MODE_LOW_POWER; private int mAdvertiseTxPowerLevel = AdvertiseSettings.ADVERTISE_TX_POWER_HIGH; private Beacon mBeacon; private BeaconParser mBeaconParser; private AdvertiseCallback mAdvertisingClientCallback; private boolean mStarted; private AdvertiseCallback mAdvertiseCallback; private boolean mConnectable = false; /** * Creates a new beacon transmitter capable of transmitting beacons with the format * specified in the BeaconParser and with the data fields specified in the Beacon object * @param context * @param parser specifies the format of the beacon transmission */ public BeaconTransmitter(Context context, BeaconParser parser) { mBeaconParser = parser; BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); if (bluetoothManager != null) { mBluetoothAdapter = bluetoothManager.getAdapter(); mBluetoothLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser(); LogManager.d(TAG, "new BeaconTransmitter constructed. mbluetoothLeAdvertiser is %s", mBluetoothLeAdvertiser); } else { LogManager.e(TAG, "Failed to get BluetoothManager"); } } /** * Tells if transmission has started * @return */ public boolean isStarted() { return mStarted; } /** * Sets the beacon whose fields will be transmitted * @param beacon */ public void setBeacon(Beacon beacon) { mBeacon = beacon; } /** * Sets the beaconParsser used for formatting the transmission * @param beaconParser */ public void setBeaconParser(BeaconParser beaconParser) { mBeaconParser = beaconParser; } /** * @see #setAdvertiseMode(int) * @return advertiseMode */ public int getAdvertiseMode() { return mAdvertiseMode; } /** * AdvertiseSettings.ADVERTISE_MODE_BALANCED 3 Hz * AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY 10 Hz * AdvertiseSettings.ADVERTISE_MODE_LOW_POWER 1 Hz * @param mAdvertiseMode */ public void setAdvertiseMode(int mAdvertiseMode) { this.mAdvertiseMode = mAdvertiseMode; } /** * @see #setAdvertiseTxPowerLevel(int mAdvertiseTxPowerLevel) * @return txPowerLevel */ public int getAdvertiseTxPowerLevel() { return mAdvertiseTxPowerLevel; } /** * AdvertiseSettings.ADVERTISE_TX_POWER_HIGH -56 dBm @ 1 meter with Nexus 5 * AdvertiseSettings.ADVERTISE_TX_POWER_LOW -75 dBm @ 1 meter with Nexus 5 * AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM -66 dBm @ 1 meter with Nexus 5 * AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW not detected with Nexus 5 * @param mAdvertiseTxPowerLevel */ public void setAdvertiseTxPowerLevel(int mAdvertiseTxPowerLevel) { this.mAdvertiseTxPowerLevel = mAdvertiseTxPowerLevel; } /** * Whether the advertisement should indicate the device is connectable. * @param connectable */ public void setConnectable(boolean connectable) { this.mConnectable = connectable; } /** * @see #setConnectable(boolean) * @return connectable */ public boolean isConnectable() { return mConnectable; } /** * Starts advertising with fields from the passed beacon * @param beacon */ public void startAdvertising(Beacon beacon) { startAdvertising(beacon, null); } /** * Starts advertising with fields from the passed beacon * @param beacon */ public void startAdvertising(Beacon beacon, AdvertiseCallback callback) { mBeacon = beacon; mAdvertisingClientCallback = callback; startAdvertising(); } /** * Starts this beacon advertising */ public void startAdvertising() { if (mBeacon == null) { throw new NullPointerException("Beacon cannot be null. Set beacon before starting advertising"); } int manufacturerCode = mBeacon.getManufacturer(); int serviceUuid = -1; if (mBeaconParser.getServiceUuid() != null) { serviceUuid = mBeaconParser.getServiceUuid().intValue(); } if (mBeaconParser == null) { throw new NullPointerException("You must supply a BeaconParser instance to BeaconTransmitter."); } byte[] advertisingBytes = mBeaconParser.getBeaconAdvertisementData(mBeacon); String byteString = ""; for (int i= 0; i < advertisingBytes.length; i++) { byteString += String.format("%02X", advertisingBytes[i]); byteString += " "; } LogManager.d(TAG, "Starting advertising with ID1: %s ID2: %s ID3: %s and data: %s of size " + "%s", mBeacon.getId1(), mBeacon.getIdentifiers().size() > 1 ? mBeacon.getId2() : "", mBeacon.getIdentifiers().size() > 2 ? mBeacon.getId3() : "", byteString, advertisingBytes.length); try{ AdvertiseData.Builder dataBuilder = new AdvertiseData.Builder(); if (serviceUuid > 0) { byte[] serviceUuidBytes = new byte[] { (byte) (serviceUuid & 0xff), (byte) ((serviceUuid >> 8) & 0xff)}; ParcelUuid parcelUuid = parseUuidFrom(serviceUuidBytes); dataBuilder.addServiceData(parcelUuid, advertisingBytes); dataBuilder.addServiceUuid(parcelUuid); dataBuilder.setIncludeTxPowerLevel(false); dataBuilder.setIncludeDeviceName(false); } else { dataBuilder.addManufacturerData(manufacturerCode, advertisingBytes); } AdvertiseSettings.Builder settingsBuilder = new AdvertiseSettings.Builder(); settingsBuilder.setAdvertiseMode(mAdvertiseMode); settingsBuilder.setTxPowerLevel(mAdvertiseTxPowerLevel); settingsBuilder.setConnectable(mConnectable); mBluetoothLeAdvertiser.startAdvertising(settingsBuilder.build(), dataBuilder.build(), getAdvertiseCallback()); LogManager.d(TAG, "Started advertisement with callback: %s", getAdvertiseCallback()); } catch (Exception e){ LogManager.e(e, TAG, "Cannot start advertising due to exception"); } } /** * Stops this beacon from advertising */ public void stopAdvertising() { if (!mStarted) { LogManager.d(TAG, "Skipping stop advertising -- not started"); return; } LogManager.d(TAG, "Stopping advertising with object %s", mBluetoothLeAdvertiser); mAdvertisingClientCallback = null; try { mBluetoothLeAdvertiser.stopAdvertising(getAdvertiseCallback()); } catch (IllegalStateException e) { LogManager.w(TAG, "Bluetooth is turned off. Transmitter stop call failed."); } mStarted = false; } /** * Checks to see if this device supports beacon advertising * @return SUPPORTED if yes, otherwise: * NOT_SUPPORTED_MIN_SDK * NOT_SUPPORTED_BLE * NOT_SUPPORTED_MULTIPLE_ADVERTISEMENTS * NOT_SUPPORTED_CANNOT_GET_ADVERTISER */ public static int checkTransmissionSupported(Context context) { int returnCode = SUPPORTED; if (android.os.Build.VERSION.SDK_INT < 21) { returnCode = NOT_SUPPORTED_MIN_SDK; } else if (!context.getApplicationContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { returnCode = NOT_SUPPORTED_BLE; } else { try { // Check to see if the getBluetoothLeAdvertiser is available. If not, this will throw an exception indicating we are not running Android L if (((BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter().getBluetoothLeAdvertiser() == null) { if (!((BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter().isMultipleAdvertisementSupported()) { returnCode = NOT_SUPPORTED_CANNOT_GET_ADVERTISER_MULTIPLE_ADVERTISEMENTS; } else { returnCode = NOT_SUPPORTED_CANNOT_GET_ADVERTISER; } } } catch (Exception e) { returnCode = NOT_SUPPORTED_CANNOT_GET_ADVERTISER; } } return returnCode; } private AdvertiseCallback getAdvertiseCallback() { if (mAdvertiseCallback == null) { mAdvertiseCallback = new AdvertiseCallback() { @Override public void onStartFailure(int errorCode) { LogManager.e(TAG,"Advertisement start failed, code: %s", errorCode); if (mAdvertisingClientCallback != null) { mAdvertisingClientCallback.onStartFailure(errorCode); } } @Override public void onStartSuccess(AdvertiseSettings settingsInEffect) { LogManager.i(TAG,"Advertisement start succeeded."); mStarted = true; if (mAdvertisingClientCallback != null) { mAdvertisingClientCallback.onStartSuccess(settingsInEffect); } } }; } return mAdvertiseCallback; } /** * Parse UUID from bytes. The {@code uuidBytes} can represent a 16-bit, 32-bit or 128-bit UUID, * but the returned UUID is always in 128-bit format. * Note UUID is little endian in Bluetooth. * * @param uuidBytes Byte representation of uuid. * @return {@link ParcelUuid} parsed from bytes. * @throws IllegalArgumentException If the {@code uuidBytes} cannot be parsed. * * Copied from java/android/bluetooth/BluetoothUuid.java * Copyright (C) 2009 The Android Open Source Project * Licensed under the Apache License, Version 2.0 */ private static ParcelUuid parseUuidFrom(byte[] uuidBytes) { /** Length of bytes for 16 bit UUID */ final int UUID_BYTES_16_BIT = 2; /** Length of bytes for 32 bit UUID */ final int UUID_BYTES_32_BIT = 4; /** Length of bytes for 128 bit UUID */ final int UUID_BYTES_128_BIT = 16; final ParcelUuid BASE_UUID = ParcelUuid.fromString("00000000-0000-1000-8000-00805F9B34FB"); if (uuidBytes == null) { throw new IllegalArgumentException("uuidBytes cannot be null"); } int length = uuidBytes.length; if (length != UUID_BYTES_16_BIT && length != UUID_BYTES_32_BIT && length != UUID_BYTES_128_BIT) { throw new IllegalArgumentException("uuidBytes length invalid - " + length); } // Construct a 128 bit UUID. if (length == UUID_BYTES_128_BIT) { ByteBuffer buf = ByteBuffer.wrap(uuidBytes).order(ByteOrder.LITTLE_ENDIAN); long msb = buf.getLong(8); long lsb = buf.getLong(0); return new ParcelUuid(new UUID(msb, lsb)); } // For 16 bit and 32 bit UUID we need to convert them to 128 bit value. // 128_bit_value = uuid * 2^96 + BASE_UUID long shortUuid; if (length == UUID_BYTES_16_BIT) { shortUuid = uuidBytes[0] & 0xFF; shortUuid += (uuidBytes[1] & 0xFF) << 8; } else { shortUuid = uuidBytes[0] & 0xFF ; shortUuid += (uuidBytes[1] & 0xFF) << 8; shortUuid += (uuidBytes[2] & 0xFF) << 16; shortUuid += (uuidBytes[3] & 0xFF) << 24; } long msb = BASE_UUID.getUuid().getMostSignificantBits() + (shortUuid << 32); long lsb = BASE_UUID.getUuid().getLeastSignificantBits(); return new ParcelUuid(new UUID(msb, lsb)); } }