package com.uriio.beacons.ble.gatt; import android.annotation.SuppressLint; import android.bluetooth.le.AdvertiseSettings; import android.util.Base64; import com.uriio.beacons.BuildConfig; import com.uriio.beacons.Storage; import com.uriio.beacons.Util; import com.uriio.beacons.ble.Advertiser; import com.uriio.beacons.ble.AdvertisersManager; import com.uriio.beacons.ble.EddystoneAdvertiser; import com.uriio.beacons.model.EddystoneBase; import com.uriio.beacons.model.EddystoneEID; import com.uriio.beacons.model.EddystoneTLM; import com.uriio.beacons.model.EddystoneUID; import com.uriio.beacons.model.EddystoneURL; import java.nio.ByteBuffer; import java.util.Arrays; /** * Manages a GATT-configurable Eddystone beacon. * Created on 5/25/2016. */ class EddystoneGattConfigurator implements EddystoneGattConfigCallback { private static final String TAG = "EddystoneGattConfig"; private static final boolean D = BuildConfig.DEBUG; /** * The configured beacon. This will never broadcast during configuration, because starting * or stopping any LE advertiser kills the GATT connection and Beacon Tools crashes during EID registration. */ private EddystoneBase mConfiguredBeacon = null; private final EddystoneBase mOriginalBeacon; private boolean mIsAdvertisingSet = false; public EddystoneGattConfigurator(EddystoneBase beacon) { mOriginalBeacon = beacon; } /** * @return Current beacon configuration, or null if advertising data is not yet set. */ public EddystoneBase getConfiguredBeacon() { return mIsAdvertisingSet ? getModifiedOrOriginalBeacon() : null; } @Override public byte[] getAdvertisedData() { // use either the currently altered beacon, or the original one EddystoneBase beacon = getModifiedOrOriginalBeacon(); if (beacon.getKind() == Storage.KIND_EDDYSTONE_EID) { EddystoneEID eddystoneEID = (EddystoneEID) this.mConfiguredBeacon; EddystoneAdvertiser advertiser = (EddystoneAdvertiser) eddystoneEID.recreateAdvertiser(null); if (null == advertiser) { return new byte[0]; } ByteBuffer buffer = ByteBuffer.allocate(14); buffer.put((byte) 0x30); buffer.put(eddystoneEID.getRotationExponent()); buffer.putInt(eddystoneEID.getEidClock()); buffer.put(advertiser.getServiceData(), 2, 8); return buffer.array(); } EddystoneAdvertiser advertiser = (EddystoneAdvertiser) beacon.recreateAdvertiser(null); return null == advertiser ? new byte[0] : advertiser.getServiceData(); } @Override public void advertiseURL(String url) { EddystoneBase beacon = getModifiedOrOriginalBeacon(); boolean sameType = beacon.getKind() == Storage.KIND_EDDYSTONE_URL; boolean changed = !sameType; if (sameType) { String currentUrl = ((EddystoneURL) beacon).getURL(); changed = (null == url && null != currentUrl) || (null != url && !url.equals(currentUrl)); } if (changed) { if (null == mConfiguredBeacon || !sameType) { mConfiguredBeacon = new EddystoneURL(url, beacon.getLockKey(), beacon.getAdvertiseMode(), beacon.getTxPowerLevel(), beacon.getName()); } else { ((EddystoneURL) mConfiguredBeacon).edit().setUrl(url).apply(); } } mIsAdvertisingSet = true; } @Override public void advertiseUID(byte[] namespaceInstance) { EddystoneBase beacon = getModifiedOrOriginalBeacon(); boolean sameType = beacon.getKind() == Storage.KIND_EDDYSTONE_UID; boolean changed = !sameType; if (sameType) { byte[] current = ((EddystoneUID) beacon).getNamespaceInstance(); changed = !Arrays.equals(current, namespaceInstance); } if (changed) { if (null == mConfiguredBeacon || beacon.getKind() != Storage.KIND_EDDYSTONE_UID) { mConfiguredBeacon = new EddystoneUID(namespaceInstance, null, beacon.getLockKey(), beacon.getAdvertiseMode(), beacon.getTxPowerLevel(), beacon.getName()); } else { ((EddystoneUID) mConfiguredBeacon).edit().setNamespaceInstance(namespaceInstance).apply(); } } mIsAdvertisingSet = true; } @Override public void advertiseTLM() { EddystoneBase beacon = getModifiedOrOriginalBeacon(); if (beacon.getKind() != Storage.KIND_EDDYSTONE_TLM) { if (null == mConfiguredBeacon) { mConfiguredBeacon = new EddystoneTLM(60000, beacon.getLockKey(), beacon.getAdvertiseMode(), beacon.getTxPowerLevel(), beacon.getName()); } } mIsAdvertisingSet = true; } @Override public void advertiseEID(byte[] identityKey, byte rotationExponent) { EddystoneBase beacon = getModifiedOrOriginalBeacon(); boolean sameType = beacon.getKind() == Storage.KIND_EDDYSTONE_EID; boolean changed = !sameType; if (sameType) { EddystoneEID eidBeacon = (EddystoneEID) beacon; changed = rotationExponent != eidBeacon.getRotationExponent() || !Arrays.equals(identityKey, eidBeacon.getIdentityKey()); } if (changed) { // NOTE - because we don't have the EID clock offset from real-time, used at registration, // this call assumes that a new beacon has just been EID registered, or that registration // is in progress. The time counter is initialized to 65280, and to account for this, // the actual offset to real time is saved as a persistent property for future beacon restarts // Latency to HTTP calls for registration, or a wrong device time setting, may impact EID accuracy. int now = (int) (System.currentTimeMillis() / 1000); // https://github.com/google/eddystone/blob/master/eddystone-eid/eid-computation.md#implementation-guidelines int timeCounter = now & ~0xffff | 65280; // save the offset between current time and time counter so we can restore correctly int clockOffset = now - timeCounter; mConfiguredBeacon = new EddystoneEID(identityKey, rotationExponent, clockOffset, beacon.getLockKey(), beacon.getAdvertiseMode(), beacon.getTxPowerLevel(), beacon.getName()); if (D) Util.log(TAG, "advertiseEID clockOffset = " + clockOffset + " identityKey = " + Base64.encodeToString(identityKey, Base64.URL_SAFE) + " rotationExponent = " + rotationExponent); } mIsAdvertisingSet = true; } @Override public void stopAdvertise() { // we never actually advertise during config mIsAdvertisingSet = false; } @Override public byte[] getSupportedRadioTxPowers() { return AdvertisersManager.getSupportedRadioTxPowers(); } @Override public int getRadioTxPower() { return AdvertisersManager.getSupportedRadioTxPowers()[getModifiedOrOriginalBeacon().getTxPowerLevel()]; } @Override public int getAdvertisedTxPower() { return AdvertisersManager.getZeroDistanceTxPower(getModifiedOrOriginalBeacon().getTxPowerLevel()); } @Override public byte[] getEidIdentityKey() { EddystoneBase beacon = getModifiedOrOriginalBeacon(); if (beacon.getKind() == Storage.KIND_EDDYSTONE_EID) { return ((EddystoneEID) beacon).getIdentityKey(); } return null; } @Override public int setRadioTxPower(byte txPower) { byte[] txPowers = getSupportedRadioTxPowers(); @Advertiser.Power int txPowerLevel = 0; for (int i = 0; i < txPowers.length; i++) { if (txPower >= txPowers[i]) { //noinspection WrongConstant txPowerLevel = i; } } if (txPowerLevel != getModifiedOrOriginalBeacon().getTxPowerLevel()) { // restarting a beacon destroys the GATT connection, make sure we use a stopped clone getOrCloneConfiguredBeacon().edit().setAdvertiseTxPower(txPowerLevel).apply(); } return txPowers[txPowerLevel]; } @SuppressLint("InlinedApi") @Override public int setAdvertiseInterval(int advertiseIntervalMs) { Util.log(TAG, "setAdvertiseInterval() called with: advertiseIntervalMs = [" + advertiseIntervalMs + "]"); @Advertiser.Mode int mode; if (advertiseIntervalMs <= 100 + (250 - 100) / 2) { // 100 ms mode = AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY; } else if (advertiseIntervalMs >= 1000 - (1000 - 250) / 2) { // 1000 ms mode = AdvertiseSettings.ADVERTISE_MODE_LOW_POWER; } else { // 250 ms actually mode = AdvertiseSettings.ADVERTISE_MODE_BALANCED; } if (mode != getModifiedOrOriginalBeacon().getAdvertiseMode()) { // restarting a beacon destroys the GATT connection, make sure we use a stopped clone getOrCloneConfiguredBeacon().edit().setAdvertiseMode(mode).apply(); } return getAdvertiseInterval(); } @Override public int getAdvertiseInterval() { switch (getModifiedOrOriginalBeacon().getAdvertiseMode()) { case AdvertiseSettings.ADVERTISE_MODE_LOW_POWER: default: return 1000; case AdvertiseSettings.ADVERTISE_MODE_BALANCED: return 250; case AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY: return 100; } } @Override public byte[] getLockKey() { return getModifiedOrOriginalBeacon().getLockKey(); } @Override public void setLockKey(byte[] lockKey) { // in case beacon is not yet modified, lock key is saved to original // if beacon is modified, the new lock key will be copied anyway when it's created getModifiedOrOriginalBeacon().edit().setLockKey(lockKey).apply(); } private EddystoneBase getModifiedOrOriginalBeacon() { return null != mConfiguredBeacon ? mConfiguredBeacon : mOriginalBeacon; } private EddystoneBase getOrCloneConfiguredBeacon() { if (null == mConfiguredBeacon) { mConfiguredBeacon = mOriginalBeacon.cloneBeacon(); } return mConfiguredBeacon; } }