package com.uriio.beacons.ble; import android.annotation.TargetApi; 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.ParcelUuid; import android.os.SystemClock; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.util.Log; import com.uriio.beacons.BuildConfig; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.UUID; /** Base class for BLE advertisers. */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public abstract class Advertiser extends AdvertiseCallback { // Some ugly decorator definitions... @Retention(RetentionPolicy.SOURCE) @IntDef({ AdvertiseSettings.ADVERTISE_MODE_LOW_POWER, AdvertiseSettings.ADVERTISE_MODE_BALANCED, AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY }) public @interface Mode {} @Retention(RetentionPolicy.SOURCE) @IntDef({ AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW, AdvertiseSettings.ADVERTISE_TX_POWER_LOW, AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM, AdvertiseSettings.ADVERTISE_TX_POWER_HIGH }) public @interface Power {} public interface SettingsProvider { @Mode int getAdvertiseMode(); @Power int getTxPowerLevel(); int getTimeout(); boolean isConnectable(); } public static final int STATUS_WAITING = 0; public static final int STATUS_RUNNING = 1; public static final int STATUS_FAILED = 2; public static final int STATUS_STOPPED = 3; private static final String TAG = "Advertiser"; /** Milliseconds between two advertisements, for each Mode */ // todo - based on Nexus 6. Other devices may behave differently - how do we get these values? private static final int[] PDU_INTERVALS = { 1000, 250, 100 }; private final AdvertiseSettings mAdvertiseSettings; private AdvertisersManager mAdvertisersManager = null; private AdvertiseSettings mSettingsInEffect = null; private int mStatus = STATUS_WAITING; private long mUnclearedPDUCount = 0; private long mLastPDUUpdateTime = 0; /** * Creates a ParcelUUID for a 16-bit or 32-bit short UUID * @param serviceId Short UUID, either 16 or 32-bit * @return The corresponding 128-bit parcelled UUID */ @NonNull public static ParcelUuid parcelUuidFromShortUUID(long serviceId) { return new ParcelUuid(new UUID(0x1000 | (serviceId << 32), 0x800000805F9B34FBL)); } public static int[] getPduIntervals() { return PDU_INTERVALS; } public Advertiser(SettingsProvider provider) { mAdvertiseSettings = new AdvertiseSettings.Builder() .setAdvertiseMode(provider.getAdvertiseMode()) .setTxPowerLevel(provider.getTxPowerLevel()) .setConnectable(provider.isConnectable()) // oups! https://code.google.com/p/android/issues/detail?id=232219 // .setTimeout(provider.getTimeout()) .build(); } @Override public void onStartSuccess(AdvertiseSettings settingsInEffect) { mStatus = STATUS_RUNNING; mSettingsInEffect = settingsInEffect; // on start or restart, rebase the clock time used for PDU count estimation mLastPDUUpdateTime = SystemClock.elapsedRealtime(); if (null != mAdvertisersManager) { mAdvertisersManager.onAdvertiserStarted(this); } } @Override public void onStartFailure(int errorCode) { if(BuildConfig.DEBUG) { Log.d(TAG, "Start/stop failed " + errorCode + " - " + getErrorName(errorCode)); } mStatus = STATUS_FAILED; if (null != mAdvertisersManager) { mAdvertisersManager.onAdvertiserFailed(this, errorCode); } } public void setManager(AdvertisersManager advertiseManager) { mAdvertisersManager = advertiseManager; } public AdvertiseSettings getAdvertiseSettings() { return mAdvertiseSettings; } public abstract AdvertiseData getAdvertiseData(); public AdvertiseData getAdvertiseScanResponse() { return null; } public String getAdvertisedLocalName() { return null; } public AdvertiseSettings getSettingsInEffect() { return mSettingsInEffect; } public int getStatus() { return mStatus; } public static String getErrorName(int errorCode) { switch (errorCode) { case ADVERTISE_FAILED_FEATURE_UNSUPPORTED: return "ADVERTISE_FAILED_FEATURE_UNSUPPORTED"; case ADVERTISE_FAILED_TOO_MANY_ADVERTISERS: return "ADVERTISE_FAILED_TOO_MANY_ADVERTISERS"; case ADVERTISE_FAILED_ALREADY_STARTED: return "ADVERTISE_FAILED_ALREADY_STARTED"; case ADVERTISE_FAILED_DATA_TOO_LARGE: return "ADVERTISE_FAILED_DATA_TOO_LARGE"; case ADVERTISE_FAILED_INTERNAL_ERROR: return "ADVERTISE_FAILED_INTERNAL_ERROR"; } return "Error " + errorCode; } /** * Attempt to start BLE advertising. * @param bleAdvertiser BLE advertiser * @return True if no exception occurred while trying to start advertising. */ boolean start(BluetoothLeAdvertiser bleAdvertiser) { try { bleAdvertiser.startAdvertising(getAdvertiseSettings(), getAdvertiseData(), getAdvertiseScanResponse(), this); } catch (IllegalStateException e) { // tried to start advertising after Bluetooth was turned off // let upper level notice that BT is off instead of reporting an error if (BuildConfig.DEBUG) { Log.e(TAG, "start", e); } return false; } return true; } /** * Mark the advertiser as stopped and attempt to actually stop BLE advertisements. * @param bleAdvertiser BLE advertiser, or null * @return True if there was no error while trying to stop the Bluetooth advertiser. */ boolean stop(BluetoothLeAdvertiser bleAdvertiser) { updateEstimatedPDUCount(); mStatus = STATUS_STOPPED; if (null != bleAdvertiser) { bleAdvertiser.stopAdvertising(this); } return true; } /** * Updates the estimated transmitted packet data units and clears the internal counter. * @return Estimated advertised PDUs since the last update (or since the advertiser started). */ public long clearPDUCount() { updateEstimatedPDUCount(); long pduCount = mUnclearedPDUCount; mUnclearedPDUCount = 0; return pduCount; } private void updateEstimatedPDUCount() { if (STATUS_RUNNING == mStatus) { long now = SystemClock.elapsedRealtime(); int mode = mSettingsInEffect.getMode(); mUnclearedPDUCount += Math.max(1, (now - mLastPDUUpdateTime) / PDU_INTERVALS[mode]); mLastPDUUpdateTime = now; } } }