public class SymmetricIdenticon extends Identicon {
	
	private static final int CENTER_COLUMN_INDEX = 3;

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

	public SymmetricIdenticon(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	public SymmetricIdenticon(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
	}
	
	protected int getSymmetricColumnIndex(int row) {
		if (row < CENTER_COLUMN_INDEX) {
			return row;
		}
		else {
			return getColumnCount() - row - 1;
		}
	}

	@Override
	protected boolean isCellVisible(int row, int column) {
		return getByte(3 + row * CENTER_COLUMN_INDEX + getSymmetricColumnIndex(column)) >= 0;
	}

	@Override
	protected int getIconColor() {
		return Color.rgb(getByte(0)+128, getByte(1)+128, getByte(2)+128);
	}

	@Override
	protected int getRowCount() {
		return 5;
	}

	@Override
	protected int getColumnCount() {
		return 5;
	}

} 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;
    private AirShareService.ServiceBinder mAirShareServiceBinder;
    private Callback mCallback;

    private HashMap<pro.dbro.airshare.session.Peer, ChatPeerFlow> mFlows = new HashMap<>();

    /** AirShare Peer -> BLEMeshChat Peer id */
    private BiMap<pro.dbro.airshare.session.Peer, Integer> mConnectedPeers = HashBiMap.create();

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

    public ChatClient(@NonNull Context context) {
        mContext = context;

        mProtocol  = new BLEProtocol();
        mDataStore = new ContentProviderStore(context);
    }

    public void setAirShareServiceBinder(AirShareService.ServiceBinder binder) {
        mAirShareServiceBinder = binder;
        mAirShareServiceBinder.setCallback(this);
    }

    public void setCallback(Callback callback) {
        mCallback = callback;
    }

    // <editor-fold desc="Identity & Availability">

    public void makeAvailable() {
        if (mDataStore.getPrimaryLocalPeer() == null) {
            Timber.e("No primary Identity. Cannot make client available");
            return;
        }

        if (mAirShareServiceBinder == null) {
            Timber.e("No AirShareBinder set. Cannot make available");
            return;
        }

        mAirShareServiceBinder.advertiseLocalUser();
        mAirShareServiceBinder.scanForOtherUsers();
    }

    public void makeUnavailable() {
        if (mAirShareServiceBinder == null) {
            Timber.e("No AirShareBinder set. Cannot make unavailable");
            return;
        }

        mAirShareServiceBinder.stop();
    }

    public Peer getPrimaryLocalPeer() {
        return mDataStore.getPrimaryLocalPeer();
    }

    public Peer createPrimaryIdentity(String alias) {
        // TODO Test if this should be moved to background thread and async call?
        return mDataStore.createLocalPeerWithAlias(alias, mProtocol);
    }

    // </editor-fold desc="Identity & Availability">

    // <editor-fold desc="Messages">

    public void sendPublicMessageFromPrimaryIdentity(String body) {
        MessagePacket messagePacket = mProtocol.serializeMessage((OwnedIdentityPacket) getPrimaryLocalPeer().getIdentity(), body);
        mDataStore.createOrUpdateMessageWithProtocolMessage(messagePacket).close();
        // TODO : Send to connected peers. Future peers will get message during flow
        if (mAirShareServiceBinder != null) {

            for (pro.dbro.airshare.session.Peer peer : mConnectedPeers.keySet()) {
                ChatPeerFlow flow = mFlows.get(peer);
                // If we're actively flowing with a peer, add the message to that flow
                // else, send immediately
                if (flow != null && !flow.isComplete())
                    flow.queueMessage(messagePacket);
                else
                    mAirShareServiceBinder.send(messagePacket.rawPacket, peer);
            }

        }
    }

    // </editor-fold desc="Messages">

    public DataStore getDataStore() {
        return mDataStore;
    }

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

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

    @Override
    public void onAppPeerStatusUpdated(@NonNull ChatPeerFlow flow,
                                       @NonNull Peer remotePeer,
                                       @NonNull ConnectionStatus status) {

        Timber.d("%s %s", remotePeer.getAlias(), status == ConnectionStatus.CONNECTED ? "connected" : "disconnected");
        if (mCallback != null)
            mCallback.onAppPeerStatusUpdated(remotePeer, status);

        if (!mAirShareServiceBinder.isActivityReceivingMessages())
            Notification.displayPeerAvailableNotification(mContext, remotePeer, status == ConnectionStatus.CONNECTED);

        switch (status) {
            case CONNECTED:
                mConnectedPeers.put(flow.getRemoteAirSharePeer(), remotePeer.getId());
                break;

            case DISCONNECTED:
                mConnectedPeers.remove(flow.getRemoteAirSharePeer());
                break;
        }
    }

    @Override
    public void onMessageSent(@NonNull ChatPeerFlow flow, @NonNull Message message, @NonNull Peer recipient) {
        Timber.d("Sent message: '%s'", message.getBody());
        // TODO : Might be unnecessary
        message.close();
    }

    @Override
    public void onMessageReceived(@NonNull ChatPeerFlow flow, @NonNull Message message, Peer sender) {
        Timber.d("Received message: '%s' with sig '%s' ", message.getBody(), DataUtil.bytesToHex(message.getSignature()).substring(0, 3));

        // We don't check that mAirShareServiceBinder is not null because this callback is provoked
        // by the binder callbacks

        // Send message notification if it's a new message and no Activity is reported active
        if (!mAirShareServiceBinder.isActivityReceivingMessages()) {
            Notification.displayMessageNotification(mContext, message, sender);
            message.close();
        }
    }

    @Override
    public void onDataRecevied(@NonNull AirShareService.ServiceBinder binder, @Nullable byte[] data, @NonNull pro.dbro.airshare.session.Peer sender, @Nullable Exception exception) {
        ChatPeerFlow flow = mFlows.get(sender);

        if (flow == null) {
            Timber.w("No flow for %s", sender.getAlias());
            return;
        }

        try {
            flow.onDataReceived(data);
        } catch (ChatPeerFlow.UnexpectedDataException e) {
            Timber.e(e, "Error processing received data");
        }
    }

    @Override
    public void onDataSent(@NonNull AirShareService.ServiceBinder binder, @Nullable byte[] data, @NonNull pro.dbro.airshare.session.Peer recipient, @Nullable Exception exception) {
        ChatPeerFlow flow = mFlows.get(recipient);

        if (flow == null) {
            Timber.w("No flow for %s", recipient.getAlias());
            return;
        }

        try {
            flow.onDataSent(data);
        } catch (ChatPeerFlow.UnexpectedDataException e) {
            Timber.e(e, "Error processing sent data");
        }
    }

    @Override
    public void onPeerStatusUpdated(@NonNull AirShareService.ServiceBinder binder, @NonNull pro.dbro.airshare.session.Peer peer, @NonNull Transport.ConnectionStatus newStatus, boolean peerIsHost) {
        if (newStatus == Transport.ConnectionStatus.CONNECTED) {
            mConnectedPeers.put(peer, null); // We will add the BLEMeshChat peer id after identity is received
            Timber.d("Beginning flow with %s as %s", peer.getAlias(), peerIsHost ? "host" : "client");
            mFlows.put(peer, new ChatPeerFlow(mDataStore, mProtocol, this, peer, peerIsHost, this));
        }
        else if (newStatus == Transport.ConnectionStatus.DISCONNECTED) {

            if (!mConnectedPeers.containsKey(peer) || mConnectedPeers.get(peer) == null) {
                if (mConnectedPeers.containsKey(peer)) mConnectedPeers.remove(peer);
                Timber.w("Cannot report peer %s disconnected, no connection record", peer.getAlias());
                return;
            }

            int blePeerId = mConnectedPeers.get(peer);
            Peer remotePeer = mDataStore.getPeerById(blePeerId);
            onAppPeerStatusUpdated(mFlows.get(peer), remotePeer, ConnectionStatus.DISCONNECTED);
        }
    }

    @Override
    public void onPeerTransportUpdated(@NonNull AirShareService.ServiceBinder binder,
                                       @NonNull pro.dbro.airshare.session.Peer peer,
                                       int newTransportCode,
                                       @Nullable Exception exception) {
        // unused. The networking demands of this app appear to works fine over BLE
    }

    @Override
    public void sendData(pro.dbro.airshare.session.Peer peer, byte[] data) {
        if(mAirShareServiceBinder == null) {
            Timber.e("AirShare Service binder is null! Cannot send data");
            return;
        }
        mAirShareServiceBinder.send(data, peer);
    }

    // </editor-fold desc="Private API">
} public class ChatPeerFlow {

    public static class UnexpectedDataException extends Exception {
        public UnexpectedDataException(String detailMessage) {
            super(detailMessage);
        }
    }

    /** Entity responsible for sending data to a peer */
    public static interface DataOutlet {
        public void sendData(Peer peer, byte[] data);
    }

    public static interface Callback {

        public static enum ConnectionStatus { CONNECTED, DISCONNECTED }

        public void onAppPeerStatusUpdated(@NonNull ChatPeerFlow flow,
                                           @NonNull pro.dbro.ble.data.model.Peer peer,
                                           @NonNull ConnectionStatus status);

        public void onMessageSent(@NonNull ChatPeerFlow flow,
                                  @NonNull Message message,
                                  @NonNull pro.dbro.ble.data.model.Peer recipient);

        public void onMessageReceived(@NonNull ChatPeerFlow flow,
                                      @NonNull Message message,
                                      @Nullable pro.dbro.ble.data.model.Peer sender);

    }

    private static final int MESSAGES_PER_RESPONSE = 50;
    private static final int IDENTITIES_PER_RESPONSE = 10;
    public static enum State { CLIENT_WRITE_ID, HOST_WRITE_ID, CLIENT_WRITE_MSGS, HOST_WRITE_MSGS }

    private State mState = State.CLIENT_WRITE_ID;
    private OwnedIdentityPacket mLocalIdentity;
    private Peer mRemoteAirSharePeer;
    private Protocol mProtocol;
    private DataStore mDataStore;
    private DataOutlet mOutlet;
    private IdentityPacket mRemoteIdentity;
    private Callback mCallback;
    private ArrayDeque<MessagePacket> mMessageOutbox = new ArrayDeque<>();
    private ArrayDeque<IdentityPacket> mIdentityOutbox = new ArrayDeque<>();

    private boolean mPeerIsHost;
    private boolean mIsComplete = false;
    private boolean mFetchedMessages = false;
    private boolean mFetchedIdentities = false;
    private boolean mGotRemotePeerIdentity = false;

    public ChatPeerFlow(DataStore dataStore,
                        Protocol protocol,
                        DataOutlet outlet,
                        Peer remotePeer,
                        boolean peerIsHost,
                        Callback callback) {

        mRemoteAirSharePeer = remotePeer;
        mOutlet = outlet;
        mProtocol = protocol;
        mDataStore = dataStore;
        mLocalIdentity = (OwnedIdentityPacket) dataStore.getPrimaryLocalPeer().getIdentity();
        mPeerIsHost = peerIsHost;
        mCallback = callback;

        // Client initiates flow
        if (mPeerIsHost)
            sendIdentity();
    }

    public boolean isComplete() {
        return mIsComplete;
    }

    public Peer getRemoteAirSharePeer() {
        return mRemoteAirSharePeer;
    }

    public void queueMessage(MessagePacket message) {
        mMessageOutbox.add(message);
    }

    /**
     * Called when data is acknowledged as sent to the peer passed to this instance's constructor
     * @return whether this flow is complete and should not receive further events.
     */
    public boolean onDataSent(byte[] data) throws UnexpectedDataException {
        // When data is ack'd we should be in a local-peer writing state
        if ((!mPeerIsHost && (mState == State.CLIENT_WRITE_ID || (mState == State.CLIENT_WRITE_MSGS && !mIsComplete))) ||
            (mPeerIsHost && (mState == State.HOST_WRITE_ID || (mState == State.HOST_WRITE_MSGS && !mIsComplete)))) {

            throw new IllegalStateException(String.format("onDataSent invalid state %s for local as %s", mState, mPeerIsHost ? "client" : "host"));

        }

        Timber.d("Sent data %s", DataUtil.bytesToHex(data));

        byte type = mProtocol.getPacketType(data);

        // TODO : Perhaps we should cache last sent item to avoid deserializing bytes we've
        // just serialized in sendData
        switch (mState) {
            case HOST_WRITE_ID:
            case CLIENT_WRITE_ID:

                switch(type) {
                    case IdentityPacket.TYPE:

                        IdentityPacket sentIdPkt = mProtocol.deserializeIdentity(data);
                        mDataStore.createOrUpdateRemotePeerWithProtocolIdentity(sentIdPkt);
                        // We can only report the identity sent once we know the peer's identity
                        // We also always want to send our own identity first
                        if (mRemoteIdentity != null) {
                            Timber.d("Marked identity %s delivered to %s", sentIdPkt.alias, mRemoteIdentity.alias);
                            mDataStore.markIdentityDeliveredToPeer(sentIdPkt, mRemoteIdentity);
                        }

                        mIdentityOutbox.poll();

                        sendAsAppropriate();
                        break;

                    case NoDataPacket.TYPE:

                        incrementStateAndSendAsAppropriate();
                        break;

                    default:
                        throw new UnexpectedDataException(String.format("Expected IdentityPacket (type %d). Got type %d", IdentityPacket.TYPE, type));

                }
                break;

            case HOST_WRITE_MSGS:
            case CLIENT_WRITE_MSGS:

                switch(type) {
                    case MessagePacket.TYPE:

                        MessagePacket msgPkt = mProtocol.deserializeMessageWithIdentity(data, mRemoteIdentity);
                        Message msg = mDataStore.createOrUpdateMessageWithProtocolMessage(msgPkt);
                        // Mark incoming messages as delivered to sender
                        mDataStore.markMessageDeliveredToPeer(msgPkt, mRemoteIdentity);
                        mCallback.onMessageSent(this, msg, mDataStore.getPeerByPubKey(mRemoteIdentity.publicKey));

                        mMessageOutbox.poll();

                        sendAsAppropriate();
                        break;

                    case NoDataPacket.TYPE:

                        incrementStateAndSendAsAppropriate();
                        break;

                    default:
                        throw new UnexpectedDataException(String.format("Expected MessagePacket (type %d). Got type %d", MessagePacket.TYPE, type));

                }

                break;

            default:
                Timber.e("Flow received unexpected response from client peer");
        }
        return mIsComplete;
    }

    /**
     * Called when data is received from the peer passed to this instance's constructor
     * @return whether this flow is complete and should not receive further events.
     */
    public boolean onDataReceived(byte[] data) throws UnexpectedDataException {
        // When data comes in we should be in a remote-peer writing state
        if ((!mPeerIsHost && (mState == State.HOST_WRITE_ID || (mState == State.HOST_WRITE_MSGS && !mIsComplete))) ||
            (mPeerIsHost && (mState == State.CLIENT_WRITE_ID || (mState == State.CLIENT_WRITE_MSGS && !mIsComplete)))) {

            throw new IllegalStateException(String.format("onDataReceived invalid state %s for local as %s", mState, mPeerIsHost ? "client" : "host"));

        }

        //Timber.d("Received data %s", DataUtil.bytesToHex(data));

        byte type = mProtocol.getPacketType(data);

        switch (mState) {
            case HOST_WRITE_ID:
            case CLIENT_WRITE_ID:

                switch(type) {
                    case IdentityPacket.TYPE:

                        mRemoteIdentity = mProtocol.deserializeIdentity(data);
                        Timber.d("Got remote identity for %s", mRemoteIdentity.alias);
                        pro.dbro.ble.data.model.Peer remotePeer = mDataStore.createOrUpdateRemotePeerWithProtocolIdentity(mRemoteIdentity);
                        // Only treat first identity as that of connected peer
                        if (!mGotRemotePeerIdentity) {
                            mCallback.onAppPeerStatusUpdated(this, remotePeer, Callback.ConnectionStatus.CONNECTED);
                            mGotRemotePeerIdentity = true;
                        }
                        break;

                    case NoDataPacket.TYPE:

                        Timber.d("Received identity NoData");
                        incrementStateAndSendAsAppropriate();
                        break;

                    default:

                        throw new UnexpectedDataException(String.format("Expected IdentityPacket (type %d). Got type %d", IdentityPacket.TYPE, type));
                }

                break;

            case HOST_WRITE_MSGS:
            case CLIENT_WRITE_MSGS:

                switch (type) {
                    case MessagePacket.TYPE:

                        MessagePacket msgPkt = mProtocol.deserializeMessageWithIdentity(data, mRemoteIdentity);
                        Timber.d("Received msg %s", msgPkt.body);

                        // Mark incoming messages as delivered to sender

                        boolean isNewMessage = true;
                        Message existingMessage = mDataStore.getMessageBySignature(msgPkt.signature);
                        if (existingMessage != null) {
                            isNewMessage = false;
                            existingMessage.close();
                        }

                        // TODO : Allow updating a message?
                        Message msg = mDataStore.createOrUpdateMessageWithProtocolMessage(msgPkt);
                        mDataStore.markMessageDeliveredToPeer(msgPkt, mRemoteIdentity);

                        if (isNewMessage)
                            mCallback.onMessageReceived(this, msg, mDataStore.getPeerByPubKey(mRemoteIdentity.publicKey));

                        break;

                    case NoDataPacket.TYPE:

                        Timber.d("Received msg NoData");
                        incrementStateAndSendAsAppropriate();
                        break;

                    default:

                        throw new UnexpectedDataException(String.format("Expected MessagePacket (type %d). Got type %d", MessagePacket.TYPE, type));

                }
                break;

            default:
                Timber.e("Flow received unexpected response from client peer");
        }
        return mIsComplete;
    }

    private void sendIdentity() {
        if (!mFetchedIdentities) {

            // If we're the client, we're initiating the identity flow, and we won't have the remote identity yet
            mIdentityOutbox.addAll(getIdentitiesForIdentity(mRemoteIdentity == null ? null : mRemoteIdentity.publicKey,
                    IDENTITIES_PER_RESPONSE));
            mFetchedIdentities = true;
        }

        Timber.d("Send identity %s", mIdentityOutbox.size() == 0 ? "NoData" : "");
        mOutlet.sendData(mRemoteAirSharePeer,
                         mIdentityOutbox.size() == 0 ?
                            mProtocol.serializeNoDataPacket(mLocalIdentity).rawPacket :
                            mIdentityOutbox.peek().rawPacket);
    }

    private void sendMessage() {
        if (!mFetchedMessages) {
            mMessageOutbox.addAll(getMessagesForIdentity(mRemoteIdentity.publicKey, MESSAGES_PER_RESPONSE));
            mFetchedMessages = true;
        }

        Timber.d("Send message %s", mMessageOutbox.size() == 0 ? "NoData" : "");
        mOutlet.sendData(mRemoteAirSharePeer,
                         mMessageOutbox.size() == 0 ?
                            mProtocol.serializeNoDataPacket(mLocalIdentity).rawPacket :
                            mMessageOutbox.peek().rawPacket);
    }

    private void incrementStateAndSendAsAppropriate() {
        if (mState == State.HOST_WRITE_MSGS) {
            Timber.d("ChatPeerFlow complete!");
            mIsComplete = true;
            return;
        }

        mState = State.values()[mState.ordinal() + 1];
        Timber.d("ChatPeerFlow New State : %s", mState);
        sendAsAppropriate();
    }

    private void sendAsAppropriate() {

        switch (mState) {
            case CLIENT_WRITE_ID:
                if (mPeerIsHost) sendIdentity();
                break;

            case HOST_WRITE_ID:
                if (!mPeerIsHost) sendIdentity();
                break;

            case CLIENT_WRITE_MSGS:
                if (mPeerIsHost) sendMessage();
                break;

            case HOST_WRITE_MSGS:
                if (!mPeerIsHost) sendMessage();
                break;
        }
    }

    /**
     * Return a queue of message packets for delivery to remote identity with given public key.
     *
     * If recipientPublicKey is null, queues most recent messages
     */
    private ArrayDeque<MessagePacket> getMessagesForIdentity(@Nullable byte[] recipientPublicKey, int maxMessages) {
        ArrayDeque<MessagePacket> messagePacketQueue = new ArrayDeque<>();

        if (recipientPublicKey != null) {
            // Get messages not delievered to peer
            pro.dbro.ble.data.model.Peer recipient = mDataStore.getPeerByPubKey(recipientPublicKey);
            List<MessagePacket> messages = mDataStore.getOutgoingMessagesForPeer(recipient, maxMessages);

            if (messages == null || messages.size() == 0) {
                Timber.d("Got no messages for peer with pub key " + DataUtil.bytesToHex(recipientPublicKey));
            } else {
                messagePacketQueue.addAll(messages);
            }
        } else {
            // Get most recent messages
            MessageCollection recentMessages = mDataStore.getRecentMessages();
            for (int x = 0; x < Math.min(maxMessages, recentMessages.getCursor().getCount()); x++) {
                Message currentMessage = recentMessages.getMessageAtPosition(x);
                if (currentMessage != null)
                    messagePacketQueue.add(currentMessage.getProtocolMessage(mDataStore));
            }
            recentMessages.close();
        }
        return messagePacketQueue;
    }

    /**
     * Return a queue of identity packets for delivery to the remote identity with the given
     * public key.
     *
     * If recipientPublicKey is null, or no messages undelivered for recipient,
     * the user identity will be queued. As such this method will never return a null
     * or empty queue. Thus it should only be called once per flow and should not
     * be used as an indication of whether identity transmission with a peer is complete.
     */
    private ArrayDeque<IdentityPacket> getIdentitiesForIdentity(@Nullable byte[] recipientPublicKey, int maxIdentities) {
        List<IdentityPacket> identities = null;
        ArrayDeque<IdentityPacket> identityPacketQueue = new ArrayDeque<>();
        if (recipientPublicKey != null) {
            // We have a public key for the remote peer, fetch undelivered identities
            pro.dbro.ble.data.model.Peer recipient = mDataStore.getPeerByPubKey(recipientPublicKey);
            identities = mDataStore.getOutgoingIdentitiesForPeer(recipient, maxIdentities);
        }

        if (identities == null || identities.size() == 0) {
            Timber.d("Got no identities to send for peer %s. Sending own identity", recipientPublicKey == null ? "" : "with pub key " + DataUtil.bytesToHex(recipientPublicKey).substring(2, 6));
            // For now, at least send our identity
            if (identities == null) identities = new ArrayList<>(1);
            identities.add(mDataStore.getPrimaryLocalPeer().getIdentity());
        }
        identityPacketQueue.addAll(identities);

        return identityPacketQueue;
    }

} public class PrefsManager {

    /** SharedPreferences store names */
    private static final String APP_PREFS = "prefs";

    /** SharedPreferences keys */
    private static final String APP_STATUS = "status";

    public static int getStatus(Context context) {
        return context.getSharedPreferences(APP_PREFS, Context.MODE_PRIVATE)
                      .getInt(APP_STATUS, 0);
    }

    public static void setStatus(Context context, int status) {
        context.getSharedPreferences(APP_PREFS, Context.MODE_PRIVATE).edit()
               .putInt(APP_STATUS, status)
               .commit();
    }

    public static void clearState(Context context) {
        context.getSharedPreferences(APP_PREFS, Context.MODE_PRIVATE).edit().clear().apply();
    }
} 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 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 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 class StatusArrayAdapter extends ArrayAdapter<String> {

    public StatusArrayAdapter(Context context, ArrayList<String> statuses) {
        super(context, android.R.layout.simple_spinner_dropdown_item, statuses);
    }

    @Override
    public View getDropDownView(int position, View convertView, ViewGroup parent) {
        return getCustomView(position, convertView, parent);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        return getCustomView(position, convertView, parent);
    }

    public View getCustomView(int position, View convertView, ViewGroup parent) {

        Context context = parent.getContext();

        // Get the data item for this position
        String status = getItem(position);
        // Check if an existing view is being reused, otherwise inflate the view
        if (convertView == null) {
            convertView = LayoutInflater.from(getContext()).inflate(android.R.layout.simple_spinner_dropdown_item, parent, false);
            ((TextView) convertView).setCompoundDrawablePadding((int) dipToPixels(context, 8));
        }

        TextView statusLabel = (TextView) convertView;
        statusLabel.setText(status);

        String[] choices = context.getResources().getStringArray(R.array.status_options);
        if (status.equals(choices[0])) { // Always online
            statusLabel.setCompoundDrawablesWithIntrinsicBounds(context.getDrawable(R.drawable.status_always_online), null, null, null);
        }
        else if (status.equals(choices[1])) { // Online when using app
            statusLabel.setCompoundDrawablesWithIntrinsicBounds(context.getDrawable(R.drawable.status_online_in_foreground), null, null, null);
        } else if (status.equals(choices[2])) { // Offline
            statusLabel.setCompoundDrawablesWithIntrinsicBounds(context.getDrawable(R.drawable.status_offline), null, null, null);
        } else {
            Timber.e("Unknown status. Cannot set adapter view correctly");
            statusLabel.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
        }

        return convertView;
    }

    public static float dipToPixels(Context context, float dipValue) {
        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dipValue, metrics);
    }
} public class MessagingFragment extends Fragment implements MessageAdapter.MessageSelectedListener {
    public static final String TAG = "MessageListFragment";

    public static interface ChatFragmentCallback {
        public void onMessageSendRequested(String message);
        public void onMessageSelected(View identiconView, View usernameView, int messageId, int peerId);
    }

    private ChatFragmentCallback mCallback;
    DataStore mDataStore;
    RecyclerView mRecyclerView;
    MessageAdapter mAdapter;
    EditText mMessageEntry;
    View mRoot;

    public MessagingFragment() {
        // Required empty public constructor
    }

    public void setDataStore(DataStore dataStore) {
        mDataStore = dataStore;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        if (mDataStore == null)
            throw new IllegalStateException("MessageListFragment must be equipped with a DataStore. Did you call #setDataStore");

        // Inflate the layout for this fragment
        mRoot = inflater.inflate(R.layout.fragment_message, container, false);
        mMessageEntry = (EditText) mRoot.findViewById(R.id.messageEntry);
        mMessageEntry.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (actionId == EditorInfo.IME_ACTION_SEND) {
                    sendMessage(v.getText().toString());
                    v.setText("");
                    return true;
                }
                return false;
            }
        });
        mRoot.findViewById(R.id.sendMessageButton).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onSendMessageButtonClick(v);
            }
        });
        mRecyclerView = (RecyclerView) mRoot.findViewById(R.id.recyclerView);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
        mAdapter = new MessageAdapter(getActivity(), null, mDataStore, this, MessageAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
        mRecyclerView.setAdapter(mAdapter);
        return mRoot;
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            mCallback = (ChatFragmentCallback) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString()
                    + " must implement ChatFragmentCallback");
        }
    }

    public void onSendMessageButtonClick(View v) {
        sendMessage(mMessageEntry.getText().toString());
        mMessageEntry.setText("");
    }

    private void sendMessage(String message) {
        if (message.length() == 0) return;
        Log.i(TAG, "Sending message " + message);
        // For now treat all messsages as public broadcast
        mCallback.onMessageSendRequested(message);

    }

    @Override
    public void onMessageSelected(View identiconView, View usernameView, int messageId, int peerId) {
        mCallback.onMessageSelected(identiconView, usernameView, messageId, peerId);
    }

    public void animateIn() {
        mRoot.setAlpha(0);
        ObjectAnimator animator = ObjectAnimator.ofFloat(mRoot, "alpha", 0f, 1f)
                .setDuration(300);

        animator.setStartDelay(550);
        animator.start();
    }
} public class ProfileFragment extends Fragment {

    DataStore mDataStore;
    RecyclerView mRecyclerView;
    MessageAdapter mAdapter;
    Peer mFromPeer;

//    TextView mUsernameView;

    public static ProfileFragment createForPeer(@NonNull DataStore dataStore,
                                                @NonNull Peer peer) {

        ProfileFragment frag = new ProfileFragment();
        frag.setFromPeer(peer);
        frag.setDataStore(dataStore);
        return frag;
    }

    public ProfileFragment() {
        // Required empty public constructor
    }

    public void setFromPeer(Peer fromPeer) {
        mFromPeer = fromPeer;
    }

    public void setDataStore(DataStore dataStore) {
        mDataStore = dataStore;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        if (mDataStore == null)
            throw new IllegalStateException("MessageListFragment must be equipped with a DataStore. Did you call #setDataStore");

        // Inflate the layout for this fragment
        final View root = inflater.inflate(R.layout.fragment_peer_profile, container, false);
        mRecyclerView = (RecyclerView) root.findViewById(R.id.recyclerView);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
        mAdapter = new MessageAdapter(getActivity(), mFromPeer, mDataStore, null, MessageAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
        mRecyclerView.setAdapter(mAdapter);

//        SymmetricIdenticon identicon = (SymmetricIdenticon) root.findViewById(R.id.profile_identicon);
//        ((SymmetricIdenticon) root.findViewById(R.id.profile_identicon)).show(new String(mFromPeer.getPublicKey()));
//        mUsernameView = ((TextView) root.findViewById(R.id.profile_name));
//        mUsernameView.setText(mFromPeer.getAlias());

        return root;
    }
} public class WelcomeFragment extends Fragment {

    public interface WelcomeFragmentCallback {
        public void onNameChosen(String name);
    }

    private WelcomeFragmentCallback mCallback;

    public WelcomeFragment() {
        // Required empty public constructor
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            mCallback = (WelcomeFragmentCallback) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString() + " must implement WelcomeFragmentCallback");
        }
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.fragment_welcome, container, false);
        ((EditText) root.findViewById(R.id.aliasEntry)).setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) {
                mCallback.onNameChosen(textView.getText().toString());
                return false;
            }
        });
        return root;
    }


}
import android.animation.ValueAnimator;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.FragmentManager;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Bitmap;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.graphics.Palette;
import android.support.v7.widget.Toolbar;
import android.transition.Slide;
import android.transition.TransitionSet;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import com.nispok.snackbar.Snackbar;
import java.util.ArrayList;
import java.util.Arrays;
import butterknife.ButterKnife;
import butterknife.InjectView;
import hugo.weaving.DebugLog;
import im.delight.android.identicons.SymmetricIdenticon;
import pro.dbro.airshare.app.AirShareService;
import pro.dbro.airshare.app.ui.AirShareFragment;
import pro.dbro.ble.ChatClient;
import pro.dbro.ble.ChatPeerFlow;
import pro.dbro.ble.PrefsManager;
import pro.dbro.ble.R;
import pro.dbro.ble.data.model.Peer;
import pro.dbro.ble.protocol.OwnedIdentityPacket;
import pro.dbro.ble.ui.Notification;
import pro.dbro.ble.ui.adapter.StatusArrayAdapter;
import pro.dbro.ble.ui.fragment.MessagingFragment;
import pro.dbro.ble.ui.fragment.ProfileFragment;
import pro.dbro.ble.ui.fragment.WelcomeFragment;
import timber.log.Timber;
package pro.dbro.ble.ui.activities;





public class MainActivity extends AppCompatActivity implements LogConsumer,
        WelcomeFragment.WelcomeFragmentCallback,
        AirShareFragment.Callback,
        MessagingFragment.ChatFragmentCallback, ChatClient.Callback {