uk.ac.cam.db538.cryptosms.storage.Conversation.java Source code

Java tutorial

Introduction

Here is the source code for uk.ac.cam.db538.cryptosms.storage.Conversation.java

Source

/*
 *   Copyright 2011 David Brazdil
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
package uk.ac.cam.db538.cryptosms.storage;

import java.nio.ByteBuffer;
import java.util.ArrayList;

import org.joda.time.DateTime;
import org.joda.time.DateTimeComparator;
import org.joda.time.format.DateTimeFormat;

import uk.ac.cam.db538.cryptosms.crypto.Encryption;
import uk.ac.cam.db538.cryptosms.crypto.EncryptionInterface.EncryptionException;
import uk.ac.cam.db538.cryptosms.utils.Charset;
import uk.ac.cam.db538.cryptosms.utils.LowLevel;
import uk.ac.cam.db538.cryptosms.utils.PhoneNumber;
import uk.ac.cam.db538.cryptosms.utils.SimNumber;

/**
 * 
 * Class representing a conversation entry in the secure storage file.
 * 
 * @author David Brazdil
 *
 */
public class Conversation implements Comparable<Conversation> {
    // FILE FORMAT
    private static final int LENGTH_FLAGS = 1;
    private static final int LENGTH_PHONENUMBER = 32;

    private static final int OFFSET_FLAGS = 0;
    private static final int OFFSET_PHONENUMBER = OFFSET_FLAGS + LENGTH_FLAGS;

    private static final int OFFSET_RANDOMDATA = OFFSET_PHONENUMBER + LENGTH_PHONENUMBER;

    private static final int OFFSET_NEXTINDEX = Storage.ENCRYPTED_ENTRY_SIZE - 4;
    private static final int OFFSET_PREVINDEX = OFFSET_NEXTINDEX - 4;
    private static final int OFFSET_MSGSINDEX = OFFSET_PREVINDEX - 4;
    private static final int OFFSET_KEYSINDEX = OFFSET_MSGSINDEX - 4;

    private static final int LENGTH_RANDOMDATA = OFFSET_KEYSINDEX - OFFSET_RANDOMDATA;

    // STATIC

    private static ArrayList<Conversation> cacheConversation = new ArrayList<Conversation>();

    /**
     * Removes all instances from the list of cached objects.
     * Be sure you don't use the instances afterwards.
     */
    public static void forceClearCache() {
        synchronized (cacheConversation) {
            cacheConversation = new ArrayList<Conversation>();
        }
    }

    /**
     * Returns instance of a new Conversation created in one of the empty spaces in file.
     *
     * @return the conversation
     * @throws StorageFileException the storage file exception
     */
    public static Conversation createConversation() throws StorageFileException {
        // create a new one
        Conversation conv = new Conversation(Empty.getEmptyIndex(), false);
        Header.getHeader().attachConversation(conv);
        Storage.notifyChange();
        return conv;
    }

    /**
     * Returns an instance of Conversation class with given index in file.
     *
     * @param phoneNumber    Contacts phone number
     * @return the conversation
     * @throws StorageFileException the storage file exception
     */
    public static Conversation getConversation(String phoneNumber) throws StorageFileException {
        Conversation conv = Header.getHeader().getFirstConversation();
        while (conv != null) {
            if (PhoneNumber.compare(conv.getPhoneNumber(), phoneNumber))
                return conv;
            conv = conv.getNextConversation();
        }
        return null;
    }

    /**
     * Returns an instance of Conversation class with given index in file.
     *
     * @param index    Index in file
     * @return the conversation
     * @throws StorageFileException the storage file exception
     */
    static Conversation getConversation(long index) throws StorageFileException {
        if (index <= 0L)
            return null;

        // try looking it up
        synchronized (cacheConversation) {
            for (Conversation conv : cacheConversation)
                if (conv.getEntryIndex() == index)
                    return conv;
        }

        // create a new one
        return new Conversation(index, true);
    }

    /**
     * Explicitly requests each conversation in the file to be loaded to memory.
     *
     * @throws StorageFileException the storage file exception
     */
    public static void cacheAllConversations() throws StorageFileException {
        Conversation convCurrent = Header.getHeader().getFirstConversation();
        while (convCurrent != null)
            convCurrent = convCurrent.getNextConversation();
    }

    // INTERNAL FIELDS
    private long mEntryIndex; // READ ONLY
    private String mPhoneNumber;
    private long mIndexSessionKeys;
    private long mIndexMessages;
    private long mIndexPrev;
    private long mIndexNext;

    // CONSTRUCTORS

    private Conversation(long index, boolean readFromFile) throws StorageFileException {
        mEntryIndex = index;

        if (readFromFile) {
            byte[] dataEncrypted = Storage.getStorage().getEntry(index);
            byte[] dataPlain;
            try {
                dataPlain = Encryption.getEncryption().decryptSymmetricWithMasterKey(dataEncrypted);
            } catch (EncryptionException e) {
                throw new StorageFileException(e);
            }

            setPhoneNumber(Charset.fromAscii8(dataPlain, OFFSET_PHONENUMBER, LENGTH_PHONENUMBER));
            setIndexSessionKeys(LowLevel.getUnsignedInt(dataPlain, OFFSET_KEYSINDEX));
            setIndexMessages(LowLevel.getUnsignedInt(dataPlain, OFFSET_MSGSINDEX));
            setIndexPrev(LowLevel.getUnsignedInt(dataPlain, OFFSET_PREVINDEX));
            setIndexNext(LowLevel.getUnsignedInt(dataPlain, OFFSET_NEXTINDEX));
        } else {
            // default values
            setPhoneNumber("");
            setIndexSessionKeys(0L);
            setIndexMessages(0L);
            setIndexPrev(0L);
            setIndexNext(0L);

            saveToFile();
        }

        synchronized (cacheConversation) {
            cacheConversation.add(this);
        }
    }

    // FUNCTIONS

    /**
     * Saves data to the storage file.
     *
     * @throws StorageFileException the storage file exception
     */
    public void saveToFile() throws StorageFileException {
        ByteBuffer convBuffer = ByteBuffer.allocate(Storage.ENCRYPTED_ENTRY_SIZE);

        // flags
        byte flags = 0;
        convBuffer.put(flags);

        // phone number
        convBuffer.put(Charset.toAscii8(this.mPhoneNumber, LENGTH_PHONENUMBER));

        // random data
        convBuffer.put(Encryption.getEncryption().generateRandomData(LENGTH_RANDOMDATA));

        // indices
        convBuffer.put(LowLevel.getBytesUnsignedInt(this.mIndexSessionKeys));
        convBuffer.put(LowLevel.getBytesUnsignedInt(this.mIndexMessages));
        convBuffer.put(LowLevel.getBytesUnsignedInt(this.mIndexPrev));
        convBuffer.put(LowLevel.getBytesUnsignedInt(this.mIndexNext));

        byte[] dataEncrypted = null;
        try {
            dataEncrypted = Encryption.getEncryption().encryptSymmetricWithMasterKey(convBuffer.array());
        } catch (EncryptionException e) {
            throw new StorageFileException(e);
        }
        Storage.getStorage().setEntry(this.mEntryIndex, dataEncrypted);
    }

    /**
     * Returns previous instance of Conversation in the double-linked list or null if this is the first.
     * @return
     * @throws StorageFileException
     */
    public Conversation getPreviousConversation() throws StorageFileException {
        return Conversation.getConversation(mIndexPrev);
    }

    /**
     * Returns next instance of Conversation in the double-linked list or null if this is the last.
     * @return
     * @throws StorageFileException
     */
    public Conversation getNextConversation() throws StorageFileException {
        return Conversation.getConversation(mIndexNext);
    }

    /**
     * Returns first SessionKeys object in the stored linked list, or null if there isn't any.
     * @return
     * @throws StorageFileException
     */
    public SessionKeys getFirstSessionKeys() throws StorageFileException {
        if (mIndexSessionKeys == 0)
            return null;
        return SessionKeys.getSessionKeys(mIndexSessionKeys);
    }

    /**
     * Attaches new SessionKeys object to the conversation.
     * Deletes other SessionKeys already attached with the same simNumber.
     *
     * @param keys the keys
     * @throws StorageFileException the storage file exception
     */
    void attachSessionKeys(SessionKeys keys) throws StorageFileException {
        long indexFirstInStack = getIndexSessionKeys();
        if (indexFirstInStack != 0) {
            SessionKeys firstInStack = SessionKeys.getSessionKeys(indexFirstInStack);
            firstInStack.setIndexPrev(keys.getEntryIndex());
            firstInStack.saveToFile();
        }
        keys.setIndexNext(indexFirstInStack);
        keys.setIndexPrev(0L);
        keys.setIndexParent(this.mEntryIndex);
        keys.saveToFile();
        this.setIndexSessionKeys(keys.getEntryIndex());
        this.saveToFile();
    }

    /**
     * Attach new MessageData object to the conversation.
     *
     * @param msg the msg
     * @throws StorageFileException the storage file exception
     */
    void attachMessageData(MessageData msg) throws StorageFileException {
        long indexFirstInStack = getIndexMessages();
        if (indexFirstInStack != 0) {
            MessageData firstInStack = MessageData.getMessageData(indexFirstInStack);
            firstInStack.setIndexPrev(msg.getEntryIndex());
            firstInStack.saveToFile();
        }
        msg.setIndexNext(indexFirstInStack);
        msg.setIndexPrev(0L);
        msg.setIndexParent(this.mEntryIndex);
        msg.saveToFile();
        this.setIndexMessages(msg.getEntryIndex());
        this.saveToFile();
    }

    /**
     * Get the first MessageData object in the linked listed attached to this conversation, or null if there isn't any
     * @return
     * @throws StorageFileException
     */
    public MessageData getFirstMessageData() throws StorageFileException {
        return MessageData.getMessageData(mIndexMessages);
    }

    /**
     * Checks for message data.
     *
     * @return true, if successful
     */
    public boolean hasMessageData() {
        return mIndexMessages != 0;
    }

    /**
     * Get the first MessageData object in the linked listed attached to this conversation, or null if there isn't any
     * @return
     * @throws StorageFileException
     */
    public ArrayList<MessageData> getMessages() throws StorageFileException {
        ArrayList<MessageData> list = new ArrayList<MessageData>();
        MessageData msg = getFirstMessageData();
        while (msg != null) {
            list.add(msg);
            msg = msg.getNextMessageData();
        }
        return list;
    }

    /**
     * Delete MessageData and all the MessageDataParts it controls.
     *
     * @throws StorageFileException the storage file exception
     */
    public void delete() throws StorageFileException {
        Conversation prev = this.getPreviousConversation();
        Conversation next = this.getNextConversation();

        if (prev != null) {
            // this is not the first Conversation in the list
            // update the previous one
            prev.setIndexNext(this.getIndexNext());
            prev.saveToFile();
        } else {
            // this IS the first Conversation in the list
            // update parent
            Header header = Header.getHeader();
            header.setIndexConversations(this.getIndexNext());
            header.saveToFile();
        }

        // update next one
        if (next != null) {
            next.setIndexPrev(this.getIndexPrev());
            next.saveToFile();
        }

        // delete all of the SessionKeys
        SessionKeys keys = getFirstSessionKeys();
        while (keys != null) {
            keys.delete();
            keys = getFirstSessionKeys();
        }

        // delete all of the MessageDatas
        MessageData msg = getFirstMessageData();
        while (msg != null) {
            msg.delete();
            msg = getFirstMessageData();
        }

        // delete this conversation
        Empty.replaceWithEmpty(mEntryIndex);

        // remove from cache
        synchronized (cacheConversation) {
            cacheConversation.remove(this);
        }
        Storage.notifyChange();

        // make this instance invalid
        this.mEntryIndex = -1L;
    }

    /**
     * Returns session keys assigned to this conversation for specified SIM number, or null if there aren't any.
     *
     * @param simNumber the sim number
     * @return the session keys
     * @throws StorageFileException the storage file exception
     */
    public SessionKeys getSessionKeys(SimNumber simNumber) throws StorageFileException {
        SessionKeys keys = getFirstSessionKeys();
        while (keys != null) {
            if (simNumber.equals(keys.getSimNumber()))
                return keys;
            keys = keys.getNextSessionKeys();
        }

        return null;
    }

    /**
     * Goes through all the assigned session keys.
     * If there is a session key with SIM number of the param original,
     * its SIM number is replaced for the one in param replacement and
     * all the other keys matching the param replacement are deleted.
     * If there isn't one matching param original, nothing happens.
     *
     * @param original the original
     * @param replacement the replacement
     * @throws StorageFileException the storage file exception
     */
    public void replaceSessionKeys(SimNumber original, SimNumber replacement) throws StorageFileException {
        if (original.equals(replacement))
            // no point in continuing
            return;

        boolean canBeReplaced = false;
        boolean sthToDelete = false;
        SessionKeys keys = this.getFirstSessionKeys();
        while (keys != null) {
            // go through all the assigned keys
            // look for ones matching the param original
            if (keys.getSimNumber().equals(original))
                // so SIM number of this key should be replaced
                canBeReplaced = true;
            if (keys.getSimNumber().equals(replacement))
                // this matches the new SIM number
                // will be deleted if sth is replaced
                sthToDelete = true;
            keys = keys.getNextSessionKeys();
        }

        if (canBeReplaced) {
            if (sthToDelete) {
                keys = this.getFirstSessionKeys();
                while (keys != null) {
                    if (keys.getSimNumber().equals(replacement))
                        keys.delete();
                    keys = keys.getNextSessionKeys();
                }
            }

            boolean found = false;
            keys = this.getFirstSessionKeys();
            while (keys != null) {
                if (keys.getSimNumber().equals(original)) {
                    // if this is the first key matching original,
                    // its SIM number will be replaced
                    // otherwise deleted because it's redundant
                    if (found)
                        keys.delete();
                    else {
                        keys.setSimNumber(replacement);
                        keys.saveToFile();
                        found = true;
                    }
                }
                keys = keys.getNextSessionKeys();
            }
        }
    }

    /**
     * Goes through all the SessionKeys assigned with the Conversation
     * and deletes those that match the simNumber in parameter,
     * Nothing happens if none are found.
     *
     * @param simNumber the sim number
     * @throws StorageFileException the storage file exception
     */
    public void deleteSessionKeys(SimNumber simNumber) throws StorageFileException {
        SessionKeys temp, keys = getFirstSessionKeys();
        while (keys != null) {
            temp = keys.getNextSessionKeys();
            if (keys.getSimNumber().equals(simNumber))
                keys.delete();
            keys = temp;
        }
        Storage.notifyChange();
    }

    public DateTime getTimeStamp() {
        MessageData firstMessage = null;
        try {
            firstMessage = getFirstMessageData();
        } catch (StorageFileException e) {
        }

        if (firstMessage != null)
            return firstMessage.getTimeStamp();
        else
            return new DateTime();
    }

    /**
     * Returns time in a nice way
     * @return
     */
    public String getFormattedTime() {
        return DateTimeFormat.forPattern("HH:mm").print(getTimeStamp());
    }

    /**
     * Returns whether this conversation should be marked unread
     * @return
     */
    public boolean getMarkedUnread() {
        MessageData firstMessage = null;
        try {
            firstMessage = getFirstMessageData();
        } catch (StorageFileException e) {
        }

        if (firstMessage != null)
            return firstMessage.getUnread();
        else
            return false;
    }

    // STATIC FUNCTIONS

    /**
     * Calls replaceSessionKeys on all the conversations.
     * Calls an update of listeners afterwards.
     *
     * @param original the original
     * @param replacement the replacement
     * @throws StorageFileException the storage file exception
     */
    public static void changeAllSessionKeys(SimNumber original, SimNumber replacement) throws StorageFileException {
        Conversation conv = Header.getHeader().getFirstConversation();
        while (conv != null) {
            conv.replaceSessionKeys(original, replacement);
            conv = conv.getNextConversation();
        }
        Storage.notifyChange();
    }

    /**
     * Returns all SIM numbers stored with session keys of all conversations
     * @return
     * @throws StorageFileException
     */
    public static ArrayList<SimNumber> getAllSimNumbersStored() throws StorageFileException {
        ArrayList<SimNumber> simNumbers = new ArrayList<SimNumber>();

        Conversation conv = Header.getHeader().getFirstConversation();
        while (conv != null) {
            SessionKeys keys = conv.getFirstSessionKeys();
            while (keys != null) {
                boolean found = false;
                for (SimNumber n : simNumbers)
                    if (keys.getSimNumber().equals(n))
                        found = true;
                if (!found)
                    simNumbers.add(keys.getSimNumber());
                keys = keys.getNextSessionKeys();
            }
            conv = conv.getNextConversation();
        }

        return simNumbers;
    }

    /**
     * Filters list of SIM numbers, looking only for phone numbers.
     *
     * @param simNumbers the sim numbers
     * @return the array list
     */
    public static ArrayList<SimNumber> filterOnlyPhoneNumbers(ArrayList<SimNumber> simNumbers) {
        ArrayList<SimNumber> phoneNumbers = new ArrayList<SimNumber>();
        for (SimNumber n : simNumbers)
            if (n.isSerial() == false)
                phoneNumbers.add(n);
        return phoneNumbers;
    }

    /**
     * Filters list of SIM numbers, removing specified one.
     *
     * @param simNumbers the sim numbers
     * @param filter the filter
     * @return the array list
     */
    public static ArrayList<SimNumber> filterOutNumber(ArrayList<SimNumber> simNumbers, SimNumber filter) {
        ArrayList<SimNumber> numbers = new ArrayList<SimNumber>();
        for (SimNumber n : simNumbers)
            if (!n.equals(filter))
                numbers.add(n);
        return numbers;
    }

    // GETTERS / SETTERS
    long getEntryIndex() {
        return mEntryIndex;
    }

    public String getPhoneNumber() {
        return mPhoneNumber;
    }

    public void setPhoneNumber(String phoneNumber) {
        this.mPhoneNumber = phoneNumber;
    }

    long getIndexSessionKeys() {
        return mIndexSessionKeys;
    }

    void setIndexSessionKeys(long indexSessionKyes) {
        if (indexSessionKyes > 0xFFFFFFFFL || indexSessionKyes < 0L)
            throw new IndexOutOfBoundsException();

        this.mIndexSessionKeys = indexSessionKyes;
    }

    long getIndexMessages() {
        return mIndexMessages;
    }

    void setIndexMessages(long indexMessages) {
        if (indexMessages > 0xFFFFFFFFL || indexMessages < 0L)
            throw new IndexOutOfBoundsException();

        this.mIndexMessages = indexMessages;
    }

    long getIndexPrev() {
        return mIndexPrev;
    }

    void setIndexPrev(long indexPrev) {
        if (indexPrev > 0xFFFFFFFFL || indexPrev < 0L)
            throw new IndexOutOfBoundsException();

        this.mIndexPrev = indexPrev;
    }

    long getIndexNext() {
        return mIndexNext;
    }

    void setIndexNext(long indexNext) {
        if (indexNext > 0xFFFFFFFFL || indexNext < 0L)
            throw new IndexOutOfBoundsException();

        this.mIndexNext = indexNext;
    }

    /* (non-Javadoc)
     * @see java.lang.Comparable#compareTo(java.lang.Object)
     */
    @Override
    public int compareTo(Conversation another) {
        try {
            return DateTimeComparator.getInstance().compare(this.getTimeStamp(), another.getTimeStamp());
        } catch (Exception e) {
            return 0;
        }
    }
}