public class ContentProviderStore extends DataStore {
    public static final String TAG = "DataManager";

    public ContentProviderStore(Context context) {
        super(context);
    }

    @Override
    public void markMessageDeliveredToPeer(@NonNull MessagePacket messagePacket, @NonNull IdentityPacket recipientPacket) {
        Message message = getMessageBySignature(messagePacket.signature);
        Peer recipient = getPeerByPubKey(recipientPacket.publicKey);

        if (message == null || recipient == null) {
            Log.w(TAG, "Unable to record message delivery. No peer or message database id available");
            return;
        }

        ContentValues delivery = new ContentValues();
        delivery.put(MessageDeliveryTable.messageId, message.getId());
        delivery.put(MessageDeliveryTable.peerId, recipient.getId());

        mContext.getContentResolver().insert(ChatContentProvider.MessageDeliveries.MESSAGE_DELIVERIES, delivery);
        Log.i(TAG, "Recorded message delivery");
        message.close();
    }

    @Override
    public void markIdentityDeliveredToPeer(@NonNull IdentityPacket payloadIdentity, @NonNull IdentityPacket recipientIdentity) {
        Peer payloadPeer = getPeerByPubKey(payloadIdentity.publicKey);
        Peer recipientPeer = getPeerByPubKey(recipientIdentity.publicKey);

        if (payloadPeer == null || recipientPeer == null) {
            Log.w(TAG, "Unable to fetch payload or recipient identity. Cannot mark identity delivered");
            return;
        }

        ContentValues delivery = new ContentValues();
        delivery.put(IdentityDeliveryTable.peerPayloadId, payloadPeer.getId());
        delivery.put(IdentityDeliveryTable.peerRecipientId, recipientPeer.getId());

        mContext.getContentResolver().insert(ChatContentProvider.IdentityDeliveries.IDENTITY_DELIVERIES, delivery);
        Log.i(TAG, "Recorded identity delivery");
    }

    @Nullable
    @Override
    public Peer createLocalPeerWithAlias(@NonNull String alias, @Nullable Protocol protocol) {
        KeyPair keyPair = SodiumShaker.generateKeyPair();
        ContentValues dbEntry = new ContentValues();
        dbEntry.put(PeerTable.pubKey, keyPair.publicKey);
        dbEntry.put(PeerTable.secKey, keyPair.secretKey);
        dbEntry.put(PeerTable.alias, alias);
        dbEntry.put(PeerTable.lastSeenDate, DataUtil.storedDateFormatter.format(new Date()));
        if (protocol != null) {
            // If protocol is available, use it to cache the Identity packet for transmission
            dbEntry.put(PeerTable.rawPkt, protocol.serializeIdentity(
                    new OwnedIdentityPacket(keyPair.secretKey, keyPair.publicKey, alias, null)));
        }
        Uri newIdentityUri = mContext.getContentResolver().insert(ChatContentProvider.Peers.PEERS, dbEntry);
        return getPeerById(Integer.parseInt(newIdentityUri.getLastPathSegment()));
    }

    /**
     * @return the first user peer entry in the database,
     * or null if no identity is set.
     */
    @Override
    @Nullable
    public Peer getPrimaryLocalPeer() {
        // TODO: caching
        Cursor result = mContext.getContentResolver().query(ChatContentProvider.Peers.PEERS,
                null,
                PeerTable.secKey + " IS NOT NULL",
                null,
                null);
        if (result != null && result.moveToFirst()) {
            Peer peer = new Peer(result);
            result.close();
            return peer;
        }
        return null;
    }

    @Nullable
    @Override
    public List<MessagePacket> getOutgoingMessagesForPeer(@NonNull Peer recipient, int maxMessages) {
        // TODO : Don't send messages past a certain age etc?
        Cursor messagesCursor = mContext.getContentResolver().query(ChatContentProvider.Messages.MESSAGES, null, null, null, null);
        if (messagesCursor != null) {
            List<MessagePacket> messagesToSend = new ArrayList<>();
            while (messagesCursor.moveToNext()) {
                Message individualMessage = new Message(messagesCursor);
                if (!haveDeliveredMessageToPeer(individualMessage, recipient)) {
                    messagesToSend.add(individualMessage.getProtocolMessage(this));
                    if (messagesToSend.size() > maxMessages) break;
                }
            }

            messagesCursor.close();
            return messagesToSend;
        }
        return null;
    }

    @Override
    public List<IdentityPacket> getOutgoingIdentitiesForPeer(@NonNull Peer recipient, int maxIdentities) {
        // TODO : Don't send identities past a certain age etc?
        Cursor identitiesCursor = mContext.getContentResolver().query(ChatContentProvider.Peers.PEERS, null, null, null, null);
        if (identitiesCursor != null) {
            List<IdentityPacket> identitiesToSend = new ArrayList<>();
            while (identitiesCursor.moveToNext()) {
                Peer payloadPeer = new Peer(identitiesCursor);
                if (!haveDeliveredPeerIdentityToPeer(payloadPeer, recipient)) {
                    identitiesToSend.add(payloadPeer.getIdentity());
                    if (identitiesToSend.size() > maxIdentities) break;
                }

            }

            identitiesCursor.close();
            return identitiesToSend;
        }
        return null;
    }

    @Override
    public MessageCollection getRecentMessages() {
        Cursor messagesCursor = mContext.getContentResolver().query(ChatContentProvider.Messages.MESSAGES,
                null,
                null,
                null,
                MessageTable.receivedDate + " DESC");

        if (messagesCursor != null /*&& messagesCursor.moveToFirst()*/) {
            return new MessageCollection(messagesCursor);
        }
        return null;
    }

    @Override
    public MessageCollection getRecentMessagesByPeer(@NonNull Peer author) {
        Cursor messagesCursor = mContext.getContentResolver().query(ChatContentProvider.Messages.MESSAGES,
                null,
                MessageTable.peerId + "=?",
                new String[] { String.valueOf(author.getId()) },
                MessageTable.receivedDate + " DESC");

        if (messagesCursor != null /*&& messagesCursor.moveToFirst()*/) {
            return new MessageCollection(messagesCursor);
        }
        return null;
    }

    @Nullable
    @Override
    public Peer createOrUpdateRemotePeerWithProtocolIdentity(@NonNull IdentityPacket remoteIdentityPacket) {
        // Query if peer exists
        Peer peer = getPeerByPubKey(remoteIdentityPacket.publicKey);

        ContentValues peerValues = new ContentValues();
        peerValues.put(PeerTable.lastSeenDate, DataUtil.storedDateFormatter.format(new Date()));
        peerValues.put(PeerTable.pubKey, remoteIdentityPacket.publicKey);
        peerValues.put(PeerTable.alias, remoteIdentityPacket.alias);
        peerValues.put(PeerTable.rawPkt, remoteIdentityPacket.rawPacket);

        if (peer != null) {
            // Peer exists. Modify lastSeenDate
            Log.i(TAG, "Updating peer for pubkey " + DataUtil.bytesToHex(remoteIdentityPacket.publicKey));

            int updated = mContext.getContentResolver().update(
                    ChatContentProvider.Peers.PEERS,
                    peerValues,
                    "quote("+ PeerTable.pubKey + ") = ?" ,
                    new String[] {DataUtil.bytesToHex(remoteIdentityPacket.publicKey)});
            if (updated != 1) {
                Log.e(TAG, "Failed to update peer last seen");
            }
        } else {
            // Peer does not exist. Create.
            Uri peerUri = mContext.getContentResolver().insert(
                    ChatContentProvider.Peers.PEERS,
                    peerValues);

            // Fetch newly created peer
            peer = getPeerById(Integer.parseInt(peerUri.getLastPathSegment()));
            Log.i(TAG, String.format("Created new peer %d for pubkey %s", Integer.parseInt(peerUri.getLastPathSegment()), DataUtil.bytesToHex(remoteIdentityPacket.publicKey)));

            if (peer == null) {
                Log.e(TAG, "Failed to query peer after insertion.");
            }
        }
        return peer;
    }

    @Nullable
    @Override
    public Message createOrUpdateMessageWithProtocolMessage(@NonNull MessagePacket protocolMessagePacket) {
        // Query if peer exists
        Peer peer = getPeerByPubKey(protocolMessagePacket.sender.publicKey);

        if (peer == null)
            throw new IllegalStateException("Failed to get peer for message");

        // See if message exists
        Message message = getMessageBySignature(protocolMessagePacket.signature);
        if (message == null) {
            // Message doesn't exist in our database

            // Insert message into database
            ContentValues newMessageEntry = new ContentValues();
            newMessageEntry.put(MessageTable.body, protocolMessagePacket.body);
            newMessageEntry.put(MessageTable.peerId, peer.getId());
            newMessageEntry.put(MessageTable.receivedDate, DataUtil.storedDateFormatter.format(new Date()));
            newMessageEntry.put(MessageTable.authoredDate, DataUtil.storedDateFormatter.format(protocolMessagePacket.authoredDate));
            newMessageEntry.put(MessageTable.signature, protocolMessagePacket.signature);
            newMessageEntry.put(MessageTable.replySig, protocolMessagePacket.replySig);
            newMessageEntry.put(MessageTable.rawPacket, protocolMessagePacket.rawPacket);

            Uri newMessageUri = mContext.getContentResolver().insert(
                    ChatContentProvider.Messages.MESSAGES,
                    newMessageEntry);
            message = getMessageById(Integer.parseInt(newMessageUri.getLastPathSegment()));
        } else {
            // We already have a message with this signature
            // Since we currently don't have any mutable message fields (e.g hopcount)
            // do nothing
            Log.i(TAG, "Received stored message. Ignoring");
        }

        return message;
    }

    @Nullable
    @Override
    public Message getMessageBySignature(@NonNull byte[] signature) {
        Cursor messageCursor = mContext.getContentResolver().query(
                ChatContentProvider.Messages.MESSAGES,
                null,
                "quote(" + MessageTable.signature + ") = ?",
                new String[] {DataUtil.bytesToHex(signature)},
                null);
        if (messageCursor != null && messageCursor.moveToFirst()) {
            return new Message(messageCursor);
        }
        return null;
    }

    @Nullable
    @Override
    public Message getMessageById(int id) {
        Cursor messageCursor = mContext.getContentResolver().query(ChatContentProvider.Messages.MESSAGES, null,
                MessageTable.id + " = ?",
                new String[]{String.valueOf(id)},
                null);
        if (messageCursor != null && messageCursor.moveToFirst()) {
            return new Message(messageCursor);
        }
        return null;
    }

    @Nullable
    @Override
    public Peer getPeerByPubKey(@NonNull byte[] publicKey) {
        Cursor peerCursor = mContext.getContentResolver().query(
                ChatContentProvider.Peers.PEERS,
                null,
                "quote(" + PeerTable.pubKey + ") = ?",
                new String[] {DataUtil.bytesToHex(publicKey)},
                null);
        if (peerCursor != null && peerCursor.moveToFirst()) {
            Peer peer = new Peer(peerCursor);
            peerCursor.close();
            return peer;
        }
        return null;
    }

    @Nullable
    @Override
    public Peer getPeerById(int id) {
        Cursor peerCursor = mContext.getContentResolver().query(
                ChatContentProvider.Peers.PEERS,
                null,
                PeerTable.id + " = ?",
                new String[] {String.valueOf(id)},
                null);
        if (peerCursor != null && peerCursor.moveToFirst()) {
            Peer peer = new Peer(peerCursor);
            peerCursor.close();
            return peer;
        }
        return null;
    }

    @Override
    public int countPeers() {
        Cursor peerCursor = mContext.getContentResolver().query(
                ChatContentProvider.Peers.PEERS,
                new String[] {PeerTable.id},
                null,
                null,
                null);
        if (peerCursor != null) {
            int result = peerCursor.getCount();
            peerCursor.close();
            return result;
        }
        return 0;
    }

    @Override
    public int countMessagesPassed() {
        Cursor deliveryCursor = mContext.getContentResolver().query(
                ChatContentProvider.MessageDeliveries.MESSAGE_DELIVERIES,
                new String[] {MessageDeliveryTable.id},
                null,
                null,
                null);
        if (deliveryCursor != null) {
            int result = deliveryCursor.getCount();
            deliveryCursor.close();
            return result;
        }
        return 0;
    }

    /** Utility */

    private boolean haveDeliveredMessageToPeer(Message message, Peer peer) {
        Cursor deliveryCursor = mContext.getContentResolver().query(ChatContentProvider.MessageDeliveries.MESSAGE_DELIVERIES,
                null,
                MessageDeliveryTable.messageId + " = ? AND " + MessageDeliveryTable.peerId + " = ?",
                new String[]{String.valueOf(message.getId()), String.valueOf(peer.getId())},
                null);
        try {
            return deliveryCursor != null && deliveryCursor.moveToFirst();
        } finally {
            if (deliveryCursor != null) deliveryCursor.close();
        }
    }

    /**
     * @return whether peerPayload has been delivered to peerRecipient
     */
    private boolean haveDeliveredPeerIdentityToPeer(Peer peerPayload, Peer peerRecipient) {
        Cursor deliveryCursor = mContext.getContentResolver().query(ChatContentProvider.IdentityDeliveries.IDENTITY_DELIVERIES,
                null,
                IdentityDeliveryTable.peerRecipientId + " = ? AND " + IdentityDeliveryTable.peerPayloadId + " = ?",
                new String[]{String.valueOf(peerRecipient.getId()), String.valueOf(peerPayload.getId())},
                null);
        try {
            return deliveryCursor != null && deliveryCursor.moveToFirst();
        } finally {
            if (deliveryCursor != null) deliveryCursor.close();
        }
    }
} public abstract class DataStore {

    protected Context mContext;

    public DataStore(@NonNull Context context) {
        mContext = context.getApplicationContext();
    }

    public abstract void markMessageDeliveredToPeer(@NonNull MessagePacket message, @NonNull IdentityPacket recipient);

    public abstract void markIdentityDeliveredToPeer(@NonNull IdentityPacket payloadIdentity, @NonNull IdentityPacket recipientIdentity);

    public abstract Peer createLocalPeerWithAlias(@NonNull String alias, @Nullable Protocol protocol);

    public abstract Peer getPrimaryLocalPeer();

    public abstract List<MessagePacket> getOutgoingMessagesForPeer(@NonNull Peer recipient, int maxMessages);

    public abstract List<IdentityPacket> getOutgoingIdentitiesForPeer(@NonNull Peer recipient, int maxMessages);

    public abstract MessageCollection getRecentMessages();

    public abstract MessageCollection getRecentMessagesByPeer(@NonNull Peer author);

    public abstract Peer createOrUpdateRemotePeerWithProtocolIdentity(@NonNull IdentityPacket identityPacket);

    public abstract Message createOrUpdateMessageWithProtocolMessage(@NonNull MessagePacket protocolMessagePacket);

    public abstract Message getMessageBySignature(@NonNull byte[] signature);

    public abstract Message getMessageById(int id);

    public abstract Peer getPeerByPubKey(@NonNull byte[] publicKey);

    public abstract Peer getPeerById(int id);

    public abstract int countPeers();

    public abstract int countMessagesPassed();

} public class DataUtil {

    public static SimpleDateFormat storedDateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);

    final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();

    /**
     * When we query rows by a BLOB column we must
     * convert the BLOB to its String hex form
     * see:
     * http://www.sqlite.org/lang_expr.html#litvalue
     */
    public static String bytesToHex(byte[] bytes) {
        char[] hexChars = new char[bytes.length * 2];
        for ( int j = 0; j < bytes.length; j++ ) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = hexArray[v >>> 4];
            hexChars[j * 2 + 1] = hexArray[v & 0x0F];
        }
        String rawHex = new String(hexChars);
        String blobLiteral = "X'" + rawHex + "'";
        return blobLiteral;
    }

} public class Message extends CursorModel {

    public Message(@NonNull Cursor cursor) {
        super(cursor);

    }

    public int getId() {
        return mCursor.getInt(mCursor.getColumnIndex(MessageTable.id));
    }

    public String getBody() {
        return mCursor.getString(mCursor.getColumnIndex(MessageTable.body));
    }

    public Date getAuthoredDate() {
        try {
            return DataUtil.storedDateFormatter.parse(mCursor.getString(mCursor.getColumnIndex(MessageTable.authoredDate)));
        } catch (ParseException e) {
            e.printStackTrace();
            return null;
        }
    }

    public byte[] getPublicKey(DataStore dataStore) {
        return getSender(dataStore).getIdentity().publicKey;
    }

    public byte[] getSignature() {
        return mCursor.getBlob(mCursor.getColumnIndex(MessageTable.signature));
    }

    public byte[] getReplySignature() {
        return mCursor.getBlob(mCursor.getColumnIndex(MessageTable.replySig));
    }

    public byte[] getRawPacket() {
        return mCursor.getBlob(mCursor.getColumnIndex(MessageTable.rawPacket));
    }

    @Nullable
    public Peer getSender(DataStore dataStore) {
        return dataStore.getPeerById(mCursor.getInt(mCursor.getColumnIndex(MessageTable.peerId)));
    }

    @Nullable
    public MessagePacket getProtocolMessage(DataStore dataStore) {
        return new MessagePacket(
                getSender(dataStore).getIdentity(),
                getSignature(),
                getReplySignature(),
                getBody(),
                getRawPacket(),
                getAuthoredDate());

    }

    @Nullable
    public Date getRelativeReceivedDate() {
        try {
            return DataUtil.storedDateFormatter.parse(
                    mCursor.getString(mCursor.getColumnIndex(MessageTable.authoredDate)));
        } catch (ParseException e) {
            return null;
        }
    }
} public class Peer {

    private int mId;
    private byte[] mPublicKey;
    private byte[] mSecretKey;
    private String mAlias;
    private Date mLastSeen;

    private byte[] mRawPkt;


    public Peer(@NonNull Cursor cursor) {
        mId = cursor.getInt(cursor.getColumnIndex(PeerTable.id));
        mPublicKey = cursor.getBlob(cursor.getColumnIndex(PeerTable.pubKey));
        mSecretKey = cursor.getBlob(cursor.getColumnIndex(PeerTable.secKey));
        mAlias = cursor.getString(cursor.getColumnIndex(PeerTable.alias));
        mRawPkt = cursor.getBlob(cursor.getColumnIndex(PeerTable.rawPkt));

        try {
            mLastSeen = DataUtil.storedDateFormatter.parse(cursor.getString(cursor.getColumnIndex(PeerTable.lastSeenDate)));
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }

    public int getId() {
       return mId;
    }

    public byte[] getPublicKey() {
        return mPublicKey;
    }

    public String getAlias() {
        return mAlias;
    }

    @Nullable
    public Date getLastDateSeen() {
        return mLastSeen;
    }
    /**
     * @return whether this peer represents the application user.
     * e.g: Do we have a secret key
     */
    public boolean isLocalPeer() {
        return mSecretKey != null && mSecretKey.length > 0;
    }

    /**
     * @return a {@link pro.dbro.ble.protocol.OwnedIdentityPacket} for this peer,
     * or an {@link pro.dbro.ble.protocol.IdentityPacket} if this peer is not a user-owned peer.
     * <p/>
     * see {@link #isLocalPeer()}
     */
    public IdentityPacket getIdentity() {
        if (!isLocalPeer()) {
            return new IdentityPacket(mPublicKey, mAlias, mLastSeen, mRawPkt);
        } else {
            return new OwnedIdentityPacket(mSecretKey, mPublicKey, mAlias, mRawPkt);
        }
    }

    @Override
    public boolean equals(Object obj) {

        if(obj == this) return true;
        if(obj == null) return false;

        if (getClass().equals(obj.getClass()))
        {
            final Peer other = (Peer) obj;

            return mId == other.mId;
        }

        return false;
    }
} public class BLEProtocol implements Protocol {
    public static final String TAG = "ChatProtocol";

    // <editor-fold desc="Public API">
    /** Bluetooth LE Mesh Chat Protocol Version */
    public static final byte VERSION = 0x01;

    /** Identity */
    public static final int NODATA_RESPONSE_LENGTH     = 106;  // bytes
    public static final int MESSAGE_RESPONSE_LENGTH    = 310;  // bytes
    public static final int IDENTITY_RESPONSE_LENGTH   = 141;  // bytes
    public static final int MESSAGE_BODY_LENGTH        = 140;  // bytes
    public static final int ALIAS_LENGTH               = 35;   // bytes

    private static final ByteBuffer sTimeStampBuffer = ByteBuffer.allocate(Long.SIZE / 8);

    static {
        sTimeStampBuffer.order(ByteOrder.LITTLE_ENDIAN);
    }

    /** Outgoing
     *
     * Create raw transmission data from protocol Objects
    */

    // TODO : Make this API consistent. Either all byte[] or all Packet structures
    // The below method would be unnecessary if we ensured the rawPacket attribute
    // was created on OwnedIdentityPacket's construction. We should only ever have
    // to serialize our own identity. Every other identity is received serialized.
    @Nullable
    public byte[] serializeIdentity(@NonNull OwnedIdentityPacket ownedIdentity) {
        // Protocol version 1
        //[[version=1][timestamp=8][sender_public_key=32][display_name=35]][signature=64]
        try {
            byte[] identity = new byte[IDENTITY_RESPONSE_LENGTH];
            int writeIndex = 0;
            writeIndex += addVersionToBuffer(identity, writeIndex);
            writeIndex += addTypeToBuffer(identity, IdentityPacket.TYPE, writeIndex);
            writeIndex += addTimestampToBuffer(identity, writeIndex);
            writeIndex += addPublicKeyToBuffer(ownedIdentity.publicKey, identity, writeIndex);
            writeIndex += addAliasToBuffer(ownedIdentity.alias, identity, writeIndex);
            writeIndex += addSignatureToBuffer(ownedIdentity.secretKey, identity, writeIndex);

            if (writeIndex != IDENTITY_RESPONSE_LENGTH)
                throw new IllegalStateException("Generated Identity does not match expected length");

            return identity;
        } catch (UnsupportedEncodingException e) {
            Log.e(TAG, "Failed to generate Identity response. Are there invalid UTF-8 characters in the user alias?");
            e.printStackTrace();
        }
        return null;
    }

    @Nullable
    public MessagePacket serializeMessage(@NonNull OwnedIdentityPacket ownedIdentity, String body) {
        // Protocol version 1
        //[[version=1][timestamp=8][sender_public_key=32][message=140][reply_signature=64]][signature=64]
        try {
            byte[] message = new byte[MESSAGE_RESPONSE_LENGTH];
            int writeIndex = 0;
            writeIndex += addVersionToBuffer(message, writeIndex);
            writeIndex += addTypeToBuffer(message, MessagePacket.TYPE, writeIndex);
            writeIndex += addTimestampToBuffer(message, writeIndex);
            writeIndex += addPublicKeyToBuffer(ownedIdentity.publicKey, message, writeIndex);
            writeIndex += addMessageBodyToBuffer(body, message, writeIndex);
            writeIndex += 64; // Empty reply_signature
            writeIndex += addSignatureToBuffer(ownedIdentity.secretKey, message, writeIndex);

            if (writeIndex != MESSAGE_RESPONSE_LENGTH)
                throw new IllegalStateException("Generated Message does not match expected length");

            return deserializeMessageWithIdentity(message, ownedIdentity);
        } catch (UnsupportedEncodingException e) {
            Log.e(TAG, "Failed to generate Identity response. Are there invalid UTF-8 characters in the user alias?");
            e.printStackTrace();
        }
        return null;
    }

    @NonNull
    public NoDataPacket serializeNoDataPacket(@NonNull OwnedIdentityPacket ownedIdentity) {
        byte[] noDataPkt = new byte[NODATA_RESPONSE_LENGTH];
        int writeIndex = 0;
        writeIndex += addVersionToBuffer(noDataPkt, writeIndex);
        writeIndex += addTypeToBuffer(noDataPkt, NoDataPacket.TYPE, writeIndex);
        writeIndex += addTimestampToBuffer(noDataPkt, writeIndex);
        writeIndex += addPublicKeyToBuffer(ownedIdentity.publicKey, noDataPkt, writeIndex);
        writeIndex += addSignatureToBuffer(ownedIdentity.secretKey, noDataPkt, writeIndex);

        if (writeIndex != NODATA_RESPONSE_LENGTH)
            throw new IllegalStateException("Generated Message does not match expected length");

        return deserializeNoDataPacket(noDataPkt);
    }

    /** Incoming
     *
     * Produce protocol Objects from raw transmission data
     */

    @Nullable
    public IdentityPacket deserializeIdentity(@NonNull byte[] identity) {
        if (identity.length != IDENTITY_RESPONSE_LENGTH)
            throw new IllegalArgumentException(String.format("Identity response is %d bytes. Expect %d", identity.length, IDENTITY_RESPONSE_LENGTH));

        // Protocol version 1
        //[[version=1][type=1][timestamp=8][sender_public_key=32][display_name=35]][signature=64]
        try {
            int readIndex     = 0;
            byte[] timestamp  = new byte[Long.SIZE / 8];
            byte[] public_key = new byte[SodiumShaker.crypto_sign_PUBLICKEYBYTES];
            byte[] alias      = new byte[ALIAS_LENGTH];
            byte[] signature  = new byte[SodiumShaker.crypto_sign_BYTES];
            byte[] signedData = new byte[IDENTITY_RESPONSE_LENGTH - SodiumShaker.crypto_sign_BYTES];

            readIndex += assertBufferVersion(identity, readIndex);
            readIndex += assertBufferType(identity, IdentityPacket.TYPE, readIndex);
            readIndex += getBytesFromBuffer(identity, timestamp, readIndex);
            readIndex += getBytesFromBuffer(identity, public_key, readIndex);
            readIndex += getBytesFromBuffer(identity, alias, readIndex);
            readIndex += getBytesFromBuffer(identity, signature, readIndex);

            System.arraycopy(identity, 0, signedData, 0, signedData.length);
            boolean validSignature = SodiumShaker.verifySignature(public_key, signature, signedData);
            if (!validSignature)
                throw new IllegalStateException("Identity signature does not match content!");

            return new IdentityPacket(public_key, new String(alias, "UTF-8"), getDateFromTimestampBuffer(timestamp), identity);
        } catch (UnsupportedEncodingException e) {
            Log.e(TAG, "Failed to generate Identity response. Are there invalid UTF-8 characters in the user alias?");
            e.printStackTrace();
        }
        return null;
    }

    @Nullable
    public MessagePacket deserializeMessageWithIdentity(@NonNull byte[] message, @NonNull IdentityPacket identity) {
        if (message.length != MESSAGE_RESPONSE_LENGTH)
            throw new IllegalArgumentException(String.format("Message response is illegal length. Got %d expected %d", message.length, MESSAGE_RESPONSE_LENGTH));

        // TODO Don't duplicate this code between de
        MessagePacket messageWithoutIdentity = deserializeMessage(message);
        MessagePacket messageWithIdentity = null;

        if (messageWithoutIdentity != null) {
            messageWithIdentity = MessagePacket.attachIdentityToMessage(messageWithoutIdentity, identity);
        }
        return messageWithIdentity;
    }

    @Nullable
    public MessagePacket deserializeMessage(@NonNull byte[] message) {
        if (message.length != MESSAGE_RESPONSE_LENGTH)
            throw new IllegalArgumentException(String.format("Message response is illegal length. Got %d expected %d", message.length, MESSAGE_RESPONSE_LENGTH));

        // Protocol version 1
        //[[version=1][type=1][timestamp=8][sender_public_key=32][message=140][reply_signature=64]][signature=64]
        try {
            int readIndex          = 0;
            byte[] timestamp       = new byte[Long.SIZE / 8];
            byte[] public_key      = new byte[SodiumShaker.crypto_sign_PUBLICKEYBYTES];
            byte[] body            = new byte[MESSAGE_BODY_LENGTH];
            byte[] signature       = new byte[SodiumShaker.crypto_sign_BYTES];
            byte[] replySignature  = new byte[SodiumShaker.crypto_sign_BYTES];
            byte[] signedData      = new byte[MESSAGE_RESPONSE_LENGTH - SodiumShaker.crypto_sign_BYTES];

            readIndex += assertBufferVersion(message, readIndex);
            readIndex += assertBufferType(message, MessagePacket.TYPE, readIndex);
            readIndex += getBytesFromBuffer(message, timestamp, readIndex);
            readIndex += getBytesFromBuffer(message, public_key, readIndex);
            readIndex += getBytesFromBuffer(message, body, readIndex);
            readIndex += getBytesFromBuffer(message, replySignature, readIndex);
            readIndex += getBytesFromBuffer(message, signature, readIndex);

            System.arraycopy(message, 0, signedData, 0, signedData.length);
            boolean validSignature = SodiumShaker.verifySignature(public_key, signature, signedData);
            if (!validSignature)
                throw new IllegalStateException("Message signature does not match content!");

            return new MessagePacket(public_key, signature, replySignature, getDateFromTimestampBuffer(timestamp), new String(body, "UTF-8"), message);
        } catch (UnsupportedEncodingException e) {
            Log.e(TAG, "Failed to generate Identity response. Are there invalid UTF-8 characters in the user alias?");
            e.printStackTrace();
        }
        return null;
    }

    @NonNull
    public NoDataPacket deserializeNoDataPacket(@NonNull byte[] noDataPkt) {
        if (noDataPkt.length != NODATA_RESPONSE_LENGTH)
            throw new IllegalArgumentException(String.format("NoData response is %d bytes. Expect %d", noDataPkt.length, IDENTITY_RESPONSE_LENGTH));

        // Protocol version 1
        // [[version=1][type=1][timestamp=8][sender_public_key=32]][signature=64]
        int readIndex     = 0;
        byte[] timestamp  = new byte[Long.SIZE / 8];
        byte[] public_key = new byte[SodiumShaker.crypto_sign_PUBLICKEYBYTES];
        byte[] signature  = new byte[SodiumShaker.crypto_sign_BYTES];
        byte[] signedData = new byte[NODATA_RESPONSE_LENGTH - SodiumShaker.crypto_sign_BYTES];

        readIndex += assertBufferVersion(noDataPkt, readIndex);
        readIndex += assertBufferType(noDataPkt, NoDataPacket.TYPE, readIndex);
        readIndex += getBytesFromBuffer(noDataPkt, timestamp, readIndex);
        readIndex += getBytesFromBuffer(noDataPkt, public_key, readIndex);
        readIndex += getBytesFromBuffer(noDataPkt, signature, readIndex);

        System.arraycopy(noDataPkt, 0, signedData, 0, signedData.length);
        boolean validSignature = SodiumShaker.verifySignature(public_key, signature, signedData);
        if (!validSignature)
            throw new IllegalStateException("NoData signature does not match content!");

        return new NoDataPacket(public_key, getDateFromTimestampBuffer(timestamp), signature, noDataPkt);
    }


    public byte getPacketType(@NonNull byte[] message) {
        byte[] type = new byte[1];
        getTypeFromBuffer(message, type, 1);
        return type[0];
    }

    // </editor-fold desc="Public API">

    // <editor-fold desc="Private API">

    private static int addVersionToBuffer(@NonNull byte[] input, int offset) {
        int bytesToWrite = 1;
        assertBufferLength(input, offset + bytesToWrite);

        input[offset] = VERSION;
        return bytesToWrite;
    }

    private static int getVersionFromBuffer(@NonNull byte[] input, @NonNull byte[] version, int offset) {
        int bytesToRead = 1;
        assertBufferLength(input, offset + bytesToRead);
        version[0] = input[offset];
        return bytesToRead;
    }

    private static int addTypeToBuffer(@NonNull byte[] input, byte type, int offset) {
        int bytesToWrite = 1;
        assertBufferLength(input, offset + bytesToWrite);

        input[offset] = type;
        return bytesToWrite;
    }

    private static int getTypeFromBuffer(@NonNull byte[] input, @NonNull byte[] type, int offset) {
        int bytesToRead = 1;
        assertBufferLength(input, offset + bytesToRead);
        type[0] = input[offset];
        return bytesToRead;
    }

    private static int addTimestampToBuffer(@NonNull byte[] input, int offset) {
        synchronized (sTimeStampBuffer) {
            int bytesToWrite = Long.SIZE / 8;
            assertBufferLength(input, offset + bytesToWrite);

            long unixTime64 = System.currentTimeMillis();
            sTimeStampBuffer.rewind();
            sTimeStampBuffer.putLong(unixTime64);
            System.arraycopy(sTimeStampBuffer.array(), 0, input, offset, bytesToWrite);
            return bytesToWrite;
        }
    }

    private static int addPublicKeyToBuffer(@NonNull byte[] public_key, @NonNull byte[] input, int offset) {
        int bytesToWrite = public_key.length;
        assertBufferLength(input, offset + bytesToWrite);

        System.arraycopy(public_key, 0, input, offset, bytesToWrite);
        return bytesToWrite;
    }

    private static int addAliasToBuffer(@NonNull String alias, @NonNull byte[] input, int offset) throws UnsupportedEncodingException {
        int bytesToWrite = ALIAS_LENGTH;
        assertBufferLength(input, offset + bytesToWrite);

        byte[] aliasAsBytes = alias.getBytes("UTF-8");
        byte[] paddedAliasAsBytes = new byte[ALIAS_LENGTH];

        truncateOrPadTextBuffer(aliasAsBytes, paddedAliasAsBytes);

        System.arraycopy(paddedAliasAsBytes, 0, input, offset, bytesToWrite);
        return bytesToWrite;
    }

    private static int addMessageBodyToBuffer(@NonNull String body, @NonNull byte[] input, int offset) throws UnsupportedEncodingException {
        int bytesToWrite = MESSAGE_BODY_LENGTH;
        assertBufferLength(input, offset + bytesToWrite);

        byte[] bodyAsBytes = body.getBytes("UTF-8");
        byte[] paddedBodyAsBytes = new byte[MESSAGE_BODY_LENGTH];

        truncateOrPadTextBuffer(bodyAsBytes, paddedBodyAsBytes);

        System.arraycopy(paddedBodyAsBytes, 0, input, offset, bytesToWrite);
        return bytesToWrite;
    }

    private static int getBytesFromBuffer(@NonNull byte[] input, @NonNull byte[] output, int offset) {
        int bytesToRead = output.length;
        assertBufferLength(input, offset + bytesToRead);

        System.arraycopy(input, offset, output, 0, bytesToRead);
        return bytesToRead;
    }

    /**
     * Generate signature for input from the first byte until the offset byte. Append signature to input after offset byte.
     */
    private static int addSignatureToBuffer(@NonNull byte[] secret_key, @NonNull byte[] input, int offset) {
        int bytesToWrite = SodiumShaker.crypto_sign_BYTES;
        assertBufferLength(input, offset + bytesToWrite);

        byte[] signature = SodiumShaker.generateSignatureForMessage(secret_key, input, offset);

        System.arraycopy(signature, 0, input, offset, bytesToWrite);
        return bytesToWrite;
    }

    /** Utility */

    /**
     * Truncates or pads input to fit precisely inside output.
     * After this call output will contain the truncated or padded input
     */
    private static void truncateOrPadTextBuffer(byte[] input, byte[] output) {
        System.arraycopy(input, 0, output, 0, Math.min(input.length, output.length));
        if (input.length < output.length) {
            for (int x = input.length; x < output.length; x++) {
                output[x] = 0x20; // UTF-8 space
            }
        }
    }

    private static void assertBufferLength(byte[] input, int minimumLength) {
        if (input.length < minimumLength)
            throw new IllegalArgumentException(String.format("Operation requires input buffer length %d. Actual: %d", minimumLength, input.length));
    }

    private static int assertBufferVersion(byte[] input, int offset) {
        byte[] version = new byte[1];
        getVersionFromBuffer(input, version, offset);

        if (version[0] != VERSION)
            throw new IllegalStateException(String.format("Response is for an unknown protocol version. Got %d. Expected %d", version[0], VERSION));
        return 1;
    }

    private static int assertBufferType(byte[] input, byte expectedType, int offset) {
        byte[] type = new byte[1];
        getTypeFromBuffer(input, type, offset);

        if (type[0] != expectedType)
            throw new IllegalStateException(String.format("Response is for an unexpected message type. Got %d. Expected %d", type[0], expectedType));
        return 1;
    }

    @Nullable
    private static Date getDateFromTimestampBuffer(byte[] timestamp) {
        synchronized (sTimeStampBuffer) {
            sTimeStampBuffer.clear();
            sTimeStampBuffer.put(timestamp);
            sTimeStampBuffer.rewind();
            // TODO: Test if flip needed
            return new Date(sTimeStampBuffer.getLong());
        }
    }

    // </editor-fold desc="Private API">
} public class MessagePacket {
    public static final byte TYPE = 0x02;

    final public IdentityPacket sender;
    final public String body;
    final public Date authoredDate;
    final public byte[] signature;
    final public byte[] replySig;
    final public byte[] rawPacket;

    /** Incoming */
    public MessagePacket(@NonNull final byte[] publicKey,
                         @NonNull byte[] signature,
                         @NonNull byte[] replySig,
                         @NonNull Date authoredDate,
                         @NonNull String body,
                         @NonNull byte[] rawPacket) {

        this.body         = body;
        this.signature    = signature;
        this.replySig     = replySig;
        this.rawPacket    = rawPacket;
        this.authoredDate = authoredDate;
        sender            = new IdentityPacket(publicKey, null, null, null); // We don't have the sender's full identity response
    }

    public static MessagePacket attachIdentityToMessage(@NonNull MessagePacket message, @NonNull IdentityPacket identity) {
        return new MessagePacket(identity, message.signature, message.replySig, message.body, message.rawPacket, message.authoredDate);
    }

    /** Outgoing */
    public MessagePacket(@NonNull IdentityPacket sender,
                         @NonNull byte[] signature,
                         @NonNull byte[] replySig,
                         @NonNull String body,
                         @NonNull byte[] rawPacket,
                         @NonNull Date authoredDate) {

        this.body         = body.trim();
        this.signature    = signature;
        this.replySig     = replySig;
        this.rawPacket    = rawPacket;
        this.authoredDate = authoredDate;
        this.sender       = sender;
    }
} public class OwnedIdentityPacket extends IdentityPacket {

    public final byte[] secretKey;

    public OwnedIdentityPacket(@NonNull final byte[] secretKey, @NonNull final byte[] publicKey,
                               @NonNull String alias, byte[] rawPacket) {
        super(publicKey, alias, null, rawPacket);
        this.secretKey = secretKey;
    }
} public interface Protocol {

    /** Outgoing
     *
     * Serialize Protocol Objects to raw transmission data
    **/

    // TODO Decide on a consistent API here
    public byte[] serializeIdentity(@NonNull OwnedIdentityPacket ownedIdentity);

    public MessagePacket serializeMessage(@NonNull OwnedIdentityPacket ownedIdentity, String body);

    public NoDataPacket serializeNoDataPacket(@NonNull OwnedIdentityPacket ownedIdentity);

    /** Incoming
     *
     * Deserialize raw transmission data into Protocol Objects
     */

    public IdentityPacket deserializeIdentity(@NonNull byte[] identity);

    /** Deserialize a message where the author identity is known */
    public MessagePacket deserializeMessageWithIdentity(@NonNull byte[] message, @Nullable IdentityPacket identity);

    /** Deserialize a message where the author identity is not known */
    public MessagePacket deserializeMessage(@NonNull byte[] message);

    public byte getPacketType(@NonNull byte[] message);

} public class Notification {

    /** Notification Ids */
    private static final int MESSAGE_NOTIFICATION_ID = 1;
    private static final int PEER_AVAILABLE_NOTIFICATION_ID = 2;

    private static final int MAX_MESSAGES_TO_SHOW = 6;

    private static final ArrayList<String> sNotificationInboxItems = new ArrayList<>(MAX_MESSAGES_TO_SHOW + 1);

    // <editor-fold desc="Public API">

    /**
     * Display a notification representing peer being available, or remove any indicating such
     * if isAvailable is false.
     *
     * Does not call peer.close()
     */
    public static void displayPeerAvailableNotification(@NonNull Context context, @NonNull Peer peer, boolean isAvailable) {
        NotificationManager mNotificationManager =
                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

        if (!isAvailable) {
            mNotificationManager.cancel(DataUtil.bytesToHex(peer.getPublicKey()), PEER_AVAILABLE_NOTIFICATION_ID);
            return;
        }
        if (peer.getAlias() == null) return; // TODO : Notify of peers without alias?

        String title = String.format("%s is nearby", peer.getAlias());

        Intent resultIntent = new Intent(context, MainActivity.class);

        NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
        builder.setSmallIcon(R.mipmap.ic_launcher);
        builder.setContentTitle(title);
        builder.setContentIntent(makePendingIntent(context, resultIntent));
        builder.setContentText(context.getString(R.string.notification_touch_to_chat));

        mNotificationManager.notify(DataUtil.bytesToHex(peer.getPublicKey()), PEER_AVAILABLE_NOTIFICATION_ID, builder.build());
    }

    /**
     * Display a notification representing a new received message. Multiple calls to this method are displayed as a single
     * notification, showing a preview of the last MAX_MESSAGES_TO_SHOW messages.
     *
     * Does not call message.close() or sender.close()
     */
    public static void displayMessageNotification(@NonNull Context context, @NonNull Message message, @Nullable Peer sender) {
        StringBuilder nBuilder = new StringBuilder();
        if (sender != null && sender.getAlias() != null) {
            nBuilder.append(sender.getAlias());
            nBuilder.append(": ");
        }
        nBuilder.append(message.getBody().length() > 80 ?
                            message.getBody().substring(0, 80) + "..." :
                            message.getBody());
        sNotificationInboxItems.add(nBuilder.toString());
        if (sNotificationInboxItems.size() > MAX_MESSAGES_TO_SHOW) sNotificationInboxItems.remove(sNotificationInboxItems.size()-1);

        Intent resultIntent = new Intent(context, MainActivity.class);

        NotificationCompat.InboxStyle inboxStyle =
                new NotificationCompat.InboxStyle();
        inboxStyle.setBigContentTitle(context.getString(R.string.notification_new_messages));

        for (String inboxItem : sNotificationInboxItems) {
            inboxStyle.addLine(inboxItem);
        }

        SymmetricIdenticon identicon = new SymmetricIdenticon(context);
        identicon.show(new String(sender.getPublicKey()));

        NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
        builder.setContentTitle(context.getString(R.string.notification_new_messages));
        builder.setLargeIcon(loadBitmapFromView(identicon, 640, 480));
        builder.setSmallIcon(R.mipmap.ic_launcher);
        builder.setContentIntent(makePendingIntent(context, resultIntent));
        builder.setStyle(inboxStyle);
        builder.setContentText(sNotificationInboxItems.get(0));
        builder.setAutoCancel(true);
        builder.setCategory(NotificationCompat.CATEGORY_MESSAGE);
        builder.setVibrate(new long[] { 500, 500, 500, 500});

        NotificationManager mNotificationManager =
                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

        mNotificationManager.notify(MESSAGE_NOTIFICATION_ID, builder.build());
    }

    // </editor-fold desc="Public API">

    // <editor-fold desc="Private API">

    private static PendingIntent makePendingIntent(@NonNull Context context, @NonNull Intent resultIntent) {
        TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
        // Adds the back stack
        stackBuilder.addParentStack(MainActivity.class);
        // Adds the Intent to the top of the stack
        stackBuilder.addNextIntent(resultIntent);
        // Gets a PendingIntent containing the entire back stack
        return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
    }

    public static Bitmap loadBitmapFromView(View v, int width, int height) {

        int measuredWidth = View.MeasureSpec.makeMeasureSpec(width,
                View.MeasureSpec.EXACTLY);
        int measuredHeight = View.MeasureSpec.makeMeasureSpec(height,
                View.MeasureSpec.EXACTLY);
        v.measure(measuredWidth, measuredHeight);
        v.layout(0, 0, v.getMeasuredWidth(),v.getMeasuredHeight());

        Bitmap b = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(b);
        v.draw(c);
        return b;
    }

    // </editor-fold desc="Private API">
} public interface LogConsumer {
    public void onLogEvent(String event);
}
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import java.util.HashMap;
import pro.dbro.airshare.app.AirShareService;
import pro.dbro.airshare.transport.Transport;
import pro.dbro.ble.data.ContentProviderStore;
import pro.dbro.ble.data.DataStore;
import pro.dbro.ble.data.model.DataUtil;
import pro.dbro.ble.data.model.Message;
import pro.dbro.ble.data.model.Peer;
import pro.dbro.ble.protocol.BLEProtocol;
import pro.dbro.ble.protocol.MessagePacket;
import pro.dbro.ble.protocol.OwnedIdentityPacket;
import pro.dbro.ble.protocol.Protocol;
import pro.dbro.ble.ui.Notification;
import pro.dbro.ble.ui.activities.LogConsumer;
import timber.log.Timber;
package pro.dbro.ble;





/**
 * Created by davidbrodsky on 10/13/14.
 */
public class ChatClient implements AirShareService.Callback,
                                   ChatPeerFlow.DataOutlet,
                                   ChatPeerFlow.Callback {

    public interface Callback {
        /** Client should not invoke remotePeer#close() */
        void onAppPeerStatusUpdated(@NonNull Peer remotePeer,
                                    @NonNull ConnectionStatus status);
    }

    public static final String TAG = "ChatApp";
    public static final String AIRSHARE_SERVICE_NAME = "BLEMeshChat";

    private Context   mContext;
    private DataStore mDataStore;
    private Protocol  mProtocol;