public class EC25519 {
    private static final int ECC_KEY_LENGTH = 32;

    private static final int ECC_SIGNATURE_RANDOM_BYTES_NUM = 64;

    static {
        System.loadLibrary("curve25519");
        try {
            mRandom = SecureRandom.getInstance("SHA1PRNG");
        } catch (NoSuchAlgorithmException e) {
            throw new AssertionError(e);
        }
    }

    private static SecureRandom mRandom;

    private static native byte[] calculateAgreement(byte[] ourPrivate, byte[] theirPublic);
    private static native byte[] generatePublicKey(byte[] privateKey);
    private static native byte[] generatePrivateKey(byte[] random);

    private static native byte[]  calculateSignature(byte[] random, byte[] privateKey, byte[] message);
    private static native boolean verifySignature(byte[] publicKey, byte[] message, byte[] signature);


    /**
     * Generates a new Curve25519 keys (Montgomery space)
     * @return Generated key pair
     */
    public static KeyPair generateKeyPair() {
        byte[] privateKey = generatePrivateKey();
        byte[] publicKey  = generatePublicKey(privateKey);

        return new KeyPair(publicKey, privateKey);
    }

    private static byte[] generatePrivateKey() {
        byte[] privateKey = new byte[ECC_KEY_LENGTH];
        mRandom.nextBytes(privateKey);

        return generatePrivateKey(privateKey);
    }

    private static byte[] getRandom(int size) {
        try {
            byte[] random = new byte[size];
            SecureRandom.getInstance("SHA1PRNG").nextBytes(random);

            return random;
        } catch (NoSuchAlgorithmException e) {
            throw new AssertionError(e);
        }
    }

    /**
     * Computes the shared secret on the Curve2119 curve
     * @param ourPrivate  Our private key
     * @param theirPublic Their public key
     * @return Curve25519 Shared secret
     */
    public static byte[] calculateCurve25519KeyAgreement(byte[] ourPrivate, byte[] theirPublic) {
        if (ourPrivate.length != ECC_KEY_LENGTH || theirPublic.length != ECC_KEY_LENGTH) {
            throw new InvalidParameterException();
        }
        return calculateAgreement(ourPrivate, theirPublic);
    }

    /**
     * Signs the data with Curve25519 keys. Keys are transposed from a Montgomery to an Edwards space
     * to enable us to produce a ed25519 signature output.
     * @param privateKey Curve25519 key
     * @param message    Message to be signed
     * @return Signed message
     * @throws InvalidParameterException
     */
    public static byte[] calculateEd25519Signature(byte[] privateKey, byte[] message) throws InvalidParameterException {
        if (privateKey.length != ECC_KEY_LENGTH) {
            throw new InvalidParameterException();
        }
        byte[] randomBytes = getRandom(ECC_SIGNATURE_RANDOM_BYTES_NUM);
        return calculateSignature(randomBytes, privateKey, message);
    }

    public static boolean verifyEd25519Signature(byte[] publicKey, byte[] message, byte[] signature)
            throws InvalidParameterException {

        if (publicKey.length != ECC_KEY_LENGTH) {
            throw new InvalidParameterException();
        }
        return verifySignature(publicKey, message, signature);
    }
} public class Chat implements Serializable {

    private long mId;
    private String mTitle;
    private Peer mHost;
    private boolean mPrivate;

    public Chat() {
        mId = -1;
    }

    public long getId() {
        return mId;
    }

    public void setId(long id) {
        mId = id;
    }

    /**
     * @return the other peer in a private chat or the name of the chatroom in a chatroom.
     */
    public String getTitle() {
        if (isPrivate()) {
            return getHost().getUserName();
        }

        return Tarsier.app().getString(R.string.chatroom_default_title);
    }

    public void setTitle(String title) {
        mTitle = title;
    }

    public boolean isPrivate() {
        return mPrivate;
    }

    public void setPrivate(boolean isPrivate) {
        mPrivate = isPrivate;
    }

    public Peer getHost() {
        return mHost;
    }

    public void setHost(Peer host) {
        mHost = host;
    }

    public boolean isHost(Peer peer) {
        return !(peer == null || getHost() == null) && getHost().getId() == peer.getId();

    }

    public Bitmap getPicture() {
        if (isPrivate()) {
            return getHost().getPicture();
        } else {
            return BitmapFactory.decodeResource(Tarsier.app().getResources(),
                    R.drawable.tarsier_group_placeholder);
        }
    }
} public class Message implements BubbleListViewItem {
    private long mChatId;
    private String mText;
    private PublicKey mSenderPublicKey;
    private long mDateTime;
    private boolean mIsSentByUser;
    // mId is set to a value >= 0 when the Message is inserted into the database
    private long mId;

    public Message() {
        mId = -1;
    }

    /**
     * Create a message sent by a peer
     *
     * @param chatId the id of the chat in which the message has been sent
     * @param text the body of the message
     * @param senderPublicKey the public of the peer which sent the message
     * @param dateTime the timestamp at which the message has been sent
     */
    public Message(long chatId, String text, byte[] senderPublicKey, long dateTime) {
        this(chatId, text, new PublicKey(senderPublicKey), dateTime);
    }

    /**
     * Create a message sent by a peer
     *
     * @param chatId the id of the chat in which the message has been sent
     * @param text the body of the message
     * @param senderPublicKey the public key of the peer which sent the message
     * @param dateTime the timestamp at which the message has been sent
     */
    public Message(long chatId, String text, PublicKey senderPublicKey, long dateTime) {
        this();
        mChatId = chatId;
        mText = text;
        mSenderPublicKey = senderPublicKey;
        mIsSentByUser = false;
        mDateTime = dateTime;
    }

    /**
     * Create a message sent by the user
     *
     * @param chatId the id of the chat in which the message has been sent
     * @param text the body of the message
     * @param dateTime the timestamp at which the message has been sent
     */
    public Message(long chatId, String text, long dateTime) {
        this();
        mChatId = chatId;
        mText = text;
        mSenderPublicKey = new PublicKey(Tarsier.app().getUserPreferences().getKeyPair().getPublicKey());
        mIsSentByUser = true;
        mDateTime = dateTime;
    }

    public String getText() {
        return mText;
    }

    public long getChatId() {
        return mChatId;
    }

    public PublicKey getSenderPublicKey() {
        return mSenderPublicKey;
    }

    public long getDateTime() {
        return mDateTime;
    }

    public boolean isSentByUser() {
        return mIsSentByUser;
    }

    public void setId(long newId) {
        mId = newId;
    }

    public void setText(String newText) {
        mText = newText;
    }

    public void setChatId(long newId) {
        mChatId = newId;
    }

    public void setDateTime(long newTime) {
        mDateTime = newTime;
    }

    /**
     * Compute the position of the message in the messages list, depending on whether
     * it was sent by the user or another peer.
     *
     * @return Right if sent by user, left otherwise.
     */
    @Override
    public BubbleAdapter.EndlessListViewType getEndlessListViewType() {
        return mIsSentByUser ? BubbleAdapter.EndlessListViewType.BUBBLE_RIGHT
                : BubbleAdapter.EndlessListViewType.BUBBLE_LEFT;
    }

    @Override
    public long getId() {
        return mId;
    }
} public class Peer implements ByteArraySerializable, Serializable {

    private static final int CASE_THREE = 3;
    private static final int CASE_FOUR = 4;
    private static final int NUMBER_OF_PLACEHOLDERS = 5;

    private long mId;
    private PublicKey mPublicKey;
    private String mUserName;
    private String mStatusMessage;
    private boolean mOnline;
    // not used yet
    private String mPicturePath;

    public Peer() {
        mId = -1;
    }

    public Peer(String name) {
        this();
        mUserName = name;
    }

    public Peer(String name, PublicKey publicKey) {
        this();
        mUserName = name;
        mPublicKey = publicKey;
    }

    /**
     * Create a new peer by unpacking its data from a ProtocolBuffer byte array.
     *
     * @param data The ProtocolBuffer byte array.
     * @throws InvalidProtocolBufferException
     */
    public Peer(byte[] data) throws InvalidProtocolBufferException {
        this();
        TarsierWireProtos.Peer peer = TarsierWireProtos.Peer.parseFrom(data);
        mUserName = peer.getName();
        mPublicKey = new PublicKey(peer.getPublicKey().toByteArray());
    }

    public long getId() {
        return mId;
    }

    public void setId(long id) {
        mId = id;
    }

    public PublicKey getPublicKey() {
        return mPublicKey;
    }

    public void setPublicKey(PublicKey publicKey) {
        mPublicKey = publicKey;
    }

    public String getUserName() {
        return mUserName;
    }

    public void setUserName(String userName) {
        this.mUserName = userName;
    }

    public String getStatusMessage() {
        return mStatusMessage;
    }

    public void setStatusMessage(String statusMessage) {
        this.mStatusMessage = statusMessage;
    }

    public void setOnline(boolean online) {
        mOnline = online;
    }

    public boolean isOnline() {
        return mOnline;
    }

    /**
     * As the network protocol cannot transfer profile pictures,
     * we need this dirty hack to show different profile pictures for up to 5 peers.
     *
     * @return A bitmap representation of a profile picture.
     */
    public Bitmap getPicture() {
        int mod5 = (int) this.getId() % NUMBER_OF_PLACEHOLDERS;

        switch (mod5) {
            case 0:
                return BitmapFactory.decodeResource(Tarsier.app().getResources(),
                        R.drawable.tarsier_placeholder);
            case 1:
                return BitmapFactory.decodeResource(Tarsier.app().getResources(),
                        R.drawable.tarsier1_placeholder);
            case 2:
                return BitmapFactory.decodeResource(Tarsier.app().getResources(),
                        R.drawable.tarsier2_placeholder);
            case CASE_THREE:
                return BitmapFactory.decodeResource(Tarsier.app().getResources(),
                        R.drawable.tarsier3_placeholder);
            case CASE_FOUR:
                return BitmapFactory.decodeResource(Tarsier.app().getResources(),
                        R.drawable.tarsier4_placeholder);
            default:
                return BitmapFactory.decodeResource(Tarsier.app().getResources(),
                        R.drawable.tarsier_placeholder);
        }
    }

    /**
     * Check whether this peer is the actual user of the app, by comparing its public
     * key with the public key stored in the UserPreferences.
     *
     * @return true if they match, false otherwise
     */
    public boolean isUser() {
        return Tarsier.app().getUserRepository().getUser().getPublicKey().equals(getPublicKey());
    }

    public void setPicturePath(String picturePath) {
        mPicturePath = picturePath;
    }

    public String getPicturePath() {
        return mPicturePath;
    }

    public byte[] toByteArray() {
        TarsierWireProtos.Peer.Builder peerBuilder = TarsierWireProtos.Peer.newBuilder();
        peerBuilder.setName(mUserName);
        peerBuilder.setPublicKey(ByteString.copyFrom(mPublicKey.getBytes()));

        return peerBuilder.build().toByteArray();
    }
} public final class User extends Peer {

    @Override
    public boolean isUser() {
        return true;
    }

    @Override
    public Bitmap getPicture() {
        return BitmapFactory.decodeFile(Tarsier.app().getUserPreferences().getPicturePath());
    }
} public class PublicKey implements Serializable, ByteArraySerializable {

    private byte[] mBytes;

    public PublicKey(byte[] bytes) {
        mBytes = bytes;
    }

    public PublicKey(String base64Encoded) {
        mBytes = Base64.decode(base64Encoded, Base64.NO_WRAP);
    }

    public byte[] getBytes() {
        return mBytes;
    }
    public byte[] toByteArray() {
        return mBytes;
    }

    @Override
    public boolean equals(Object that) {
        if (this == that) {
            return true;
        }

        // noinspection SimplifiableIfStatement
        if (that == null || ((Object) this).getClass() != that.getClass()) {
            return false;
        }

        return Arrays.equals(mBytes, ((PublicKey) that).getBytes());
    }

    @Override
    public int hashCode() {
        return Arrays.hashCode(mBytes);
    }

    public String base64Encoded() {
        return Base64.encodeToString(mBytes, Base64.NO_WRAP);
    }

    public String toString() {
        return base64Encoded();
    }

} public class ChatRepository extends AbstractRepository<Chat> {

    private static final String TABLE_NAME = Columns.Chat.TABLE_NAME;
    private static final String ID_DESCEND = Columns.Chat._ID + " DESC";
    private static final String TAG = "ChatRepository";

    private PeerRepository mPeerRepository;

    public ChatRepository(Database database) {
        super(database);

        mPeerRepository = Tarsier.app().getPeerRepository();
    }

    public void insert(Chat chat) throws InvalidModelException, InsertException {
        // cannot use validate(Chat chat) because before inserting a Chat, it's id is < 0
        if (chat == null) {
            throw new InvalidModelException("Chat is null.");
        }

        if (exists(chat)) {
            return;
        }

        ContentValues values = getValuesForChat(chat);

        long rowId = getWritableDatabase().insert(
                TABLE_NAME,
                null,
                values
        );

        if (rowId == -1) {
            throw new InsertException("INSERT operation failed.");
        }

        chat.setId(rowId);
    }

    public void update(Chat chat) throws InvalidModelException, UpdateException {
        validate(chat);

        if (!exists(chat)) {
            return;
        }

        ContentValues values = getValuesForChat(chat);

        String whereClause = Columns.Chat._ID + " = " + chat.getId();

        long rowUpdated = getWritableDatabase().update(
                TABLE_NAME,
                values,
                whereClause,
                null
        );

        if (rowUpdated == 0) {
            throw new UpdateException("UPDATE operation failed.");
        }
    }

    public void delete(Chat chat) throws InvalidModelException, DeleteException {
        validate(chat);

        if (!exists(chat)) {
            return;
        }

        String whereClause = Columns.Chat._ID + " = " + chat.getId();

        long rowDeleted = getWritableDatabase().delete(
                TABLE_NAME,
                whereClause,
                null
        );

        if (rowDeleted == 0) {
            throw new DeleteException("DELETE operation failed.");
        }

        chat.setId(-1);
    }

    public List<Chat> findAll() throws NoSuchModelException {

        Cursor cursor = getReadableDatabase().query(
                TABLE_NAME,
                null, null, null, null, null,
                ID_DESCEND,
                null
        );

        try {
            return buildListFromCursor(cursor);
        } catch (InvalidCursorException e) {
            throw new NoSuchModelException(e);
        } finally {
            cursor.close();
        }
    }

    public Chat findById(long id) throws IllegalArgumentException, NoSuchModelException {
        if (id < 0) {
            throw new IllegalArgumentException("Chat ID is invalid.");
        }

        String whereClause = Columns.Chat._ID + " = " + id;

        Cursor cursor = getReadableDatabase().query(
                TABLE_NAME,
                null,
                whereClause,
                null, null, null, null,
                "1"
        );

        if (!cursor.moveToFirst()) {
            throw new NoSuchModelException("Couldn't find a Chat with id " + id);
        }

        try {
            return buildFromCursor(cursor);
        } catch (InvalidCursorException e) {
            throw new NoSuchModelException(e);
        } finally {
            cursor.close();
        }
    }

    public Chat findPublicChat() throws InvalidModelException, NoSuchModelException {
        String whereClause = Columns.Chat.COLUMN_NAME_IS_PRIVATE + " = 0";

        Cursor cursor = getReadableDatabase().query(
                TABLE_NAME,
                null,
                whereClause,
                null, null, null, null,
                "1"
        );

        if (!cursor.moveToFirst()) {
            Log.d(TAG, "Cannot find public chat, creating a new one.");
            cursor.close();

            Chat chat = new Chat();
            chat.setPrivate(false);
            chat.setHost(Tarsier.app().getUserRepository().getUser());

            try {
                insert(chat);
            } catch (InsertException e) {
                throw new NoSuchModelException(e);
            }

            return chat;
        }

        try {
            return buildFromCursor(cursor);
        } catch (InvalidCursorException e) {
            throw new NoSuchModelException(e);
        } finally {
            cursor.close();
        }
    }

    public Chat findPrivateChatForPeer(Peer peer) throws InvalidModelException, NoSuchModelException {
        return findChatForPeer(peer, true);
    }

    public Chat findChatForPeer(Peer peer, boolean isPrivate) throws InvalidModelException, NoSuchModelException {
        if (peer == null) {
            throw new InvalidModelException("Peer is null.");
        }

        if (peer.getId() < 0) {
            throw new InvalidModelException("Peer ID is invalid.");
        }

        String whereClause = Columns.Chat.COLUMN_NAME_HOST_ID + " = " + peer.getId()
                + " AND "
                + Columns.Chat.COLUMN_NAME_IS_PRIVATE + " = " + (isPrivate ? "1" : "0");

        Cursor cursor = getReadableDatabase().query(
                TABLE_NAME,
                null,
                whereClause,
                null, null, null, null,
                "1"
        );

        if (!cursor.moveToFirst()) {
            cursor.close();

            Chat chat = new Chat();
            chat.setHost(peer);
            chat.setPrivate(isPrivate);

            try {
                insert(chat);
            } catch (InsertException e) {
                throw new NoSuchModelException(e);
            }

            return chat;
        }

        try {
            return buildFromCursor(cursor);
        } catch (InvalidCursorException e) {
            throw new NoSuchModelException(e);
        } finally {
            cursor.close();
        }
    }

    @Override
    protected Chat buildFromCursor(Cursor c) throws InvalidCursorException {
        if (c == null) {
            throw new InvalidCursorException("Cursor is null.");
        }

        if (c.isAfterLast()) {
            throw new InvalidCursorException("Cursor is after last row.");
        }

        if (c.isBeforeFirst()) {
            c.moveToFirst();
        }

        try {
            long id = c.getLong(c.getColumnIndexOrThrow(Columns.Chat._ID));
            String title = c.getString(c.getColumnIndexOrThrow(Columns.Chat.COLUMN_NAME_TITLE));
            long hostId = c.getLong(c.getColumnIndexOrThrow(Columns.Chat.COLUMN_NAME_HOST_ID));
            boolean isPrivate = c.getInt(c.getColumnIndexOrThrow(
                    Columns.Chat.COLUMN_NAME_IS_PRIVATE)) != 0;

            Chat chat = new Chat();
            chat.setId(id);
            chat.setTitle(title);
            chat.setPrivate(isPrivate);

            if (hostId > 0) {
                Peer host = mPeerRepository.findById(hostId);
                chat.setHost(host);
            }

            return chat;

        } catch (CursorIndexOutOfBoundsException | NoSuchModelException e) {
            throw new InvalidCursorException(e);
        }
    }

    private ContentValues getValuesForChat(Chat chat) {
        ContentValues values = new ContentValues();

        values.put(Columns.Chat.COLUMN_NAME_TITLE, chat.getTitle());
        values.put(Columns.Chat.COLUMN_NAME_HOST_ID, chat.getHost().getId());
        values.put(Columns.Chat.COLUMN_NAME_IS_PRIVATE, chat.isPrivate());

        return values;
    }

    private boolean exists(Chat chat) {
        Log.d(TAG, "Check if the chat exists");

        if (chat.getId() < 0) {
            return false;
        }

        String whereClause = Columns.Chat._ID + " = " + chat.getId();

        Cursor cursor = getReadableDatabase().query(
                TABLE_NAME,
                null,
                whereClause,
                null, null, null, null,
                "1"
        );

        return cursor.moveToFirst();
    }

    /**
     * Check if the given chat is valid.
     */
    private void validate(Chat chat) throws InvalidModelException {
        Log.d(TAG, "validate the chat");

        if (chat == null) {
            throw new InvalidModelException("Chat is null.");
        }

        if (chat.getId() < 0) {
            throw new InvalidModelException("Chat ID is invalid.");
        }
    }
} public class MessageRepository extends AbstractRepository<Message> {

    private static final String TABLE_NAME = Columns.Message.TABLE_NAME;
    private static final String DATETIME_DESCEND = Columns.Message.COLUMN_NAME_DATETIME + " DESC";

    public MessageRepository(Database database) {
        super(database);
    }

    public void insert(Message message) throws InvalidModelException, InsertException {
        // cannot use validate(Message message) because before inserting a Message, it's id is < 0
        if (message == null) {
            throw new InvalidModelException("Message is null.");
        }

        if (exists(message)) {
            return;
        }

        ContentValues values = getValuesForMessage(message);

        long rowId = getWritableDatabase().insert(
                TABLE_NAME,
                null,
                values
        );

        if (rowId == -1) {
            throw new InsertException("INSERT operation failed.");
        }

        message.setId(rowId);
    }

    public void update(Message message) throws InvalidModelException, UpdateException {
        validate(message);

        if (!exists(message)) {
            return;
        }

        ContentValues values = getValuesForMessage(message);

        String whereClause = Columns.Message._ID + " = " + message.getId();

        long rowUpdated = getWritableDatabase().update(
                TABLE_NAME,
                values,
                whereClause,
                null
        );

        if (rowUpdated == 0) {
            throw new UpdateException("UPDATE operation failed.");
        }
    }

    public void delete(Message message) throws InvalidModelException, DeleteException {
        validate(message);

        if (!exists(message)) {
            return;
        }

        String whereClause = Columns.Message._ID + " = " + message.getId();

        long rowDeleted = getWritableDatabase().delete(
                TABLE_NAME,
                whereClause,
                null
        );

        if (rowDeleted == 0) {
            throw new DeleteException("DELETE operation failed.");
        }

        message.setId(-1);
    }

    public Message findById(long id)
            throws IllegalArgumentException, NoSuchModelException {

        if (id < 0) {
            throw new IllegalArgumentException("Message ID is invalid.");
        }

        String whereClause = Columns.Message._ID + " = " + id;

        Cursor cursor = getReadableDatabase().query(
                TABLE_NAME,
                null,
                whereClause,
                null, null, null, null,
                "1"
        );

        if (!cursor.moveToFirst()) {
            throw new NoSuchModelException("Couldn't find Message with id " + id);
        }

        try {
            return buildFromCursor(cursor);
        } catch (InvalidCursorException e) {
            throw new NoSuchModelException(e);
        } finally {
            cursor.close();
        }
    }

    /**
     * @param chat   the chat from which the messages shall be retrieved
     * @param until  retrieve messages older than this timestamp
     * @param max    number of messages to retrieve
     * @return a list of message
     */
    public List<Message> findByChatUntil(Chat chat, long until, int max)
            throws IllegalArgumentException, NoSuchModelException {

        if (chat.getId() < 0) {
            throw new IllegalArgumentException("Chat ID is invalid.");
        }

        String whereClause = Columns.Message.COLUMN_NAME_CHAT_ID + " = " + chat.getId()
                + " AND " + Columns.Message.COLUMN_NAME_DATETIME + " < " + until;

        Cursor cursor = getReadableDatabase().query(
                TABLE_NAME,
                null,
                whereClause,
                null, null, null,
                DATETIME_DESCEND,
                Integer.toString(max));

        try {
            return buildListFromCursor(cursor);
        } catch (InvalidCursorException e) {
            throw new NoSuchModelException(e);
        } finally {
            cursor.close();
        }
    }

    public Message getLastMessageOf(Chat chat) throws NoSuchModelException, InvalidModelException {
        if (chat == null) {
            throw new InvalidModelException("Chat is null.");
        }

        if (chat.getId() < 0) {
            throw new IllegalArgumentException("Chat ID is invalid.");
        }

        String whereClause = Columns.Message.COLUMN_NAME_CHAT_ID + " = " + chat.getId();

        Cursor cursor = getReadableDatabase().query(
                TABLE_NAME,
                null,
                whereClause,
                null, null, null,
                DATETIME_DESCEND,
                "1"
        );

        try {
            return buildFromCursor(cursor);
        } catch (InvalidCursorException e) {
            throw new NoSuchModelException(e);
        } finally {
            cursor.close();
        }
    }

    public List<Message> findAll() throws NoSuchModelException {

        Cursor cursor = getReadableDatabase().query(
                TABLE_NAME,
                null, null, null, null, null,
                DATETIME_DESCEND,
                null
        );

        try {
            return buildListFromCursor(cursor);
        } catch (InvalidCursorException e) {
            throw new NoSuchModelException(e);
        } finally {
            cursor.close();
        }
    }

    @Override
    protected Message buildFromCursor(Cursor c) throws InvalidCursorException {
        if (c == null) {
            throw new InvalidCursorException("Cursor is null.");
        }

        if (c.isAfterLast()) {
            throw new InvalidCursorException("Cursor is after last row.");
        }

        if (c.isBeforeFirst()) {
            c.moveToFirst();
        }

        try {
            int chatId = c.getInt(c.getColumnIndexOrThrow(Columns.Message.COLUMN_NAME_CHAT_ID));
            String text = c.getString(c.getColumnIndexOrThrow(Columns.Message.COLUMN_NAME_MSG));
            String base64PublicKey = c.getString(
                    c.getColumnIndexOrThrow(Columns.Message.COLUMN_NAME_SENDER_PUBLIC_KEY));
            boolean isSentByUser = c.getInt(c.getColumnIndexOrThrow(
                    Columns.Message.COLUMN_NAME_SENT_BY_USER)) != 0;
            long dateTime = c.getLong(c.getColumnIndexOrThrow(Columns.Message.COLUMN_NAME_DATETIME));

            Message message;
            if (isSentByUser) {
                message = new Message(chatId, text, dateTime);
            } else {
                message = new Message(chatId, text, new PublicKey(base64PublicKey), dateTime);
            }
            message.setId(c.getLong(c.getColumnIndexOrThrow(Columns.Message._ID)));

            return message;

        } catch (CursorIndexOutOfBoundsException e) {
            throw new InvalidCursorException(e);
        }
    }

    private ContentValues getValuesForMessage(Message message) {
        ContentValues values = new ContentValues();

        values.put(Columns.Message.COLUMN_NAME_CHAT_ID, message.getChatId());
        values.put(Columns.Message.COLUMN_NAME_MSG, message.getText());
        values.put(Columns.Message.COLUMN_NAME_SENDER_PUBLIC_KEY, message.getSenderPublicKey().base64Encoded());
        values.put(Columns.Message.COLUMN_NAME_SENT_BY_USER, message.isSentByUser());
        values.put(Columns.Message.COLUMN_NAME_DATETIME, message.getDateTime());

        return values;
    }

    private boolean exists(Message message) {
        if (message.getId() < 0) {
            return false;
        }

        String whereClause = Columns.Message._ID + " = " + message.getId();

        Cursor cursor = getReadableDatabase().query(
                TABLE_NAME,
                null,
                whereClause,
                null, null, null, null,
                "1"
        );

        return cursor.moveToFirst();
    }

    // Check if the Message model is valid
    private void validate(Message message) throws InvalidModelException {
        if (message == null) {
            throw new InvalidModelException("Message is null.");
        }

        if (message.getId() < 0) {
            throw new InvalidModelException("Message ID is invalid.");
        }
    }
} public class PeerRepository extends AbstractRepository<Peer> {

    private static final String TABLE_NAME = Columns.Peer.TABLE_NAME;
    private static final String ID_DESCEND = Columns.Peer._ID + " DESC";

    public PeerRepository(Database database) {
        super(database);
    }

    public Peer findById(long id) throws IllegalArgumentException, NoSuchModelException {
        if (id < 0) {
            throw new IllegalArgumentException("Peer ID is invalid.");
        }

        String whereClause = Columns.Peer._ID + " = " + id;

        Cursor cursor = getReadableDatabase().query(
                TABLE_NAME,
                null,
                whereClause,
                null, null, null, null,
                "1"
        );

        if (!cursor.moveToFirst()) {
            throw new NoSuchModelException("Could not find Peer with id " + id);
        }

        try {
            return buildFromCursor(cursor);
        } catch (InvalidCursorException e) {
            throw new NoSuchModelException(e);
        } finally {
            cursor.close();
        }
    }

    public Peer findByPublicKey(byte[] publicKey)
            throws IllegalArgumentException, NoSuchModelException {

        if (publicKey == null) {
            throw new IllegalArgumentException("PublicKey is null.");
        }

        return findByPublicKey(new PublicKey(publicKey));
    }

    public Peer findByPublicKey(PublicKey publicKey)
            throws IllegalArgumentException, NoSuchModelException {

        if (publicKey == null) {
            throw new IllegalArgumentException("PublicKey is null.");
        }

        String whereClause = Columns.Peer.COLUMN_NAME_PUBLIC_KEY + " = '" + publicKey.base64Encoded() + "'";

        Cursor cursor = getReadableDatabase().query(
                TABLE_NAME,
                null,
                whereClause,
                null, null, null, null,
                "1"
        );

        if (!cursor.moveToFirst()) {
            throw new NoSuchModelException(
                    "Cannot find a peer with public key "
                            + publicKey.base64Encoded()
                            + "\n user publicKey : "
                            + new PublicKey(Tarsier.app()
                            .getUserPreferences()
                            .getKeyPair()
                            .getPublicKey())
                            .base64Encoded());
        }

        try {
            return buildFromCursor(cursor);
        } catch (InvalidCursorException e) {
            throw new NoSuchModelException(e);
        } finally {
            cursor.close();
        }
    }

    public void insert(Peer peer) throws InvalidModelException, InsertException {
        // cannot use validate(Peer peer) because before inserting a Peer, it's id is < 0
        if (peer == null) {
            throw new InvalidModelException("Peer is null.");
        }

        if (exists(peer)) {
            return;
        }

        ContentValues values = getPeerValues(peer);

        long rowId = getWritableDatabase().insert(
                TABLE_NAME,
                null,
                values
        );

        if (rowId == -1) {
            throw new InsertException("INSERT operation failed.");
        }

        peer.setId(rowId);
    }

    public void insertIfNotExistsWithPublicKey(Peer peer) throws InvalidModelException, InsertException {
        if (peer == null) {
            throw new InvalidModelException("Peer is null.");
        }

        try {
            Peer existingPeer = findByPublicKey(peer.getPublicKey());
            peer.setId(existingPeer.getId());
            update(peer);
        } catch (NoSuchModelException e) {
            insert(peer);
        } catch (UpdateException e) {
            e.printStackTrace();
        }
    }

    public void update(Peer peer) throws InvalidModelException, UpdateException {
        validate(peer);

        if (!exists(peer)) {
            return;
        }

        ContentValues values = getPeerValues(peer);

        String whereClause = Columns.Peer._ID + " = " + peer.getId();

        long updatedRows = getWritableDatabase().update(
                TABLE_NAME,
                values,
                whereClause,
                null
        );

        if (updatedRows == 0) {
            throw new UpdateException("UPDATE operation failed.");
        }
    }

    public void delete(Peer peer) throws InvalidModelException, DeleteException {
        validate(peer);

        if (!exists(peer)) {
            return;
        }

        String whereClause = Columns.Peer._ID + " = " + peer.getId();

        long deletedRows = getWritableDatabase().delete(
                TABLE_NAME,
                whereClause,
                null
        );

        if (deletedRows == 0) {
            throw new DeleteException("DELETE operation failed.");
        }

        peer.setId(-1);
    }

    // return null if there are no chats in the database
    public List<Peer> findAll() throws NoSuchModelException {

        Cursor cursor = getReadableDatabase().query(
                TABLE_NAME,
                null, null, null, null, null,
                ID_DESCEND,
                null
        );

        try {
            return buildListFromCursor(cursor);
        } catch (InvalidCursorException e) {
            throw new NoSuchModelException(e);
        } finally {
            cursor.close();
        }
    }

    @Override
    protected Peer buildFromCursor(Cursor c) throws InvalidCursorException {
        if (c == null) {
            throw new InvalidCursorException("Cursor is null.");
        }

        if (c.isAfterLast()) {
            throw new InvalidCursorException("Cursor is after last row.");
        }

        if (c.isBeforeFirst()) {
            c.moveToFirst();
        }

        try {
            long id = c.getLong(c.getColumnIndexOrThrow(Columns.Peer._ID));
            String base64PublicKey = c.getString(c.getColumnIndexOrThrow(Columns.Peer.COLUMN_NAME_PUBLIC_KEY));
            String userName = c.getString(c.getColumnIndexOrThrow(Columns.Peer.COLUMN_NAME_USERNAME));
            String statusMessage = c.getString(c.getColumnIndexOrThrow(Columns.Peer.COLUMN_NAME_STATUS_MESSAGE));
            String picturePath = c.getString(c.getColumnIndexOrThrow(Columns.Peer.COLUMN_NAME_PICTURE_PATH));
            boolean online = c.getInt(c.getColumnIndexOrThrow(Columns.Peer.COLUMN_NAME_IS_ONLINE)) != 0;

            Peer peer = new Peer();
            peer.setId(id);
            peer.setPublicKey(new PublicKey(base64PublicKey));
            peer.setUserName(userName);
            peer.setStatusMessage(statusMessage);
            peer.setPicturePath(picturePath);
            peer.setOnline(online);

            return peer;

        } catch (CursorIndexOutOfBoundsException e) {
            throw new InvalidCursorException(e);
        }
    }

    private ContentValues getPeerValues(Peer peer) {
        ContentValues values = new ContentValues();

        values.put(Columns.Peer.COLUMN_NAME_PUBLIC_KEY, peer.getPublicKey().base64Encoded());
        values.put(Columns.Peer.COLUMN_NAME_USERNAME, peer.getUserName());
        values.put(Columns.Peer.COLUMN_NAME_STATUS_MESSAGE, peer.getStatusMessage());
        values.put(Columns.Peer.COLUMN_NAME_PICTURE_PATH, peer.getPicturePath());
        values.put(Columns.Peer.COLUMN_NAME_IS_ONLINE, peer.isOnline());

        return values;
    }

    private boolean exists(Peer peer) {
        if (peer.getId() < 0) {
            return false;
        }

        String whereClause = Columns.Peer._ID + " = " + peer.getId();

        Cursor cursor = getReadableDatabase().query(
                TABLE_NAME,
                null,
                whereClause,
                null, null, null, null,
                "1"
        );

        return cursor.moveToFirst();
    }

    // Check if the Peer model is valid
    private void validate(Peer peer) throws InvalidModelException {
        if (peer == null) {
            throw new InvalidModelException("Peer is null.");
        }

        if (peer.getId() < 0) {
            throw new InvalidModelException("Peer ID is invalid.");
        }
    }
} public class InsertException extends DomainException {

    public InsertException(String detailMessage) {
        super(detailMessage);
    }
} public class InvalidModelException extends DomainException {

    public InvalidModelException(String detailMessage) {
        super(detailMessage);
    }
} public class DateUtil {
    private static Calendar calendar = Calendar.getInstance();
    private static final int NEGATIVE_FIVE = -5;
    private static final int YEAR_2014 = 2014;

    public static String computeDateSeparator(long timestamp) {
        //Set calendar to Now
        calendar.setTime(new Date());

        //Today at 00:00
        calendar.set(Calendar.HOUR, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        long todayTimestamp = calendar.getTimeInMillis();

        calendar.add(Calendar.DAY_OF_MONTH, -1); //Yesterday at 00:00
        long yesterdayTimestamp = calendar.getTimeInMillis();

        calendar.add(Calendar.DAY_OF_MONTH, NEGATIVE_FIVE); //One week ago at 00:00
        long weekTimestamp = calendar.getTimeInMillis();

        Format format;
        if (timestamp >= todayTimestamp) { // today
            return "Today";
        } else if (timestamp >= yesterdayTimestamp) { // yesterday
            return "Yesterday";
        } else if (timestamp >= weekTimestamp) { // one week ago
            format = new SimpleDateFormat("EEEE", Locale.ENGLISH);
            return format.format(timestamp);
        } else { // further
            format = new SimpleDateFormat("dd.MM.yyyy");
            return format.format(timestamp);
        }
    }

    public static long getNowTimestamp() {
        //Set calendar to Now
        calendar.setTime(new Date());

        return calendar.getTimeInMillis();
    }

    public static long getFirstDecemberTimestamp() {
        //Set calendar to 1st december
        calendar.set(YEAR_2014, Calendar.DECEMBER, 1, 0, 0, 0);
        return calendar.getTimeInMillis();
    }

    public static String computeHour(long timestamp) {
        Date sentHour = new Date(timestamp);

        Format format = new SimpleDateFormat("HH:mm");
        return format.format(sentHour);
    }
}
import android.util.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import ch.tarsier.tarsier.crypto.EC25519;
import ch.tarsier.tarsier.domain.model.Chat;
import ch.tarsier.tarsier.domain.model.Message;
import ch.tarsier.tarsier.domain.model.Peer;
import ch.tarsier.tarsier.domain.model.User;
import ch.tarsier.tarsier.domain.model.value.PublicKey;
import ch.tarsier.tarsier.domain.repository.ChatRepository;
import ch.tarsier.tarsier.domain.repository.MessageRepository;
import ch.tarsier.tarsier.domain.repository.PeerRepository;
import ch.tarsier.tarsier.exception.InsertException;
import ch.tarsier.tarsier.exception.InvalidModelException;
import ch.tarsier.tarsier.util.DateUtil;
package ch.tarsier.tarsier.test;



/**
 * The FillDBForTesting class is used to fill the mocked database for testing purposes.
 *
 * @author gluthier
 */
public class FillDBForTesting {

    private static final String TAG = "FillDBForTesting";

    public static User user;

    private static final int NUMBER_OF_PEERS = 3;
    public static Peer[] peers;

    public static Chat chat2;
    public static Chat chat1;

    private static final int NUMBER_OF_MESSAGES = 75;
    public static Message[] messagesChat1;
    public static Message[] messagesChat2;

    public static List<Chat> allChats;
    public static List<Message> allMessages;
    public static List<Peer> allPeers;

    private static final long FIRST_DECEMBER_2014_MIDNIGHT = DateUtil.getFirstDecemberTimestamp();

    /**
     * Insert the Models into the Repositories
     *
     * @param chatRepository The ChatRepository
     * @param messageRepository The MessageRepository
     * @param peerRepository The PeerRepository
     */
    public static void populate(ChatRepository chatRepository,
                                MessageRepository messageRepository,
                                PeerRepository peerRepository) {

        //Generate the user
        user = new User();
        user.setUserName("user");
        user.setPublicKey(new PublicKey(EC25519.generateKeyPair().getPublicKey()));