Conversation.java revision 25b939e5a7ecb1e0879b684dc5bc55183cf468b4
15821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)package com.android.mms.data;
25821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
35821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import java.util.HashSet;
45821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import java.util.Iterator;
55821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import java.util.Set;
65821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
75821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import android.content.AsyncQueryHandler;
85821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import android.content.ContentUris;
95821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import android.content.ContentValues;
105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import android.content.Context;
115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import android.database.Cursor;
125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import android.net.Uri;
132a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import android.provider.Telephony.MmsSms;
145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import android.provider.Telephony.Threads;
157dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdochimport android.provider.Telephony.Sms.Conversations;
165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import android.text.TextUtils;
175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import android.util.Log;
185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import com.android.mms.R;
205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import com.android.mms.LogTag;
215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import com.android.mms.transaction.MessagingNotification;
225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import com.android.mms.ui.MessageUtils;
235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import com.android.mms.util.DraftCache;
245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/**
265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * An interface for finding information about conversations and/or creating new ones.
275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */
285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)public class Conversation {
295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private static final String TAG = "Mms/conv";
305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private static final boolean DEBUG = false;
315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private static final Uri sAllThreadsUri =
335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build();
345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private static final String[] ALL_THREADS_PROJECTION = {
365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        Threads._ID, Threads.DATE, Threads.MESSAGE_COUNT, Threads.RECIPIENT_IDS,
375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        Threads.SNIPPET, Threads.SNIPPET_CHARSET, Threads.READ, Threads.ERROR,
385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        Threads.HAS_ATTACHMENT
395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    };
402a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    private static final int ID             = 0;
412a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    private static final int DATE           = 1;
425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private static final int MESSAGE_COUNT  = 2;
435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private static final int RECIPIENT_IDS  = 3;
445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private static final int SNIPPET        = 4;
455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private static final int SNIPPET_CS     = 5;
465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private static final int READ           = 6;
475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private static final int ERROR          = 7;
485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private static final int HAS_ATTACHMENT = 8;
495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private final Context mContext;
525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // The thread ID of this conversation.  Can be zero in the case of a
545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // new conversation where the recipient set is changing as the user
555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // types and we have not hit the database yet to create a thread.
565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private long mThreadId;
575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
582a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    private ContactList mRecipients;    // The current set of recipients.
592a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    private long mDate;                 // The last update time.
605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private int mMessageCount;          // Number of messages.
615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private String mSnippet;            // Text of the most recent message.
625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private boolean mHasUnreadMessages; // True if there are unread messages.
635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private boolean mHasAttachment;     // True if any message has an attachment.
645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private boolean mHasError;          // True if any message is in an error state.
655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
662a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    private static ContentValues mReadContentValues;
675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
682a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
692a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    private Conversation(Context context) {
702a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        mContext = context;
712a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        mRecipients = new ContactList();
722a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        mThreadId = 0;
732a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    }
742a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
752a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    private Conversation(Context context, long threadId) {
762a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        mContext = context;
772a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if (!loadFromThreadId(threadId)) {
782a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            mRecipients = new ContactList();
792a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            mThreadId = 0;
802a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        }
812a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    }
822a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
832a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    private Conversation(Context context, Cursor cursor, boolean allowQuery) {
842a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        mContext = context;
852a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        fillFromCursor(context, this, cursor, allowQuery);
862a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    }
875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Create a new conversation with no recipients.  {@link setRecipients} can
905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * be called as many times as you like; the conversation will not be
915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * created in the database until {@link ensureThreadId} is called.
925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     */
932a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    public static Conversation createNew(Context context) {
942a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        return new Conversation(context);
952a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    }
962a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Find the conversation matching the provided thread ID.
992a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)     */
1002a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    public static Conversation get(Context context, long threadId) {
1012a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        synchronized (Cache.getInstance()) {
1022a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            Conversation conv = Cache.get(threadId);
1032a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            if (conv != null)
1042a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                return conv;
1052a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1062a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            conv = new Conversation(context, threadId);
1072a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            try {
1085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                Cache.put(conv);
1095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            } catch (IllegalStateException e) {
1102a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                LogTag.error("Tried to add duplicate Conversation to Cache");
1112a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            }
1125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            return conv;
1132a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        }
1142a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    }
1155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
1175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Find the conversation matching the provided recipient set.
1185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * When called with an empty recipient list, equivalent to {@link createEmpty}.
1195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     */
1205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    public static Conversation get(Context context, ContactList recipients) {
1215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // If there are no recipients in the list, make a new conversation.
1225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (recipients.size() < 1) {
1235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            return createNew(context);
1245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
1255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        synchronized (Cache.getInstance()) {
1275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            Conversation conv = Cache.get(recipients);
1282a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            if (conv != null)
1295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                return conv;
1305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1312a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            long threadId = getOrCreateThreadId(context, recipients);
1322a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            conv = new Conversation(context, threadId);
1332a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            Cache.put(conv);
1352a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            return conv;
1362a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        }
1372a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    }
1385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
1402a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)     * Find the conversation matching in the specified Uri.  Example
1415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * forms: {@value content://mms-sms/conversations/3} or
1425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * {@value sms:+12124797990}.
1432a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)     * When called with a null Uri, equivalent to {@link createEmpty}.
1445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     */
1455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    public static Conversation get(Context context, Uri uri) {
1465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (uri == null) {
1475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            return createNew(context);
1485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
1495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (DEBUG) {
1515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            Log.v(TAG, "Conversation get URI: " + uri);
1525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
1535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Handle a conversation URI
1545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (uri.getPathSegments().size() >= 2) {
1555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            try {
1565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                long threadId = Long.parseLong(uri.getPathSegments().get(1));
1575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                if (DEBUG) {
1585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    Log.v(TAG, "Conversation get threadId: " + threadId);
1595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                }
1605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                return get(context, threadId);
1615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            } catch (NumberFormatException exception) {
1625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                LogTag.error("Invalid URI: " + uri);
1635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            }
1645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
1655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        String recipient = uri.getSchemeSpecificPart();
1675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        return get(context, ContactList.getByNumbers(recipient, false));
1685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
1695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
1715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Returns true if the recipient in the uri matches the recipient list in this
1725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * conversation.
173868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)     */
1745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    public boolean sameRecipient(Uri uri) {
1755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        int size = mRecipients.size();
1765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (size > 1) {
1775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            return false;
1785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
1795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (uri == null) {
1805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            return size == 0;
1815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
1825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (uri.getPathSegments().size() >= 2) {
1835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            return false;       // it's a thread id for a conversation
1845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
1855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        String recipient = uri.getSchemeSpecificPart();
1865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        ContactList incomingRecipient = ContactList.getByNumbers(recipient, false);
1875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        return mRecipients.equals(incomingRecipient);
1885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
1895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
1915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Returns a temporary Conversation (not representing one on disk) wrapping
1925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * the contents of the provided cursor.  The cursor should be the one
1935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * returned to your AsyncQueryHandler passed in to {@link startQueryForAll}.
1945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * The recipient list of this conversation can be empty if the results
1955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * were not in cache.
1965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     */
1975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // TODO: check why can't load a cached Conversation object here.
1985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    public static Conversation from(Context context, Cursor cursor) {
1995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        return new Conversation(context, cursor, false);
2005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
2015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private void buildReadContentValues() {
2035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (mReadContentValues == null) {
2045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            mReadContentValues = new ContentValues(1);
2055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            mReadContentValues.put("read", 1);
2065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
2075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
2085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
2105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Marks all messages in this conversation as read and updates
2115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * relevant notifications.  This method returns immediately;
2125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * work is dispatched to a background thread.
2135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     */
2145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    public synchronized void markAsRead() {
2155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // If we have no Uri to mark (as in the case of a conversation that
2165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // has not yet made its way to disk), there's nothing to do.
2175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        final Uri threadUri = getUri();
2185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        new Thread(new Runnable() {
2205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            public void run() {
2215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                if (threadUri != null) {
2225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    buildReadContentValues();
2235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    mContext.getContentResolver().update(threadUri, mReadContentValues,
2245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                            "read=0", null);
2255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    mHasUnreadMessages = false;
2265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                }
2275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                // Always update notifications regardless of the read state.
2285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                MessagingNotification.updateAllNotifications(mContext);
2295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            }
2305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }).start();
2315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
2325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
2345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Returns a content:// URI referring to this conversation,
2355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * or null if it does not exist on disk yet.
2365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     */
2375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    public synchronized Uri getUri() {
2385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (mThreadId <= 0)
2395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            return null;
2405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        return ContentUris.withAppendedId(Threads.CONTENT_URI, mThreadId);
2425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
2435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
2455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Return the Uri for all messages in the given thread ID.
2465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * @deprecated
2475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     */
2485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    public static Uri getUri(long threadId) {
2495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // TODO: Callers using this should really just have a Conversation
2505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // and call getUri() on it, but this guarantees no blocking.
2515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        return ContentUris.withAppendedId(Threads.CONTENT_URI, threadId);
2525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
2535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
2555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Returns the thread ID of this conversation.  Can be zero if
2565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * {@link ensureThreadId} has not been called yet.
2575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     */
2585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    public synchronized long getThreadId() {
2595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        return mThreadId;
2605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
2615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
2635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Guarantees that the conversation has been created in the database.
264     * This will make a blocking database call if it hasn't.
265     *
266     * @return The thread ID of this conversation in the database
267     */
268    public synchronized long ensureThreadId() {
269        if (DEBUG) {
270            LogTag.debug("ensureThreadId before: " + mThreadId);
271        }
272        if (mThreadId <= 0) {
273            mThreadId = getOrCreateThreadId(mContext, mRecipients);
274        }
275        if (DEBUG) {
276            LogTag.debug("ensureThreadId after: " + mThreadId);
277        }
278
279        return mThreadId;
280    }
281
282    public synchronized void clearThreadId() {
283        // remove ourself from the cache
284        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
285            LogTag.debug("clearThreadId old threadId was: " + mThreadId + " now zero");
286        }
287        Cache.remove(mThreadId);
288
289        mThreadId = 0;
290    }
291
292    /**
293     * Sets the list of recipients associated with this conversation.
294     * If called, {@link ensureThreadId} must be called before the next
295     * operation that depends on this conversation existing in the
296     * database (e.g. storing a draft message to it).
297     */
298    public synchronized void setRecipients(ContactList list) {
299        mRecipients = list;
300
301        // Invalidate thread ID because the recipient set has changed.
302        mThreadId = 0;
303    }
304
305    /**
306     * Returns the recipient set of this conversation.
307     */
308    public synchronized ContactList getRecipients() {
309        return mRecipients;
310    }
311
312    /**
313     * Returns true if a draft message exists in this conversation.
314     */
315    public synchronized boolean hasDraft() {
316        if (mThreadId <= 0)
317            return false;
318
319        return DraftCache.getInstance().hasDraft(mThreadId);
320    }
321
322    /**
323     * Sets whether or not this conversation has a draft message.
324     */
325    public synchronized void setDraftState(boolean hasDraft) {
326        if (mThreadId <= 0)
327            return;
328
329        DraftCache.getInstance().setDraftState(mThreadId, hasDraft);
330    }
331
332    /**
333     * Returns the time of the last update to this conversation in milliseconds,
334     * on the {@link System.currentTimeMillis} timebase.
335     */
336    public synchronized long getDate() {
337        return mDate;
338    }
339
340    /**
341     * Returns the number of messages in this conversation, excluding the draft
342     * (if it exists).
343     */
344    public synchronized int getMessageCount() {
345        return mMessageCount;
346    }
347
348    /**
349     * Returns a snippet of text from the most recent message in the conversation.
350     */
351    public synchronized String getSnippet() {
352        return mSnippet;
353    }
354
355    /**
356     * Returns true if there are any unread messages in the conversation.
357     */
358    public synchronized boolean hasUnreadMessages() {
359        return mHasUnreadMessages;
360    }
361
362    /**
363     * Returns true if any messages in the conversation have attachments.
364     */
365    public synchronized boolean hasAttachment() {
366        return mHasAttachment;
367    }
368
369    /**
370     * Returns true if any messages in the conversation are in an error state.
371     */
372    public synchronized boolean hasError() {
373        return mHasError;
374    }
375
376    private static long getOrCreateThreadId(Context context, ContactList list) {
377        HashSet<String> recipients = new HashSet<String>();
378        Contact cacheContact = null;
379        for (Contact c : list) {
380            cacheContact = Contact.get(c.getNumber(),true);
381            if (cacheContact != null) {
382                recipients.add(cacheContact.getNumber());
383            } else {
384                recipients.add(c.getNumber());
385            }
386        }
387        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
388            LogTag.debug("getOrCreateThreadId %s", recipients);
389        }
390        return Threads.getOrCreateThreadId(context, recipients);
391    }
392
393    /*
394     * The primary key of a conversation is its recipient set; override
395     * equals() and hashCode() to just pass through to the internal
396     * recipient sets.
397     */
398    @Override
399    public synchronized boolean equals(Object obj) {
400        try {
401            Conversation other = (Conversation)obj;
402            return (mRecipients.equals(other.mRecipients));
403        } catch (ClassCastException e) {
404            return false;
405        }
406    }
407
408    @Override
409    public synchronized int hashCode() {
410        return mRecipients.hashCode();
411    }
412
413    @Override
414    public synchronized String toString() {
415        return String.format("[%s] (tid %d)", mRecipients.serialize(), mThreadId);
416    }
417
418    /**
419     * Remove any obsolete conversations sitting around on disk.
420     * @deprecated
421     */
422    public static void cleanup(Context context) {
423        // TODO: Get rid of this awful hack.
424        context.getContentResolver().delete(Threads.OBSOLETE_THREADS_URI, null, null);
425    }
426
427    /**
428     * Start a query for all conversations in the database on the specified
429     * AsyncQueryHandler.
430     *
431     * @param handler An AsyncQueryHandler that will receive onQueryComplete
432     *                upon completion of the query
433     * @param token   The token that will be passed to onQueryComplete
434     */
435    public static void startQueryForAll(AsyncQueryHandler handler, int token) {
436        handler.cancelOperation(token);
437        handler.startQuery(token, null, sAllThreadsUri,
438                ALL_THREADS_PROJECTION, null, null, Conversations.DEFAULT_SORT_ORDER);
439    }
440
441    /**
442     * Start a delete of the conversation with the specified thread ID.
443     *
444     * @param handler An AsyncQueryHandler that will receive onDeleteComplete
445     *                upon completion of the conversation being deleted
446     * @param token   The token that will be passed to onDeleteComplete
447     * @param deleteAll Delete the whole thread including locked messages
448     * @param threadId Thread ID of the conversation to be deleted
449     */
450    public static void startDelete(AsyncQueryHandler handler, int token, boolean deleteAll,
451            long threadId) {
452        Uri uri = ContentUris.withAppendedId(Threads.CONTENT_URI, threadId);
453        String selection = deleteAll ? null : "locked=0";
454        handler.startDelete(token, null, uri, selection, null);
455    }
456
457    /**
458     * Start deleting all conversations in the database.
459     * @param handler An AsyncQueryHandler that will receive onDeleteComplete
460     *                upon completion of all conversations being deleted
461     * @param token   The token that will be passed to onDeleteComplete
462     * @param deleteAll Delete the whole thread including locked messages
463     */
464    public static void startDeleteAll(AsyncQueryHandler handler, int token, boolean deleteAll) {
465        String selection = deleteAll ? null : "locked=0";
466        handler.startDelete(token, null, Threads.CONTENT_URI, selection, null);
467    }
468
469    /**
470     * Check for locked messages in all threads or a specified thread.
471     * @param handler An AsyncQueryHandler that will receive onQueryComplete
472     *                upon completion of looking for locked messages
473     * @param threadId   The threadId of the thread to search. -1 means all threads
474     * @param token   The token that will be passed to onQueryComplete
475     */
476    public static void startQueryHaveLockedMessages(AsyncQueryHandler handler, long threadId,
477            int token) {
478        handler.cancelOperation(token);
479        Uri uri = MmsSms.CONTENT_LOCKED_URI;
480        if (threadId != -1) {
481            uri = ContentUris.withAppendedId(uri, threadId);
482        }
483        handler.startQuery(token, new Long(threadId), uri,
484                ALL_THREADS_PROJECTION, null, null, Conversations.DEFAULT_SORT_ORDER);
485    }
486
487    /**
488     * Fill the specified conversation with the values from the specified
489     * cursor, possibly setting recipients to empty if {@value allowQuery}
490     * is false and the recipient IDs are not in cache.  The cursor should
491     * be one made via {@link startQueryForAll}.
492     */
493    private static void fillFromCursor(Context context, Conversation conv,
494                                       Cursor c, boolean allowQuery) {
495        synchronized (conv) {
496            conv.mThreadId = c.getLong(ID);
497            conv.mDate = c.getLong(DATE);
498            conv.mMessageCount = c.getInt(MESSAGE_COUNT);
499
500            // Replace the snippet with a default value if it's empty.
501            String snippet = MessageUtils.extractEncStrFromCursor(c, SNIPPET, SNIPPET_CS);
502            if (TextUtils.isEmpty(snippet)) {
503                snippet = context.getString(R.string.no_subject_view);
504            }
505            conv.mSnippet = snippet;
506
507            conv.mHasUnreadMessages = (c.getInt(READ) == 0);
508            conv.mHasError = (c.getInt(ERROR) != 0);
509            conv.mHasAttachment = (c.getInt(HAS_ATTACHMENT) != 0);
510
511            String recipientIds = c.getString(RECIPIENT_IDS);
512            conv.mRecipients = ContactList.getByIds(recipientIds, allowQuery);
513        }
514    }
515
516    /**
517     * Private cache for the use of the various forms of Conversation.get.
518     */
519    private static class Cache {
520        private static Cache sInstance = new Cache();
521        static Cache getInstance() { return sInstance; }
522        private final HashSet<Conversation> mCache;
523        private Cache() {
524            mCache = new HashSet<Conversation>(10);
525        }
526
527        /**
528         * Return the conversation with the specified thread ID, or
529         * null if it's not in cache.
530         */
531        static Conversation get(long threadId) {
532            synchronized (sInstance) {
533                if (DEBUG) {
534                    LogTag.debug("Conversation get with threadId: " + threadId);
535                }
536                dumpCache();
537                for (Conversation c : sInstance.mCache) {
538                    if (DEBUG) {
539                        LogTag.debug("Conversation get() threadId: " + threadId +
540                                " c.getThreadId(): " + c.getThreadId());
541                    }
542                    if (c.getThreadId() == threadId) {
543                        return c;
544                    }
545                }
546            }
547            return null;
548        }
549
550        /**
551         * Return the conversation with the specified recipient
552         * list, or null if it's not in cache.
553         */
554        static Conversation get(ContactList list) {
555            synchronized (sInstance) {
556                if (DEBUG) {
557                    LogTag.debug("Conversation get with ContactList: " + list);
558                    dumpCache();
559                }
560                for (Conversation c : sInstance.mCache) {
561                    if (c.getRecipients().equals(list)) {
562                        return c;
563                    }
564                }
565            }
566            return null;
567        }
568
569        /**
570         * Put the specified conversation in the cache.  The caller
571         * should not place an already-existing conversation in the
572         * cache, but rather update it in place.
573         */
574        static void put(Conversation c) {
575            synchronized (sInstance) {
576                // We update cache entries in place so people with long-
577                // held references get updated.
578                if (DEBUG) {
579                    LogTag.debug("Conversation c: " + c + " put with threadid: " + c.getThreadId() +
580                            " c.hash: " + c.hashCode());
581                    dumpCache();
582                }
583
584                if (sInstance.mCache.contains(c)) {
585                    throw new IllegalStateException("cache already contains " + c +
586                            " threadId: " + c.mThreadId);
587                }
588                sInstance.mCache.add(c);
589            }
590        }
591
592        static void remove(long threadId) {
593            if (DEBUG) {
594                LogTag.debug("remove threadid: " + threadId);
595                dumpCache();
596            }
597            for (Conversation c : sInstance.mCache) {
598                if (c.getThreadId() == threadId) {
599                    sInstance.mCache.remove(c);
600                    return;
601                }
602            }
603        }
604
605        static void dumpCache() {
606            if (DEBUG) {
607                synchronized (sInstance) {
608                    LogTag.debug("Conversation dumpCache: ");
609                    for (Conversation c : sInstance.mCache) {
610                        LogTag.debug("   c: " + c + " c.getThreadId(): " + c.getThreadId() +
611                                " hash: " + c.hashCode());
612                    }
613                }
614            }
615        }
616
617        /**
618         * Remove all conversations from the cache that are not in
619         * the provided set of thread IDs.
620         */
621        static void keepOnly(Set<Long> threads) {
622            synchronized (sInstance) {
623                Iterator<Conversation> iter = sInstance.mCache.iterator();
624                while (iter.hasNext()) {
625                    Conversation c = iter.next();
626                    if (!threads.contains(c.getThreadId())) {
627                        iter.remove();
628                    }
629                }
630            }
631        }
632    }
633
634    /**
635     * Set up the conversation cache.  To be called once at application
636     * startup time.
637     */
638    public static void init(final Context context) {
639        new Thread(new Runnable() {
640            public void run() {
641                cacheAllThreads(context);
642            }
643        }).start();
644    }
645
646    private static void cacheAllThreads(Context context) {
647        synchronized (Cache.getInstance()) {
648            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
649                LogTag.debug("[Conversation] cacheAllThreads");
650            }
651            // Keep track of what threads are now on disk so we
652            // can discard anything removed from the cache.
653            HashSet<Long> threadsOnDisk = new HashSet<Long>();
654
655            // Query for all conversations.
656            Cursor c = context.getContentResolver().query(sAllThreadsUri,
657                    ALL_THREADS_PROJECTION, null, null, null);
658            try {
659                while (c.moveToNext()) {
660                    long threadId = c.getLong(ID);
661                    threadsOnDisk.add(threadId);
662
663                    // Try to find this thread ID in the cache.
664                    Conversation conv = Cache.get(threadId);
665
666                    if (conv == null) {
667                        // Make a new Conversation and put it in
668                        // the cache if necessary.
669                        conv = new Conversation(context, c, true);
670                        try {
671                            Cache.put(conv);
672                        } catch (IllegalStateException e) {
673                            LogTag.error("Tried to add duplicate Conversation to Cache");
674                        }
675                    } else {
676                        // Or update in place so people with references
677                        // to conversations get updated too.
678                        fillFromCursor(context, conv, c, true);
679                    }
680                }
681            } finally {
682                c.close();
683            }
684
685            // Purge the cache of threads that no longer exist on disk.
686            Cache.keepOnly(threadsOnDisk);
687        }
688    }
689
690    private boolean loadFromThreadId(long threadId) {
691        Cursor c = mContext.getContentResolver().query(sAllThreadsUri, ALL_THREADS_PROJECTION,
692                "_id=" + Long.toString(threadId), null, null);
693        try {
694            if (c.moveToFirst()) {
695                fillFromCursor(mContext, this, c, true);
696            } else {
697                LogTag.error("loadFromThreadId: Can't find thread ID " + threadId);
698                return false;
699            }
700        } finally {
701            c.close();
702        }
703        return true;
704    }
705}
706