public static interface BatteryLevelCallback {
    public void onBatteryLevel(int percentage);
} public class GattClient {

    private static final String TAG = "GattClient";

    // Profiles
    private final GattSerialTransportProfile mSerialProfile;
    private final DeviceProfile mDeviceProfile;
    private final BatteryProfile mBatteryProfile;
    private final OADProfile mOADProfile;
    private final ScratchProfile mScratchProfile;
    private List<BaseProfile> mProfiles = new ArrayList<>(10);

    // Internal dependencies
    private BluetoothGatt mGatt;
    private ConnectionListener connectionListener;
    private BluetoothDevice device;

    // Internal state
    private Queue<Runnable> mOperationsQueue = new ArrayDeque<>(32);
    private boolean mOperationInProgress = false;
    private boolean mConnected = false;

    public GattClient(Handler handler, BluetoothDevice device) {
        this.device = device;
        mSerialProfile = new GattSerialTransportProfile(this, handler);
        mDeviceProfile = new DeviceProfile(this);
        mBatteryProfile = new BatteryProfile(this);
        mOADProfile = new OADProfile(this, new Watchdog(handler));
        mScratchProfile = new ScratchProfile(this);
        mProfiles.add(mSerialProfile);
        mProfiles.add(mDeviceProfile);
        mProfiles.add(mBatteryProfile);
        mProfiles.add(mScratchProfile);
        mProfiles.add(mOADProfile);
    }

    private BaseProfile profileForUUID(UUID uuid) throws UnimplementedProfileException {
        if (uuid.equals(Constants.UUID_OAD_SERVICE)) {
            return mOADProfile;
        } else if (uuid.equals(Constants.UUID_SERIAL_SERVICE)) {
            return mSerialProfile;
        } else if (uuid.equals(Constants.UUID_BATTERY_SERVICE)) {
            return mBatteryProfile;
        } else if (uuid.equals(Constants.UUID_SCRATCH_SERVICE)) {
            return mScratchProfile;
        } else if (uuid.equals(Constants.UUID_DEVICE_INFO_SERVICE)) {
            return mDeviceProfile;
        } else {
            throw new UnimplementedProfileException("No profile with UUID: " + uuid.toString());
        }
    }

    /**
     * Attempt to invalidate Androids internal GATT table cache
     *
     * http://stackoverflow.com/a/22709467/5640435
     *
     * @param gatt BluetoothGatt object
     */
    private void refreshDeviceCache(BluetoothGatt gatt) {
        try {
            BluetoothGatt localBluetoothGatt = gatt;
            Method localMethod = localBluetoothGatt.getClass().getMethod("refresh", new Class[0]);
            if (localMethod != null) {
                localMethod.invoke(localBluetoothGatt, new Object[0]);
            } else {
                Log.e(TAG, "Couldn't find local method: refresh");
            }
        }
        catch (Exception localException) {
            Log.e(TAG, "An exception occurred while refreshing device");
        }
    }

    private void describeService(BluetoothGattService service) {
        Log.i(TAG, "Service Found: " + service.getUuid().toString());
        for (BluetoothGattCharacteristic c : service.getCharacteristics()) {
            Log.i(TAG, "    Char: " + c.getUuid().toString());
        }
    }

    private final BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() {

        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {

            if (status != BluetoothGatt.GATT_SUCCESS) {
                if (getOADProfile().uploadInProgress()) {
                    // Since an OAD update is currently in progress, only alert the OAD Profile
                    // of the Bean disconnecting, not the ConnectionListener(s)
                    getOADProfile().onBeanConnectionFailed();
                } else {
                    connectionListener.onConnectionFailed();
                }

                mConnected = false;
                return;
            }

            if (newState == BluetoothGatt.STATE_CONNECTED) {
                mConnected = true;

                // Bean is connected, before alerting the ConnectionListener(s), we must
                // discover available services (lookup GATT table).
                Log.i(TAG, "Discovering Services!");
                mGatt.discoverServices();
            }

            if (newState == BluetoothGatt.STATE_DISCONNECTED) {
                mOperationsQueue.clear();
                mOperationInProgress = false;
                mConnected = false;
                connectionListener.onDisconnected();
                for (BaseProfile profile : mProfiles) {
                    profile.onBeanDisconnected();
                }
            }
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {

            if (status != BluetoothGatt.GATT_SUCCESS) {
                Log.e(TAG, "Failed to discover services!");
                disconnect();
            } else {
                Log.i(TAG, "Service discovery complete!");

                for (BaseProfile profile : mProfiles) {
                    profile.clearReady();
                }

                // Tell each profile that they are ready and to do any other further configuration
                // that may be necessary such as looking up available characteristics.
                Log.i(TAG, "Starting to setup each available profile!");
                for (BluetoothGattService service : mGatt.getServices()) {
                    try {
                        BaseProfile profile = profileForUUID(service.getUuid());
                        profile.onProfileReady();
                        Log.i(TAG, "Profile ready: " + profile.getName());
                    } catch (UnimplementedProfileException e) {
                        Log.e(TAG, "No profile with UUID: " + service.getUuid().toString());
                    }
                }

                if (mOADProfile.uploadInProgress()) {
                    Log.i(TAG, "OAD In progress, continuing OAD process without calling ConnectionListener.onConnected()");
                    mOADProfile.continueOAD();
                } else {

                    for (BaseProfile profile : mProfiles) {
                        if (!profile.isReady()) {
                            Log.e(TAG, "Profile NOT Discovered: " + profile.getName());
                        }
                    }

                    connectionListener.onConnected();
                }
            }
        }

        @Override
        public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            if (status != BluetoothGatt.GATT_SUCCESS) {
                disconnect();
            } else {
                fireCharacteristicsRead(characteristic);
                executeNextOperation();
            }
        }

        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            if (status != BluetoothGatt.GATT_SUCCESS) {
                disconnect();
            } else {
                fireCharacteristicWrite(characteristic);
                executeNextOperation();
            }
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
            fireCharacteristicChanged(characteristic);
        }

        @Override
        public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            if (status != BluetoothGatt.GATT_SUCCESS) {
                disconnect();
                return;
            }
            fireDescriptorRead(descriptor);
            executeNextOperation();
        }

        @Override
        public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            if (status != BluetoothGatt.GATT_SUCCESS) {
                disconnect();
                return;
            }
            fireDescriptorWrite(descriptor);
            executeNextOperation();
        }

        @Override
        public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
            if (status != BluetoothGatt.GATT_SUCCESS) {
                disconnect();
                return;
            }
            fireReadRemoteRssi(rssi);
            executeNextOperation();
        }
    };

    private void fireDescriptorRead(BluetoothGattDescriptor descriptor) {
        for (BaseProfile profile : mProfiles) {
            profile.onDescriptorRead(this, descriptor);
        }
    }

    private synchronized void queueOperation(Runnable operation) {
        mOperationsQueue.offer(operation);
        if (!mOperationInProgress) {
            executeNextOperation();
        }
    }

    private synchronized void executeNextOperation() {
        Runnable operation = mOperationsQueue.poll();
        if (operation != null) {
            mOperationInProgress = true;
            operation.run();
        } else {
            mOperationInProgress = false;
        }
    }

    private void fireDescriptorWrite(BluetoothGattDescriptor descriptor) {
        for (BaseProfile profile : mProfiles) {
            profile.onDescriptorWrite(this, descriptor);
        }
    }

    private void fireCharacteristicChanged(BluetoothGattCharacteristic characteristic) {
        for (BaseProfile profile : mProfiles) {
            profile.onCharacteristicChanged(this, characteristic);
        }
    }

    private void fireCharacteristicWrite(BluetoothGattCharacteristic characteristic) {
        for (BaseProfile profile : mProfiles) {
            profile.onCharacteristicWrite(this, characteristic);
        }
    }

    private void fireCharacteristicsRead(BluetoothGattCharacteristic characteristic) {
        for (BaseProfile profile : mProfiles) {
            profile.onCharacteristicRead(this, characteristic);
        }
    }

    private void fireReadRemoteRssi(int rssi) {
        for (BaseProfile profile : mProfiles) {
            profile.onReadRemoteRssi(this, rssi);
        }
    }

    /****************************************************************************
                                  PUBLIC API
     ****************************************************************************/

    public String bleAddress() {
        return device.getAddress();
    }

    public void connect(Context context, BluetoothDevice device) {
        if (mGatt != null) {
            mGatt.disconnect();
            mGatt.close();
            mConnected = false;
        }

        Log.i(TAG, "Gatt connection started");
        mGatt = device.connectGatt(context, false, mBluetoothGattCallback);
        Log.i(TAG, "Refreshing GATT Cache");
        refreshDeviceCache(mGatt);
    }

    /**
     * Sets a listener that will be alerted on connection related events
     *
     * @param listener ConnectionListener object
     */
    public void setListener(ConnectionListener listener) {
        this.connectionListener = listener;
    }

    public boolean isConnected() {
        return mConnected;
    }

    public List<BluetoothGattService> getServices() {
        return mGatt.getServices();
    }

    public BluetoothGattService getService(UUID uuid) {
        return mGatt.getService(uuid);
    }

    public synchronized boolean readCharacteristic(final BluetoothGattCharacteristic characteristic) {
        queueOperation(new Runnable() {
            @Override
            public void run() {
                if (mGatt != null) {
                    mGatt.readCharacteristic(characteristic);
                }
            }
        });
        return true;
    }

    public synchronized boolean writeCharacteristic(final BluetoothGattCharacteristic characteristic) {
        final byte[] value = characteristic.getValue();
        queueOperation(new Runnable() {
            @Override
            public void run() {
                if (mGatt != null) {
                    characteristic.setValue(value);
                    mGatt.writeCharacteristic(characteristic);
                }
            }
        });
        return true;
    }

    public boolean readDescriptor(final BluetoothGattDescriptor descriptor) {
        queueOperation(new Runnable() {
            @Override
            public void run() {
                if (mGatt != null) {
                    mGatt.readDescriptor(descriptor);
                }
            }
        });
        return true;
    }

    public boolean writeDescriptor(final BluetoothGattDescriptor descriptor) {
        final byte[] value = descriptor.getValue();
        queueOperation(new Runnable() {
            @Override
            public void run() {
                if (mGatt != null) {
                    descriptor.setValue(value);
                    mGatt.writeDescriptor(descriptor);
                }
            }
        });
        return true;
    }

    public boolean readRemoteRssi() {
        return mGatt.readRemoteRssi();
    }

    public boolean setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enable) {
        return mGatt.setCharacteristicNotification(characteristic, enable);
    }

    public void disconnect() {
        mGatt.disconnect();
    }

    public synchronized void close() {
        if (mGatt != null) {
            mGatt.close();
        }
        mGatt = null;
    }

    public GattSerialTransportProfile getSerialProfile() {
        return mSerialProfile;
    }

    public DeviceProfile getDeviceProfile() {
        return mDeviceProfile;
    }

    public BatteryProfile getBatteryProfile() {
        return mBatteryProfile;
    }

    public OADProfile getOADProfile() {
        return mOADProfile;
    }

    // This listener is only for communicating with the Bean class
    public static interface ConnectionListener {
        public void onConnected();

        public void onConnectionFailed();

        public void onDisconnected();

    }
} public class DeviceProfile extends BaseProfile {

    protected static final String TAG = "DeviceProfile";

    private boolean ready = false;
    private String mSoftwareVersion;
    private String mHardwareVersion;
    private String mFirmwareVersion;
    private BluetoothGattService mDeviceService;
    private DeviceInfoCallback deviceInfoCallback;
    private VersionCallback firmwareVersionCallback;
    private VersionCallback hardwareVersionCallback;

    public DeviceProfile(GattClient client) {
        super(client);
    }

    @Override
    public void onProfileReady() {
        List<BluetoothGattService> services = mGattClient.getServices();
        for (BluetoothGattService service : services) {
            if (service.getUuid().equals(Constants.UUID_DEVICE_INFO_SERVICE)) {
                mDeviceService = service;
            }
        }
        ready = true;
    }

    @Override
    public void onCharacteristicRead(GattClient client, BluetoothGattCharacteristic characteristic) {

        if (characteristic.getUuid().equals(Constants.UUID_DEVICE_INFO_CHAR_FIRMWARE_VERSION)) {
            Log.i(TAG, "Read response (FW Version): " + Convert.bytesToHexString(characteristic.getValue()));
            mFirmwareVersion = characteristic.getStringValue(0);
        } else if (characteristic.getUuid().equals(Constants.UUID_DEVICE_INFO_CHAR_HARDWARE_VERSION)) {
            Log.i(TAG, "Read response (HW Version): " + Convert.bytesToHexString(characteristic.getValue()));
            mHardwareVersion = characteristic.getStringValue(0);
        } else if (characteristic.getUuid().equals(Constants.UUID_DEVICE_INFO_CHAR_SOFTWARE_VERSION)) {
            Log.i(TAG, "Read response (SW Version): " + Convert.bytesToHexString(characteristic.getValue()));
            mSoftwareVersion = characteristic.getStringValue(0);
        }

        if (mFirmwareVersion != null && mSoftwareVersion != null && mHardwareVersion != null && deviceInfoCallback != null) {
            DeviceInfo info = DeviceInfo.create(mHardwareVersion, mSoftwareVersion, mFirmwareVersion);
            deviceInfoCallback.onDeviceInfo(info);
            deviceInfoCallback = null;
        }

        if (mFirmwareVersion != null && firmwareVersionCallback != null) {
            firmwareVersionCallback.onComplete(mFirmwareVersion);
            firmwareVersionCallback = null;
        }

        if (mHardwareVersion != null && hardwareVersionCallback != null) {
            hardwareVersionCallback.onComplete(mHardwareVersion);
            hardwareVersionCallback = null;
        }
    }

    public void getDeviceInfo(DeviceInfoCallback callback) {
        deviceInfoCallback = callback;
        for (BluetoothGattCharacteristic characteristic : mDeviceService.getCharacteristics()) {
            if (characteristic.getUuid().equals(Constants.UUID_DEVICE_INFO_CHAR_FIRMWARE_VERSION) ||
                    characteristic.getUuid().equals(Constants.UUID_DEVICE_INFO_CHAR_HARDWARE_VERSION) ||
                    characteristic.getUuid().equals(Constants.UUID_DEVICE_INFO_CHAR_SOFTWARE_VERSION)) {
                mGattClient.readCharacteristic(characteristic);
            }
        }
    }

    public void getFirmwareVersion(VersionCallback callback) {
        firmwareVersionCallback = callback;
        mGattClient.readCharacteristic(
                mDeviceService.getCharacteristic(
                        Constants.UUID_DEVICE_INFO_CHAR_FIRMWARE_VERSION));
    }

    public void getHardwareVersion(VersionCallback callback) {
        hardwareVersionCallback = callback;
        mGattClient.readCharacteristic(
                mDeviceService.getCharacteristic(
                        Constants.UUID_DEVICE_INFO_CHAR_HARDWARE_VERSION));
    }

    public String getName() {
        return TAG;
    }

    public boolean isReady() {
        return ready;
    }

    public void clearReady() {
        ready = false;
    }

    public static interface VersionCallback {
        public void onComplete(String version);
    }

    public static interface DeviceInfoCallback {
        public void onDeviceInfo(DeviceInfo info);
    }
} public static interface DeviceInfoCallback {
    public void onDeviceInfo(DeviceInfo info);
} public class NoEnumFoundException extends Exception {
    public NoEnumFoundException(String detailMessage) {
        super(detailMessage);
    }
} public class GattSerialMessage {

    private static final String TAG = "GattSerialMessage";
    private final Buffer mBuffer;

    private GattSerialMessage(Buffer buffer) {
        this.mBuffer = buffer;
    }

    /**
     * Create a GattSerialMessage from a byte array payload
     *
     * In this case, "payload" means the properly packed message ID and message payload.
     *
     * Example: For the message CC_LED_WRITE_ALL (0x2001)
     *
     * GattSerialMessage.fromPayload(new byte[] {
     *        0x20, 0x01,                           <--- Message ID big endian
     *        0x00, 0x00, 0x00,                     <--- Payload data little endian
     * });
     *
     * @param payload    Byte array of the message payload
     */
    public static GattSerialMessage fromPayload(byte[] payload) {
        Buffer buffer = new Buffer();
        byte[] header = new byte[2];
        header[0] = (byte) (payload.length & 0xff);
        header[1] = 0;
        int crc = computeCRC16(header, 0, header.length);
        crc = computeCRC16(crc, payload, 0, payload.length);
        buffer.write(header);
        buffer.write(payload);
        buffer.writeByte(crc & 0xff);
        buffer.writeByte((crc >> 8) & 0xff);
        return new GattSerialMessage(buffer);
    }

    public static byte[] parse(byte[] payload) {
        int length = (payload[0] & 0xff);
        Buffer buffer = new Buffer();
        buffer.write(payload, 2, length);
        int crc = computeCRC16(payload, 0, payload.length - 2);
        int checkCrc = (((payload[payload.length - 1] & 0xff) << 8)) + (payload[payload.length - 2] & 0xff);
        if (crc != checkCrc) {
            Log.w(TAG, "Checksum failed");
            throw new IllegalStateException("Checksum mismatch");
        }
        return buffer.readByteArray();
    }

    static int computeCRC16(byte[] data, int offset, int length) {
        return computeCRC16(0xffff, data, offset, length);
    }

    static int computeCRC16(int startingCrc, byte[] data, int offset, int length) {

        int crc = (startingCrc & 0xffff);

        for (int i = offset; i < offset + length; i++) {
            crc = ((crc >> 8) & 0xffff) | ((crc << 8) & 0xffff);
            crc ^= data[i] & 0xff;
            crc &= 0xffff;
            crc ^= ((crc & 0xff) >> 4);
            crc &= 0xffff;
            crc ^= (crc << 8) << 4;
            crc &= 0xffff;
            crc ^= ((crc & 0xff) << 4) << 1;
            crc &= 0xffff;
        }

        return crc & 0xffff;
    }

    public Buffer getBuffer() {
        return mBuffer;
    }
} public class GattSerialTransportProfile extends BaseProfile {

    protected static final String TAG = "GattSerialXportProfile";

    // Constants
    public static final int PACKET_TX_MAX_PAYLOAD_LENGTH = 19;
    private static final List<UUID> BEAN_SCRATCH_UUIDS = Arrays.asList(
            Constants.UUID_SCRATCH_CHAR_1,
            Constants.UUID_SCRATCH_CHAR_2,
            Constants.UUID_SCRATCH_CHAR_3,
            Constants.UUID_SCRATCH_CHAR_4,
            Constants.UUID_SCRATCH_CHAR_5
    );

    // Internal dependencies
    private SerialListener mListener;
    private BluetoothGattCharacteristic mSerialCharacteristic;
    private Handler mHandler;
    private MessageAssembler mMessageAssembler = new MessageAssembler();

    // Internal state
    private boolean ready = false;
    private boolean mReadyToSend = false;
    private List<GattSerialPacket> mPendingPackets = new ArrayList<>(32);
    private int mOutgoingMessageCount = 0;

    private final Runnable mDequeueRunnable = new Runnable() {
        @Override
        public void run() {
            if (!mPendingPackets.isEmpty()) {
                if (mReadyToSend && mSerialCharacteristic != null) {
                    mReadyToSend = false;
                    GattSerialPacket packet = mPendingPackets.remove(0);
                    mSerialCharacteristic.setValue(packet.getPacketData());
                    if (!mGattClient.writeCharacteristic(mSerialCharacteristic)) {
                        Log.e(TAG, "Failed char write");
                    }
                } else {
                    mHandler.postDelayed(this, 150);
                }
            }
        }
    };

    public GattSerialTransportProfile(GattClient client, Handler handler) {
        super(client);
        mHandler = handler;
    }

    @Override
    public void onProfileReady() {

        BluetoothGattService service = mGattClient.getService(Constants.UUID_SERIAL_SERVICE);
        mSerialCharacteristic = service.getCharacteristic(Constants.UUID_SERIAL_CHAR);
        if (mSerialCharacteristic == null) {
            Log.w(TAG, "Did not find bean serial on device");
            abort("Did not find bean serial on device");
        } else {

            // Enable Notifications for Serial chars
            mGattClient.setCharacteristicNotification(mSerialCharacteristic, true);
            for (BluetoothGattDescriptor descriptor : mSerialCharacteristic.getDescriptors()) {
                if ((descriptor.getUuid().getMostSignificantBits() >> 32) == 0x2902) {
                    descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
                    mGattClient.writeDescriptor(descriptor);
                }
            }

            service = mGattClient.getService(Constants.UUID_SCRATCH_SERVICE);

            boolean hasScratchChars = true;

            for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
                if (characteristic.getDescriptors().size() < 2) {
                    /* This Bean has a scratch characteristic with less than two
                     * descriptors.
                     *
                     * This is a very old (probably factory-firmware) Bean. These Beans have
                     * malformed scratch characteristic tables. If Android writes to one of
                     * these Beans' scratch characteristic tables, it will close the
                     * connection.
                     *
                     * To fix this problem, update these Beans to the latest firmware before
                     * use.
                     */
                    hasScratchChars = false;
                }
            }

            if (hasScratchChars) {
                // Enable Notifications for Scratch chars
                for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
                    mGattClient.setCharacteristicNotification(characteristic, true);
                    for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors()) {
                        if ((descriptor.getUuid().getMostSignificantBits() >> 32) == 0x2902) {

                            descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
                            mGattClient.writeDescriptor(descriptor);
                        }
                    }
                }
            }

            mMessageAssembler.reset();
            mReadyToSend = true;
            mOutgoingMessageCount = 0;
            if (BuildConfig.DEBUG) {
                Log.i(TAG, "Setup complete");
            }
            ready = true;
        }
    }

    @Override
    public void onCharacteristicWrite(GattClient client, BluetoothGattCharacteristic characteristic) {
        if (mSerialCharacteristic == characteristic) {
            mHandler.removeCallbacks(mDequeueRunnable);
            mReadyToSend = true;
            mHandler.post(mDequeueRunnable);
        }
    }

    @Override
    public void onCharacteristicChanged(GattClient client, BluetoothGattCharacteristic characteristic) {
        if (characteristic == mSerialCharacteristic) {
            byte[] data = mMessageAssembler.assemble(new GattSerialPacket(characteristic.getValue()));
            if (data != null) {
                if (BuildConfig.DEBUG) {
                    Log.d(TAG, "Received data");
                }
                if (mListener != null) {
                    mListener.onMessageReceived(data);
                } else {
                    client.disconnect();
                }
            }
        } else {
            // scratch
            int index = BEAN_SCRATCH_UUIDS.indexOf(characteristic.getUuid());
            if (index > -1) {
                index += 1;
                if (BuildConfig.DEBUG) {
                    Log.d(TAG, "Received scratch bank update (" + index + ")");
                }
                if (mListener != null) {
                    try {
                        ScratchBank bank = EnumParse.enumWithRawValue(ScratchBank.class, index);
                        mListener.onScratchValueChanged(bank, characteristic.getValue());
                    } catch (NoEnumFoundException e) {
                        Log.e(TAG, "Couldn't parse bank enum from scratch bank with index " +
                                index);
                        e.printStackTrace();
                    }
                } else {
                    client.disconnect();
                }
            }
        }
    }

    @Override
    public void onReadRemoteRssi(GattClient client, int rssi) {
        final SerialListener listener = mListener;
        if (listener != null) {
            listener.onReadRemoteRssi(rssi);
        } else {
            abort("No listener, this must be a stale connection --> disconnect");
        }
    }

    public void abort(String message) {
        mListener.onError(message);
    }

    public void sendMessage(Buffer message) {
        if (mSerialCharacteristic == null) {
            Log.e(TAG, "Unexpected: mSerialCharacteristic is null");
        }

        // create packet, add to queue, schedule
        int packets = (int) (message.size() / PACKET_TX_MAX_PAYLOAD_LENGTH);
        mOutgoingMessageCount = (mOutgoingMessageCount + 1) % 4;
        int size = (int) message.size();
        for (int i = 0; i < size; i += PACKET_TX_MAX_PAYLOAD_LENGTH) {
            GattSerialPacket packet = new GattSerialPacket(i == 0, mOutgoingMessageCount, packets--, message);
            mPendingPackets.add(packet);
        }
        mHandler.post(mDequeueRunnable);
    }

    /**
     * Sets a listener that will be alerted for serial and scratch events
     *
     * @param listener SerialListener object
     */
    public void setListener(SerialListener listener) {
        this.mListener = listener;
    }

    public String getName() {
        return TAG;
    }

    public boolean isReady() {
        return ready;
    }

    public void clearReady() {
        ready = false;
    }

    // This listener is only for communicating with the Bean class
    public static interface SerialListener {

        public void onMessageReceived(byte[] data);

        public void onScratchValueChanged(ScratchBank bank, byte[] value);

        public void onError(String message);

        public void onReadRemoteRssi(int rssi);
    }
} public class OADProfile extends BaseProfile {
    /**
     * Custom OAD Profile for LightBlue Bean devices.
     *
     * This class encapsulates data and processes related to firmware updates to the CC2540.
     *
     */

    protected static final String TAG = "OADProfile";

    // OAD Characteristic handles
    private BluetoothGattCharacteristic oadIdentify;
    private BluetoothGattCharacteristic oadBlock;

    private Watchdog watchdog;

    // OAD Internal State

    private static final int OAD_TIMEOUT_SECONDS = 30;
    private boolean ready = false;

    /* The current state of the OAD state-machine */
    private OADState oadState = OADState.INACTIVE;

    /* The last offered image or the accepted image depending on state of the OAD process */
    private FirmwareImage currentImage;

    /* Bundle of firmware images provided by the client */
    private FirmwareBundle firmwareBundle;

    /* Oad Listener */
    private OADListener oadListener;

    /* The maximum allowed blocks queued and/or in-flight before receiving a new request */
    private final int MAX_IN_AIR_BLOCKS = 8;

    /* Keeps track of the next block to send which is not equal to the block requested */
    private int nextBlock = 0;

    /* Used to record KB/s during block transfers */
    private long blockTransferStarted = 0;

    public OADProfile(GattClient client, Watchdog watchdog) {
        super(client);
        this.watchdog = watchdog;
        reset();
    }

    private Watchdog.WatchdogListener watchdogListener = new Watchdog.WatchdogListener() {
        @Override
        public void expired() {
            fail(BeanError.OAD_TIMEOUT);
        }
    };

    private OADApproval oadApproval = new OADApproval() {

        public boolean approved = false;

        @Override
        public void allow() {
            Log.i(TAG, "Client has allowed the OAD Process to continue.");
            approved = true;
            startOfferingImages();
            watchdog.start(OAD_TIMEOUT_SECONDS, watchdogListener);
        }

        @Override
        public void deny() {
            Log.i(TAG, "Client denied the OAD Process from continuing.");
            approved = false;
            fail(BeanError.CLIENT_REJECTED);
        }

        @Override
        public void reset() {
            approved = false;
        }

        @Override
        public boolean isApproved() {
            return approved;
        }
    };

    private void setState(OADState state) {
        Log.i(TAG, String.format("(%s) OAD State Change: %s -> %s", mGattClient.bleAddress(), oadState.name(), state.name()));
        oadState = state;
        watchdog.poke();
        if (oadListener != null ) {
            oadListener.stateChange(state);
        }
    }

    /**
     * Set the state to INACTIVE and clear state variables
     */
    private void reset() {
        setState(OADState.INACTIVE);
        currentImage = null;
        firmwareBundle = null;
        nextBlock = 0;
        oadListener = null;
        watchdog.stop();
        oadApproval.reset();
    }

    /**
     * Attempt to reconnect to a Bean that is in the middle of OAD process
     */
    private void reconnect() {
        if(uploadInProgress()) {
            setState(OADState.RECONNECTING);
            BeanManager.getInstance().startDiscovery();
            Log.i(TAG, "Waiting for device to reconnect...");
        }
    }

    /**
     * Offer the next image available in the Firmware Bundle
     */
    private void offerNextImage() {
        if (oadState == OADState.OFFERING_IMAGES) {
            try {
                currentImage = firmwareBundle.getNextImage();
                if (currentImage != null) {
                    Log.i(TAG, "Offering image: " + currentImage.name());
                    writeToCharacteristic(oadIdentify, currentImage.metadata());
                }
            } catch (OADException e) {
                // This gets thrown if the firmware bundle is "exhausted", meaning the Bean
                // has rejected all of the images in the bundle
                Log.e(TAG, e.getMessage());
                fail(BeanError.BEAN_REJECTED_FW);
            }
        } else {
            Log.e(TAG, "Got notification on OAD Identify while in unexpected state: " + oadState);
        }
    }

    /**
     * Begin the OFFERING_IMAGES state
     */
    private void startOfferingImages() {
        setState(OADState.OFFERING_IMAGES);
        currentImage = null;
        firmwareBundle.reset();
        offerNextImage();
    }

    /**
     *  Received a notification on Identify characteristic
     *
     * @param characteristic Not used
     */
    private void onNotificationIdentify(BluetoothGattCharacteristic characteristic) {
        offerNextImage();
    }

    /**
     * Received a notification on Block characteristic
     *
     * A notification to this characteristic means the Bean has accepted the most recent
     * firmware file we have offered, which is stored as `this.currentImage`. It is now
     * time to start sending blocks of FW to the device.
     *
     * @param characteristic BLE characteristic with a value equal to the the block number
     */
    private void onNotificationBlock(BluetoothGattCharacteristic characteristic) {

        int requestedBlock = Convert.twoBytesToInt(characteristic.getValue(), Constants.CC2540_BYTE_ORDER);

        // Check for First block
        if (requestedBlock == 0) {
            Log.i(TAG, String.format("Image accepted (Name: %s) (Size: %s bytes)",currentImage.name(), currentImage.sizeBytes()));
            blockTransferStarted = System.currentTimeMillis() / 1000L;
            setState(OADState.BLOCK_XFER);
            nextBlock = 0;
        }

        // Normal BLOCK XFER state logic
        while (oadState == OADState.BLOCK_XFER &&
               nextBlock <= currentImage.blockCount() - 1 &&
               nextBlock < (requestedBlock + MAX_IN_AIR_BLOCKS)) {

            // Write the block, tell the OAD Listener
            writeToCharacteristic(oadBlock, currentImage.block(nextBlock));
            oadListener.progress(UploadProgress.create(nextBlock + 1, currentImage.blockCount()));
            nextBlock++;
            watchdog.poke();
        }

        // Check for final block requested, for logging purposes only
        if (requestedBlock == currentImage.blockCount() - 1) {

            // Calculate throughput
            long secondsElapsed = System.currentTimeMillis() / 1000L - blockTransferStarted;
            double KBs = 0;
            if (secondsElapsed > 0) {
                KBs = (double) (currentImage.sizeBytes() / secondsElapsed) / 1000;
            }

            // Log some stats
            Log.i(TAG, String.format("Final OAD Block Requested: %s/%s", nextBlock, currentImage.blockCount()));
            Log.i(TAG, String.format("Sent %d blocks in %d seconds (%.2f KB/s)", currentImage.blockCount(), secondsElapsed, KBs));
        }

    }

    /**
     * Setup BLOCK and IDENTIFY characteristics
     */
    private void setupOAD() {
        BluetoothGattService oadService = mGattClient.getService(Constants.UUID_OAD_SERVICE);
        if (oadService == null) {
            fail(BeanError.MISSING_OAD_SERVICE);
            return;
        }

        oadIdentify = oadService.getCharacteristic(Constants.UUID_OAD_CHAR_IDENTIFY);
        if (oadIdentify == null) {
            fail(BeanError.MISSING_OAD_IDENTIFY);
            return;
        }

        oadBlock = oadService.getCharacteristic(Constants.UUID_OAD_CHAR_BLOCK);
        if (oadBlock == null) {
            fail(BeanError.MISSING_OAD_BLOCK);
            return;
        }
    }

    /**
     * Enables notifications for all OAD characteristics.
     */
    private void setupNotifications() {

        Log.i(TAG, "Enabling OAD notifications");

        boolean oadIdentifyNotifying = enableNotifyForChar(oadIdentify);
        boolean oadBlockNotifying = enableNotifyForChar(oadBlock);

        if (oadIdentifyNotifying && oadBlockNotifying) {
            Log.i(TAG, "Enable notifications successful");
        } else {
            Log.e(TAG, "Error while enabling notifications");
            fail(BeanError.ENABLE_OAD_NOTIFY_FAILED);
        }
    }

    /**
     * Enable notifications for a given characteristic.
     *
     * See <a href="https://developer.android.com/guide/topics/connectivity/bluetooth-le.html#notification">
     *     the Android docs
     * </a>
     * on this subject.
     *
     * @param characteristic    The characteristic to enable notifications for
     * @return                  true if notifications were enabled successfully
     */
    private boolean enableNotifyForChar(BluetoothGattCharacteristic characteristic) {

        boolean success = true;

        // Enable notifications/indications for this characteristic
        boolean successEnable = mGattClient.setCharacteristicNotification(characteristic, true);
        if (successEnable) {
            Log.i(TAG, "Enabled notify for characteristic: " + characteristic.getUuid());
        } else {
            success = false;
            Log.e(TAG, "Enable notify failed for characteristic: " + characteristic.getUuid());
        }

        BluetoothGattDescriptor descriptor = characteristic.getDescriptor(Constants.UUID_CLIENT_CHAR_CONFIG);
        descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
        boolean successDescriptor = mGattClient.writeDescriptor(descriptor);

        if (successDescriptor) {
            Log.i(TAG, "Successfully wrote notification descriptor: " + descriptor.getUuid());
        } else {
            success = false;
            Log.e(TAG, "Failed to write notification descriptor: " + descriptor.getUuid());
        }

        return success;
    }

    /**
     * Write to a OAD characteristic
     *
     * @param charc The characteristic being inspected
     * @return      true if it's the OAD Block characteristic
     */
    private boolean writeToCharacteristic(BluetoothGattCharacteristic charc, byte[] data) {
        charc.setValue(data);
        boolean result = mGattClient.writeCharacteristic(charc);
        if (result) {
            Log.d(TAG, "Wrote to characteristic: " + charc.getUuid() +
                    ", data: " + Arrays.toString(data));
        } else {
            Log.e(TAG, "Write failed to characteristic: " + charc.getUuid() +
                    ", data: " + Arrays.toString(data));
        }
        return result;
    }

    /**
     * Helper function to determine whether a Bean needs a FW update given a specific Bundle version
     *
     * @param bundleVersion the version string from the provided firmware bundle
     * @param beanVersion the version string provided from the Bean Device Information Service
     * @return boolean value stating whether the Bean needs an update
     */
    private boolean needsUpdate(Long bundleVersion, String beanVersion) {
        if (beanVersion.contains("OAD")) {
            Log.i(TAG, "Bundle version: " + bundleVersion);
            Log.i(TAG, "Bean version: " + beanVersion);
            return true;
        } else {
            try {
                long parsedVersion = Long.parseLong(beanVersion.split(" ")[0]);
                Log.i(TAG, "Bundle version: " + bundleVersion);
                Log.i(TAG, "Bean version: " + parsedVersion);
                if (bundleVersion > parsedVersion) {
                    return true;
                } else {
                    Log.i(TAG, "No update required!");
                }
            } catch (NumberFormatException e) {
                Log.e(TAG, "Couldn't parse Bean Version: " + beanVersion);
                fail(BeanError.UNPARSABLE_FW_VERSION);
            }
        }
        return false;
    }

    /**
     * Check the Beans FW version to determine if an update is required
     */
    private void checkFirmwareVersion() {
        Log.i(TAG, "Checking Firmware version...");
        setState(OADState.CHECKING_FW_VERSION);
        mGattClient.getDeviceProfile().getFirmwareVersion(new DeviceProfile.VersionCallback() {
            @Override
            public void onComplete(String version) {

                // Check the Bean version against the Bundle version
                boolean updateNeeded = needsUpdate(firmwareBundle.version(), version);

                if (updateNeeded && oadApproval.isApproved()) {
                    // Needs update and client has approved, keep the update going

                    startOfferingImages();

                } else if (updateNeeded && !oadApproval.isApproved()) {
                    // Needs update but client has not approved, ask for approval

                    watchdog.pause();
                    oadListener.updateRequired(true);

                } else if (!updateNeeded && !oadApproval.isApproved()){
                    // Does not need update and the client has never approved. This means
                    // no update is required, and no firmware update ever occurred

                    finishNoUpdateOccurred();
                    oadListener.updateRequired(false);

                } else if (!updateNeeded && oadApproval.isApproved()) {
                    // Does not need update, and the client has approved. This means that
                    // the firmware process actually took place, and completed

                    finishUpdateOccurred();

                } else {
                    Log.w(TAG, "Unexpected OAD Condition!");
                }

            }
        });
    }

    /**
     * Stop the firmware upload and alert the OADListener
     *
     * @param error The error to be returned to the user
     */
    private void fail(BeanError error) {
        Log.e(TAG, "OAD Error: " + error.toString());
        if (uploadInProgress()) {
            oadListener.error(error);
            reset();
        }
    }

    /**
     * Finish the OAD process, similar to fail() except assumes a better outcome
     */
    private void finishUpdateOccurred() {
        Log.i(TAG, "OAD Finished: Update Occurred");
        oadListener.complete();
        reset();
    }

    private void finishNoUpdateOccurred() {
        Log.i(TAG, "OAD Finished: No update Occurred");

        // Don't reset() here, just set state to inactive. By not resetting, this allows
        // the client to force an OAD update even if there isn't one required.
        watchdog.stop();
        setState(OADState.INACTIVE);
    }

    /****************************************************************************
                                   PUBLIC METHODS
     ****************************************************************************/

    public String getName() {
        return TAG;
    }

    public boolean isReady() {
        return ready;
    }

    public void clearReady() {
        ready = false;
    }

    public boolean uploadInProgress() {
        return oadState != OADState.INACTIVE;
    }

    public OADState getState() {
        return oadState;
    }

    @Override
    public void onProfileReady() {
        setupOAD();
        setupNotifications();
        ready = true;
    }

    public void continueOAD() {
        if (uploadInProgress()) {
            checkFirmwareVersion();
            BeanManager.getInstance().cancelDiscovery();
        }
    }

    @Override
    public void onBeanConnected() {
        Log.i(TAG, "OAD Profile Detected Bean Connection");
    }

    @Override
    public void onBeanDisconnected() {
        Log.i(TAG, "OAD Profile Detected Bean Disconnection");
        reconnect();
    }

    @Override
    public void onBeanConnectionFailed() {
        Log.i(TAG, "OAD Profile Detected Connection Failure, Likely a device reboot");
        reconnect();
    }

    @Override
    public void onCharacteristicChanged(GattClient client, BluetoothGattCharacteristic characteristic) {
        if (uploadInProgress()) {
            if (characteristic.getUuid().equals(Constants.UUID_OAD_CHAR_IDENTIFY)) {
                onNotificationIdentify(characteristic);
            } else if (characteristic.getUuid().equals(Constants.UUID_OAD_CHAR_BLOCK)) {
                onNotificationBlock(characteristic);
            }
        }
    }

    /**
     * Program the Bean's CC2540 with new firmware.
     *
     * @param bundle        The {@link com.punchthrough.bean.sdk.upload.FirmwareBundle} to be sent
     * @param listener      Listener object to alert client of events/state of the Firmware update process
     */
    public OADApproval programWithFirmware(final FirmwareBundle bundle, OADListener listener) {

        if (!mGattClient.isConnected()) {
            listener.error(BeanError.NOT_CONNECTED);
        }

        Log.i(TAG, "Starting firmware update procedure");

        // Save state for this firmware procedure
        this.oadListener = listener;
        this.firmwareBundle = bundle;

        watchdog.start(OAD_TIMEOUT_SECONDS, watchdogListener);
        checkFirmwareVersion();

        return this.oadApproval;
    }

    /**
     * Communication interface from SDK to client
     */
    public interface OADListener {

        /**
         * Called when the OAD procedure completes without error
         */
        public void complete();

        /**
         * Called when there is an error during the OAD procedure
         *
         * @param error BeanError
         */
        public void error(BeanError error);

        /**
         * Called when forward progress has been made during OAD procedure
         *
         * @param uploadProgress UploadProgress object describing the OAD procedure progress
         */
        public void progress(UploadProgress uploadProgress);

        /**
         * Called to let the client know if a FW update is required
         *
         * @param required Boolean flag
         */
        public void updateRequired(boolean required);

        /**
         * Called when the OAD state machine changes states
         *
         * @param state OADState representing the state of the OAD procedure
         */
        public void stateChange(OADState state);
    }

    /**
     * Communication interface from client to SDK
     */
    public interface OADApproval {

        /**
         * Client should call this method to allow the OAD procedure to continue
         */
        public void allow();

        /**
         * Client should call this method to deny the OAD procedure from continuing
         */
        public void deny();

        /**
         * Reset this objects state
         */
        public void reset();

        /**
         *
         * @return Boolean flag representing the last approval status
         */
        public boolean isApproved();
    }
} public enum SketchUploadState {
    INACTIVE, RESETTING_REMOTE, SENDING_START_COMMAND, SENDING_BLOCKS, FINISHED
} public enum BeanError {

    GATT_SERIAL_TRANSPORT_ERROR,

    /**
     * Bean is not connected
     */
    NOT_CONNECTED,

    /**
     * Bean's services have not yet been discovered
     */
    SERVICES_NOT_DISCOVERED,

    /**
     * OAD service was not found
     */
    MISSING_OAD_SERVICE,

    /**
     * OAD Identify characteristic was not found
     */
    MISSING_OAD_IDENTIFY,

    /**
     * OAD Block characteristic was not found
     */
    MISSING_OAD_BLOCK,

    /**
     * Notifications could not be enabled for either OAD Identify, Block, or both characteristics
     */
    ENABLE_OAD_NOTIFY_FAILED,

    /**
     * Timed out during sketch programming, before sending blocks: Bean took too long to update its
     * current state
     */
    STATE_TIMEOUT,

    /**
     * Firmware version to send could not be parsed from OAD request header
     */
    UNPARSABLE_FW_VERSION,

    /**
     * Timed out during OAD
     */
    OAD_TIMEOUT,

    /**
     * Bean rejected firmware version for being older than the current version
     */
    BEAN_REJECTED_FW,

    /**
     * The client (user) rejected the OAD process
     */
    CLIENT_REJECTED,

    /**
     * Bean responded with a message with an ID we don't know anything about
     */
    UNKNOWN_MESSAGE_ID,

    /**
     * Bean did not provide a reason for the error
     */
    UNKNOWN

} public interface Callback<T> {
    /**
     * Override this method to get results back from an asynchronous process.
     *
     * @param result The result passed back from the asynchronous process
     */
    public void onResult(T result);
} @AutoParcel
public abstract class DeviceInfo implements Parcelable {
    public static final String UNKNOWN_VERSION = "";

    public static DeviceInfo create() {
        return new AutoParcel_DeviceInfo(UNKNOWN_VERSION, UNKNOWN_VERSION, UNKNOWN_VERSION);
    }

    public static DeviceInfo create(String hardwareVersion, String softwareVersion, String firmwareVersion) {
        return new AutoParcel_DeviceInfo(hardwareVersion, softwareVersion, firmwareVersion);
    }

    public abstract String hardwareVersion();

    public abstract String softwareVersion();

    public abstract String firmwareVersion();
} @AutoParcel
public abstract class LedColor implements Parcelable {

    public abstract int red();
    public abstract int green();
    public abstract int blue();

    public static LedColor create(int red, int green, int blue) {
        return new AutoParcel_LedColor(clampToUInt8(red), clampToUInt8(green), clampToUInt8(blue));
    }

    public static LedColor fromPayload(Buffer buffer) {
        return new AutoParcel_LedColor(buffer.readByte() & 0xff, buffer.readByte() & 0xff, buffer.readByte() & 0xff);
    }

    @Override
    public boolean equals(Object o) {

        if ( ! (o instanceof LedColor) ) {
            return false;
        }
        LedColor c = (LedColor) o;

        return (c.red() == this.red() &&
                c.green() == this.green() &&
                c.blue() == this.blue());
    }
} public interface Message {
    public byte[] toPayload();
} @AutoParcel
public abstract class RadioConfig implements Parcelable, Message {

    public static RadioConfig create(String name,
                                     int advertisementInterval,
                                     int connectionInterval,
                                     TxPower power,
                                     AdvertisementMode mode,
                                     int beaconUuid,
                                     int beaconMajor,
                                     int beaconMinor) {
        if (TextUtils.isEmpty(name)) {
            throw new IllegalArgumentException("Name must not be empty");
        }
        if (name.length() > 20) {
            throw new IllegalArgumentException("Name must be max 20 characters");
        }
        return new AutoParcel_RadioConfig(name,
                advertisementInterval,
                connectionInterval,
                power,
                mode,
                beaconUuid,
                beaconMajor,
                beaconMinor);
    }

    private static AdvertisementMode adverismentModeFromPayload(int b) {
        switch (b) {
            case 0x00:
                return AdvertisementMode.STANDARD;
            case 0x01:
                return AdvertisementMode.IBEACON;
            case 0x80:
                return AdvertisementMode.STANDARD_AUTHENTICATED;
            case 0x81:
                return AdvertisementMode.IBEACON_AUTHENTICATED;
            default:
                throw new IllegalArgumentException("Invalid value for advertisment mode: " + Integer.toHexString(b));
        }
    }


    public static RadioConfig fromPayload(Buffer buffer) {
        int advertisementInterval = buffer.readShortLe() & 0xffff;
        int connectionInterval = buffer.readShortLe() & 0xffff;
        int power = buffer.readByte() & 0xff;
        int mode = buffer.readByte() & 0xff;
        int beaconUuid = buffer.readShortLe() & 0xffff;
        int beaconMajor = buffer.readShortLe() & 0xffff;
        int beaconMinor = buffer.readShortLe() & 0xffff;
        String localName = buffer.readString(buffer.size() - 1, Charset.forName("UTF-8"));
        int size = buffer.readByte() & 0xff;
        localName = localName.substring(0, size);
        return new AutoParcel_RadioConfig(localName,
                advertisementInterval,
                connectionInterval,
                TxPower.values()[power],
                adverismentModeFromPayload(mode),
                beaconUuid,
                beaconMajor,
                beaconMinor);
    }

    public abstract String name();

    public abstract int advertisementInterval();

    public abstract int connectionInterval();

    public abstract TxPower power();

    public abstract AdvertisementMode advertisementMode();

    public abstract int beaconUuid();

    public abstract int beaconMajor();

    public abstract int beaconMinor();

    @Override
    public byte[] toPayload() {
        Buffer buffer = new Buffer();
        buffer.writeShortLe(advertisementInterval() & 0xffff);
        buffer.writeShortLe(connectionInterval() & 0xffff);
        buffer.writeByte(power().ordinal() & 0xff);
        switch (advertisementMode()) {
            case STANDARD:
                buffer.writeByte(0x00);
                break;
            case STANDARD_AUTHENTICATED:
                buffer.writeByte(0x80);
                break;
            case IBEACON:
                buffer.writeByte(0x01);
                break;
            case IBEACON_AUTHENTICATED:
                buffer.writeByte(0x81);
                break;
        }
        buffer.writeShortLe(beaconUuid() & 0xffff);
        buffer.writeShortLe(beaconMajor() & 0xffff);
        buffer.writeShortLe(beaconMinor() & 0xffff);
        StringBuilder sb = new StringBuilder(name());
        sb.setLength(20);
        buffer.writeString(sb.toString(), Charset.forName("UTF-8"));
        buffer.writeByte(name().length() & 0xff);
        return buffer.readByteArray();
    }

    public enum TxPower {TX_NEG23DB, TX_NEG6DB, TX_0DB, TX_4DB}

    public enum AdvertisementMode {
        STANDARD, IBEACON, STANDARD_AUTHENTICATED, IBEACON_AUTHENTICATED
    }
} public enum ScratchBank implements EnumParse.ParsableEnum {
    BANK_1(1), BANK_2(2), BANK_3(3), BANK_4(4), BANK_5(5);

    private final int value;

    private ScratchBank(final int value) {
        this.value = value;
    }

    public int getRawValue() {
        return value;
    }
} @AutoParcel
public abstract class ScratchData implements Parcelable, Message {
    public static ScratchData fromPayload(Buffer buffer) {
        return new AutoParcel_ScratchData((buffer.readByte() & 0xff), buffer.readByteArray());
    }

    public static ScratchData create(ScratchBank bank, byte[] data) {
        int bankNumber = bank.getRawValue();
        return new AutoParcel_ScratchData(bankNumber, data);
    }

    public static ScratchData create(ScratchBank bank, String data) {
        try {
            return create(bank, data == null ? new byte[0] : data.getBytes("UTF-8"));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    public abstract int number();

    public abstract byte[] data();

    public String getDataAsString() {
        try {
            return new String(data(), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public byte[] toPayload() {
        Buffer buffer = new Buffer();
        buffer.writeByte((number()) & 0xff);
        buffer.write(data());
        return buffer.readByteArray();
    }
} @AutoParcel
public abstract class SketchMetadata implements Parcelable {

    public abstract int hexSize();

    public abstract int hexCrc();

    public abstract Date timestamp();

    public abstract String hexName();

    public static SketchMetadata create(int hexSize, int hexCrc, Date timestamp, String hexName) {
        return new AutoParcel_SketchMetadata(hexSize, hexCrc, timestamp, hexName);
    }

    /**
     * Create a SketchMetadata object with the given
     * {@link com.punchthrough.bean.sdk.upload.SketchHex} data and programmed-at timestamp.
     * @param hex       The {@link com.punchthrough.bean.sdk.upload.SketchHex} to be sent
     * @param timestamp The time the bean will indicate it was programmed
     * @return          The SketchMetadata object
     */
    public static SketchMetadata create(SketchHex hex, Date timestamp) {
        int hexSize = hex.bytes().length;
        String hexName = hex.sketchName();

        CRC32 crc = new CRC32();
        crc.update(hex.bytes());
        int hexCrc = (int) crc.getValue();

        return SketchMetadata.create(hexSize, hexCrc, timestamp, hexName);
    }

    /**
     * Get this object's metadata as a payload of bytes, ready to send to Bean in a
     * {@link com.punchthrough.bean.sdk.internal.BeanMessageID#BL_CMD_START} message.
     * @return The SketchMetadata in payload form
     */
    public Buffer toPayload() {
        /* From AppMessages.h: BL_SKETCH_META_DATA_T struct
         *
         * {
         *   PTD_UINT32 hexSize;
         *   PTD_UINT32 hexCrc;
         *   PTD_UINT32 timestamp;
         *   PTD_UINT8 hexNameSize;
         *   PTD_UINT8 hexName[MAX_SKETCH_NAME_SIZE];
         * }
         */
        Buffer buffer = new Buffer();

        // Pad name to 20 bytes to fill buffer completely. It will be truncated by the Bean.
        // Name has already been truncated to max length by the SketchHex constructor.
        String fullName = hexName();
        while (fullName.length() < MAX_SKETCH_NAME_LENGTH) {
            fullName += " ";
        }

        // !! The Bean uses little endian. Don't screw this up like I did!
        ByteOrder endian = ByteOrder.LITTLE_ENDIAN;

        byte[] hexSize = intToUInt32(hexSize(), endian);
        byte[] hexCrc = intToUInt32(hexCrc(), endian);
        byte[] timestamp = intToUInt32( (int) (new Date().getTime() / 1000), endian);
        byte nameLength = intToByte(hexName().length());
        ByteString name = ByteString.encodeUtf8(fullName);

        buffer.write(hexSize);
        buffer.write(hexCrc);
        buffer.write(timestamp);
        buffer.writeByte(nameLength);
        buffer.write(name);

        return buffer;
    }

    public static SketchMetadata fromPayload(Buffer buffer) {
        int hexSize = buffer.readIntLe();
        int hexCrc = buffer.readIntLe();
        long timestamp = (buffer.readIntLe() & 0xffffffffL) * 1000L;
        int hexNameSize = buffer.readByte() & 0xff;
        String hexName = "";
        if (hexNameSize > 0 && hexNameSize <= 20) {
            hexName = buffer.readString(hexNameSize, Charset.forName("UTF-8"));
        }
        return new AutoParcel_SketchMetadata(hexSize, hexCrc, new Date(timestamp), hexName);
    }

} @AutoParcel
public abstract class Status implements Parcelable {
    /* AppMessages.h -> BL_MSG_STATUS_T:
     * {
     *     PTD_UINT8 hlState;   // BL_HL_STATE_T
     *     PTD_UINT8 intState;  // BL_STATE_T
     *     PTD_UINT16 blocksSent;
     *     PTD_UINT16 bytesSent;
     * }
     */

    public abstract BeanState beanState();

    public abstract BeanSubstate beanSubstate();

    public abstract int blocksSent();

    public abstract int bytesSent();

    public static Status create(BeanState beanState, BeanSubstate beanSubstate, int blocksSent, int bytesSent) {
        return new AutoParcel_Status(beanState, beanSubstate, blocksSent, bytesSent);
    }

    public static Status fromPayload(Buffer buffer) throws NoEnumFoundException {
        BeanState beanState = EnumParse.enumWithRawValue(BeanState.class, buffer.readByte());
        BeanSubstate beanSubstate = EnumParse.enumWithRawValue(BeanSubstate.class, buffer.readByte());
        int blocksSent = bytesToInt(buffer.readByte(), buffer.readByte());
        int bytesSent = bytesToInt(buffer.readByte(), buffer.readByte());
        return Status.create(beanState, beanSubstate, blocksSent, bytesSent);
    }

} @AutoParcel
public abstract class UploadProgress implements Parcelable {

    /**
     * The number of blocks sent to the Bean so far.
     * @return The number of blocks sent
     */
    public abstract int blocksSent();

    /**
     * The total number of blocks being sent to the Bean.
     * @return The total number of blocks being sent
     */
    public abstract int totalBlocks();

    public static UploadProgress create(int blocksSent, int totalBlocks) {
        return new AutoParcel_UploadProgress(blocksSent, totalBlocks);
    }

    /**
     * The completion percentage of the upload in progress
     *
     * @return Completion percentage, from 0.0 to 1.0
     */
    public float completionPercent() {
        return ((float) blocksSent() ) / totalBlocks();
    }

    public String completionBlocks() {
        return String.format("%s/%s", blocksSent(), totalBlocks());
    }

} public class FirmwareBundle {

    private List<FirmwareImage> images;
    private int imageCounter;


    public FirmwareBundle(List<FirmwareImage> images) {
        this.images = images;
        this.imageCounter = 0;
    }

    public long version() {
        return images.get(0).version();
    }

    public FirmwareImage getNextImage() throws OADException {
        if (imageCounter >= images.size() ) {
            throw new OADException("Firmware bundle is exhausted, all images rejected");
        }

        FirmwareImage i = images.get(imageCounter);
        imageCounter++;
        return i;
    }

    public void reset() {
        imageCounter = 0;
    }

} @AutoParcel
public abstract class SketchHex implements Parcelable, Chunk.Chunkable {

    public abstract String sketchName();

    public abstract byte[] bytes();

    @Override
    public byte[] getChunkableData() {
        return bytes();
    }

    /**
     * Initialize a SketchHex object with no data.
     */
    public static SketchHex create(String sketchName) throws HexParsingException {
        return SketchHex.create(sketchName, "");
    }

    /**
     * Initialize a SketchHex object with a string of Intel Hex data.
     *
     * @param sketchName The name of the sketch.
     * @param hexString The Intel Hex data as a string
     * @return The new SketchHex object
     * @throws com.punchthrough.bean.sdk.internal.exception.HexParsingException
     *         if the string data being parsed is not valid Intel Hex
     */
    public static SketchHex create(String sketchName, String hexString) throws HexParsingException {

        if (sketchName.length() > Constants.MAX_SKETCH_NAME_LENGTH) {
            sketchName = sketchName.substring(0, Constants.MAX_SKETCH_NAME_LENGTH);
        }

        List<Line> lines = parseHexStringToLines(hexString);
        byte[] bytes = convertLinesToBytes(lines);
        return new AutoParcel_SketchHex(sketchName, bytes);
    }

    /**
     * Parse a string of Intel Hex data to a list of
     * {@link com.punchthrough.bean.sdk.internal.intelhex.Line} objects.
     *
     * @param hexString The Intel Hex data as a string
     * @return          A list of {@link com.punchthrough.bean.sdk.internal.intelhex.Line} objects
     *                  representing the Intel Hex data
     * @throws HexParsingException  If the Intel Hex could not be parsed
     */
    private static List<Line> parseHexStringToLines(String hexString) throws HexParsingException {
        List<String> rawLines = Arrays.asList(hexString.split("\n"));
        ListIterator<String> iterator = rawLines.listIterator();

        List<Line> lines = new ArrayList<>();
        while (iterator.hasNext()) {

            int rawLineNum = iterator.nextIndex();
            String rawLine = iterator.next();

            if (rawLine.length() < 11) continue;

            if ( ! rawLine.startsWith(":") ) {
                throw new HexParsingException(String.format(
                        "Couldn't parse hex: line %d did not start with ':'", rawLineNum));
            }

            rawLine = rawLine.replaceAll("\r", "");

            // rawBytes: bytes of the string without the leading colon, parsed into hex
            byte[] rawBytes;
            try {
                rawBytes = asciiHexToBytes(rawLine.substring(1));
            } catch (DecoderException e) {
                throw new HexParsingException(String.format(
                        "Couldn't parse hex: line %d ASCII could not be parsed to byte array: %s",
                        rawLineNum, e.getLocalizedMessage()));
            }

            Line line = new Line();
            line.setByteCount(rawBytes[0]);
            line.setAddress(bytesToInt(rawBytes[1], rawBytes[2]));

            byte rawRecordType = rawBytes[3];
            LineRecordType recordType;
            try {
                recordType = EnumParse.enumWithRawValue(LineRecordType.class, rawRecordType);
            } catch (NoEnumFoundException e) {
                throw new HexParsingException(String.format(
                        "Couldn't parse hex: line %d had invalid record type %d",
                        rawLineNum, rawRecordType));
            }

            line.setRecordType(recordType);

            if (line.getByteCount() > 0) {
                line.setData(Arrays.copyOfRange(rawBytes, 4, 4 + line.getByteCount()));
            }

            line.setChecksum(rawBytes[4 + line.getByteCount()]);

            lines.add(line);
        }

        return lines;
    }

    /**
     * Convert a list of Intel Hex {@link com.punchthrough.bean.sdk.internal.intelhex.Line} objects
     * to an array of the raw bytes they represent.
     *
     * @param lines The List of {@link com.punchthrough.bean.sdk.internal.intelhex.Line} objects
     * @return      An array of raw bytes represented by the
     *              {@link com.punchthrough.bean.sdk.internal.intelhex.Line} objects
     */
    private static byte[] convertLinesToBytes(List<Line> lines) {
        List<Line> dataLines = new ArrayList<>();
        int byteCount = 0;
        for (Line line : lines) {
            if (line.getRecordType() == LineRecordType.DATA) {
                dataLines.add(line);
                byteCount += line.getByteCount();
            }
        }

        byte[] bytes = new byte[byteCount];
        int i = 0;
        for (Line line : dataLines) {
            for (byte b : line.getData()) {
                bytes[i] = b;
                i++;
            }
        }

        return bytes;
    }

} public static byte intToByte(int i) {
    return (byte) (i & 0xFF);
}
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import com.punchthrough.bean.sdk.internal.BeanMessageID;
import com.punchthrough.bean.sdk.internal.battery.BatteryProfile.BatteryLevelCallback;
import com.punchthrough.bean.sdk.internal.ble.GattClient;
import com.punchthrough.bean.sdk.internal.device.DeviceProfile;
import com.punchthrough.bean.sdk.internal.device.DeviceProfile.DeviceInfoCallback;
import com.punchthrough.bean.sdk.internal.exception.NoEnumFoundException;
import com.punchthrough.bean.sdk.internal.serial.GattSerialMessage;
import com.punchthrough.bean.sdk.internal.serial.GattSerialTransportProfile;
import com.punchthrough.bean.sdk.internal.upload.firmware.OADProfile;
import com.punchthrough.bean.sdk.internal.upload.sketch.BeanState;
import com.punchthrough.bean.sdk.internal.upload.sketch.SketchUploadState;
import com.punchthrough.bean.sdk.internal.utility.Chunk;
import com.punchthrough.bean.sdk.message.Acceleration;
import com.punchthrough.bean.sdk.message.AccelerometerRange;
import com.punchthrough.bean.sdk.message.BatteryLevel;
import com.punchthrough.bean.sdk.message.BeanError;
import com.punchthrough.bean.sdk.message.Callback;
import com.punchthrough.bean.sdk.message.DeviceInfo;
import com.punchthrough.bean.sdk.message.LedColor;
import com.punchthrough.bean.sdk.message.Message;
import com.punchthrough.bean.sdk.message.RadioConfig;
import com.punchthrough.bean.sdk.message.ScratchBank;
import com.punchthrough.bean.sdk.message.ScratchData;
import com.punchthrough.bean.sdk.message.SketchMetadata;
import com.punchthrough.bean.sdk.message.Status;
import com.punchthrough.bean.sdk.message.UploadProgress;
import com.punchthrough.bean.sdk.upload.FirmwareBundle;
import com.punchthrough.bean.sdk.upload.SketchHex;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import okio.Buffer;
import static com.punchthrough.bean.sdk.internal.Protocol.APP_MSG_RESPONSE_BIT;
import static com.punchthrough.bean.sdk.internal.utility.Convert.intToByte;
            public void onConnected() {
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        beanListener.onConnected();
                    }
                });
            }

            @Override
            public void onConnectionFailed() {
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        beanListener.onConnectionFailed();
                    }
                });
            }

            @Override
            public void onDisconnected() {
                beanCallbacks.clear();
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        beanListener.onDisconnected();
                    }
                });
            }
        };

        GattSerialTransportProfile.SerialListener serialListener = new GattSerialTransportProfile.SerialListener() {

            @Override
            public void onMessageReceived(final byte[] data) {
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        handleMessage(data);
                    }
                });
            }

            @Override
            public void onScratchValueChanged(final ScratchBank bank, final byte[] value) {
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        beanListener.onScratchValueChanged(bank, value);
                    }
                });
            }

            @Override
            public void onError(String message) {
                Log.e(TAG, message);

                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        beanListener.onError(BeanError.GATT_SERIAL_TRANSPORT_ERROR);
                    }
                });
            }

            @Override
            public void onReadRemoteRssi(final int rssi) {
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        beanListener.onReadRemoteRssi(rssi);
                    }
                });
            }
        };

        gattClient.setListener(connectionListener);
        gattClient.getSerialProfile().setListener(serialListener);
    }

    /**
     * Handles incoming messages from the Bean and dispatches them to the proper handlers.
     * @param data The raw byte data received from the Bean
     */
    private void handleMessage(byte[] data) {
        Buffer buffer = new Buffer();
        buffer.write(data);
        int type = (buffer.readShort() & 0xffff) & ~(APP_MSG_RESPONSE_BIT);

        if (type == BeanMessageID.SERIAL_DATA.getRawValue()) {
            beanListener.onSerialMessageReceived(buffer.readByteArray());

        } else if (type == BeanMessageID.BT_GET_CONFIG.getRawValue()) {
            returnConfig(buffer);

        } else if (type == BeanMessageID.CC_TEMP_READ.getRawValue()) {
            returnTemperature(buffer);

        } else if (type == BeanMessageID.BL_GET_META.getRawValue()) {
            returnMetadata(buffer);

        } else if (type == BeanMessageID.BT_GET_SCRATCH.getRawValue()) {
            returnScratchData(buffer);

        } else if (type == BeanMessageID.CC_LED_READ_ALL.getRawValue()) {
            returnLed(buffer);

        } else if (type == BeanMessageID.CC_ACCEL_READ.getRawValue()) {
            returnAcceleration(buffer);

        } else if (type == BeanMessageID.CC_ACCEL_GET_RANGE.getRawValue()) {
            returnAccelerometerRange(buffer);

        // Ignore CC_LED_WRITE; it appears to be only an ack

        } else if (type == BeanMessageID.CC_GET_AR_POWER.getRawValue()) {
            returnArduinoPowerState(buffer);

        } else if (type == BeanMessageID.BL_STATUS.getRawValue()) {
            try {
                Status status = Status.fromPayload(buffer);