Conversation.java revision 6cba8248f3e41921b03cc74a823a6347016e69ba
1f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereirapackage com.android.mms.data;
2f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira
3f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereiraimport java.util.HashSet;
4f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereiraimport java.util.Iterator;
5f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereiraimport java.util.Set;
6f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira
7f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereiraimport android.content.AsyncQueryHandler;
8f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereiraimport android.content.ContentUris;
9f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereiraimport android.content.ContentValues;
10f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereiraimport android.content.Context;
11f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereiraimport android.database.Cursor;
12f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereiraimport android.net.Uri;
13f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereiraimport android.provider.Telephony.MmsSms;
14f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereiraimport android.provider.Telephony.Threads;
15f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereiraimport android.provider.Telephony.Sms.Conversations;
16f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereiraimport android.text.TextUtils;
17f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereiraimport android.util.Log;
18f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira
19f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereiraimport com.android.mms.R;
20b2033d855ab0f13e253e5403ce25989bcbc49488Andy Huangimport com.android.mms.transaction.MessagingNotification;
21f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereiraimport com.android.mms.ui.MessageUtils;
22f913894d3f1fa80b7174477999074b813e1deb12Mindy Pereiraimport com.android.mms.util.DraftCache;
237517e3b61b898a57f19be0671f70d58a82224643Andy Huang
248db7e407109532557718c6b8064792f2df7a073dMindy Pereira/**
25f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira * An interface for finding information about conversations and/or creating new ones.
26f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira */
27f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereirapublic class Conversation {
28f913894d3f1fa80b7174477999074b813e1deb12Mindy Pereira    private static final String TAG = "Conversation";
297c411aafd9ef059f881b7532426e61d1def54a07Andrew Sapperstein    private static final boolean DEBUG = false;
307c411aafd9ef059f881b7532426e61d1def54a07Andrew Sapperstein
31af9dc29f589c6ee771db22e3a2923b8bae46aafcJin Cao    private static final Uri sAllThreadsUri =
32f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira        Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build();
33f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira
34f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira    private static final String[] ALL_THREADS_PROJECTION = {
35042a530b2296487fa5899a3e871214ac4a47e3d8Andy Huang        Threads._ID, Threads.DATE, Threads.MESSAGE_COUNT, Threads.RECIPIENT_IDS,
36bf232c3735f65b1a4746943e4a134e59e36f0bdePaul Westbrook        Threads.SNIPPET, Threads.SNIPPET_CHARSET, Threads.READ, Threads.ERROR,
37f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira        Threads.HAS_ATTACHMENT
381ef988f0c8be136fda75ed207c222413db1d3f0cMindy Pereira    };
396126d72ae2769bd39451872f45781cadb5b90515Mark Wei    private static final int ID             = 0;
40f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira    private static final int DATE           = 1;
4106642fab1bf4ab95b5dd97a65b262845cf60c865Mindy Pereira    private static final int MESSAGE_COUNT  = 2;
42b2033d855ab0f13e253e5403ce25989bcbc49488Andy Huang    private static final int RECIPIENT_IDS  = 3;
43f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira    private static final int SNIPPET        = 4;
44b334c9035e9b7a38766bb66c29da2208525d1e11Paul Westbrook    private static final int SNIPPET_CS     = 5;
45b0219eb7b77212902a38821c050922ac73a68250Mindy Pereira    private static final int READ           = 6;
467517e3b61b898a57f19be0671f70d58a82224643Andy Huang    private static final int ERROR          = 7;
47f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira    private static final int HAS_ATTACHMENT = 8;
48866d319dd23ec8b7b7d5476c65f7f83469d55d2dMindy Pereira
49f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira
508db7e407109532557718c6b8064792f2df7a073dMindy Pereira    private final Context mContext;
51f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira
5269b440141f402669fe44dfd8924a94fd22ebccf7mindyp    // The thread ID of this conversation.  Can be zero in the case of a
536126d72ae2769bd39451872f45781cadb5b90515Mark Wei    // new conversation where the recipient set is changing as the user
54ff9aff3c28c4c20c7866ebfaaa642997e324f274Mindy Pereira    // types and we have not hit the database yet to create a thread.
55489dd22c64c718b6953b4bd6acef925e82c53c87Andy Huang    private long mThreadId;
56b334c9035e9b7a38766bb66c29da2208525d1e11Paul Westbrook
5752a61db87c487fe5bb7cc673037887a6d35b0f0fMark Wei    private ContactList mRecipients;    // The current set of recipients.
58866d319dd23ec8b7b7d5476c65f7f83469d55d2dMindy Pereira    private long mDate;                 // The last update time.
596c72a787b58a0bc3afcb71093eddf8c29d1cf5edMindy Pereira    private int mMessageCount;          // Number of messages.
606126d72ae2769bd39451872f45781cadb5b90515Mark Wei    private String mSnippet;            // Text of the most recent message.
6106642fab1bf4ab95b5dd97a65b262845cf60c865Mindy Pereira    private boolean mHasUnreadMessages; // True if there are unread messages.
629365a826b46b0e274df88e92534f7d871eef2aa2mindyp    private boolean mHasAttachment;     // True if any message has an attachment.
6369b440141f402669fe44dfd8924a94fd22ebccf7mindyp    private boolean mHasError;          // True if any message is in an error state.
64866d319dd23ec8b7b7d5476c65f7f83469d55d2dMindy Pereira
65e6f4ddc19c205796cde91d58483fa39c3cc13f1aAlice Yang    private static ContentValues mReadContentValues;
66e6f4ddc19c205796cde91d58483fa39c3cc13f1aAlice Yang
674485ade4b8c949a222f8b98650a9a48d074dc87eVikram Aggarwal
684485ade4b8c949a222f8b98650a9a48d074dc87eVikram Aggarwal    private Conversation(Context context) {
69f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira        mContext = context;
70f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira        mRecipients = new ContactList();
71f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira        mThreadId = 0;
72f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira    }
73f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira
74f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira    private Conversation(Context context, long threadId) {
75f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira        mContext = context;
76f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira        if (!loadFromThreadId(threadId)) {
77f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira            mRecipients = new ContactList();
78f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira            mThreadId = 0;
792229eddfba6f7517c43ef49ee67c50c24d4b961aMark Wei        }
80f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira    }
81f913894d3f1fa80b7174477999074b813e1deb12Mindy Pereira
826f0b2be9d3855e93ec2977b2736798c60adb9b7eMindy Pereira    private Conversation(Context context, Cursor cursor, boolean allowQuery) {
836f0b2be9d3855e93ec2977b2736798c60adb9b7eMindy Pereira        mContext = context;
84f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira        fillFromCursor(context, this, cursor, allowQuery);
85f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira    }
86f913894d3f1fa80b7174477999074b813e1deb12Mindy Pereira
87f913894d3f1fa80b7174477999074b813e1deb12Mindy Pereira    /**
88f913894d3f1fa80b7174477999074b813e1deb12Mindy Pereira     * Create a new conversation with no recipients.  {@link setRecipients} can
89f913894d3f1fa80b7174477999074b813e1deb12Mindy Pereira     * be called as many times as you like; the conversation will not be
90f913894d3f1fa80b7174477999074b813e1deb12Mindy Pereira     * created in the database until {@link ensureThreadId} is called.
91f913894d3f1fa80b7174477999074b813e1deb12Mindy Pereira     */
92f913894d3f1fa80b7174477999074b813e1deb12Mindy Pereira    public static Conversation createNew(Context context) {
93f913894d3f1fa80b7174477999074b813e1deb12Mindy Pereira        return new Conversation(context);
94f913894d3f1fa80b7174477999074b813e1deb12Mindy Pereira    }
957517e3b61b898a57f19be0671f70d58a82224643Andy Huang
967517e3b61b898a57f19be0671f70d58a82224643Andy Huang    /**
97bc88f16af48d9538b79cbeab88cd275cb769a67dVikram Aggarwal     * Find the conversation matching the provided thread ID.
987517e3b61b898a57f19be0671f70d58a82224643Andy Huang     */
997517e3b61b898a57f19be0671f70d58a82224643Andy Huang    public static Conversation get(Context context, long threadId) {
1007517e3b61b898a57f19be0671f70d58a82224643Andy Huang        synchronized (Cache.getInstance()) {
101bc88f16af48d9538b79cbeab88cd275cb769a67dVikram Aggarwal            Conversation conv = Cache.get(threadId);
1027517e3b61b898a57f19be0671f70d58a82224643Andy Huang            if (conv != null)
1037517e3b61b898a57f19be0671f70d58a82224643Andy Huang                return conv;
1047517e3b61b898a57f19be0671f70d58a82224643Andy Huang
1057517e3b61b898a57f19be0671f70d58a82224643Andy Huang            conv = new Conversation(context, threadId);
106ff9aff3c28c4c20c7866ebfaaa642997e324f274Mindy Pereira            try {
107ff9aff3c28c4c20c7866ebfaaa642997e324f274Mindy Pereira                Cache.put(conv);
108ff9aff3c28c4c20c7866ebfaaa642997e324f274Mindy Pereira            } catch (IllegalStateException e) {
109ff9aff3c28c4c20c7866ebfaaa642997e324f274Mindy Pereira                Log.e(TAG, "Tried to add duplicate Conversation to Cache");
110ff9aff3c28c4c20c7866ebfaaa642997e324f274Mindy Pereira            }
111ff9aff3c28c4c20c7866ebfaaa642997e324f274Mindy Pereira            return conv;
112ff9aff3c28c4c20c7866ebfaaa642997e324f274Mindy Pereira        }
1136c72a787b58a0bc3afcb71093eddf8c29d1cf5edMindy Pereira    }
1146c72a787b58a0bc3afcb71093eddf8c29d1cf5edMindy Pereira
115f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira    /**
116f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira     * Find the conversation matching the provided recipient set.
1177c411aafd9ef059f881b7532426e61d1def54a07Andrew Sapperstein     * When called with an empty recipient list, equivalent to {@link createEmpty}.
1189365a826b46b0e274df88e92534f7d871eef2aa2mindyp     */
1199365a826b46b0e274df88e92534f7d871eef2aa2mindyp    public static Conversation get(Context context, ContactList recipients) {
1209365a826b46b0e274df88e92534f7d871eef2aa2mindyp        // If there are no recipients in the list, make a new conversation.
121de3e74a82043733243c7391d7f983a5af8842891Mindy Pereira        if (recipients.size() < 1) {
122de3e74a82043733243c7391d7f983a5af8842891Mindy Pereira            return createNew(context);
123de3e74a82043733243c7391d7f983a5af8842891Mindy Pereira        }
124de3e74a82043733243c7391d7f983a5af8842891Mindy Pereira
125866d319dd23ec8b7b7d5476c65f7f83469d55d2dMindy Pereira        synchronized (Cache.getInstance()) {
126866d319dd23ec8b7b7d5476c65f7f83469d55d2dMindy Pereira            Conversation conv = Cache.get(recipients);
127866d319dd23ec8b7b7d5476c65f7f83469d55d2dMindy Pereira            if (conv != null)
128866d319dd23ec8b7b7d5476c65f7f83469d55d2dMindy Pereira                return conv;
1296126d72ae2769bd39451872f45781cadb5b90515Mark Wei
1306126d72ae2769bd39451872f45781cadb5b90515Mark Wei            long threadId = getOrCreateThreadId(context, recipients);
1316126d72ae2769bd39451872f45781cadb5b90515Mark Wei            conv = new Conversation(context, threadId);
1326126d72ae2769bd39451872f45781cadb5b90515Mark Wei
13306642fab1bf4ab95b5dd97a65b262845cf60c865Mindy Pereira            Cache.put(conv);
13406642fab1bf4ab95b5dd97a65b262845cf60c865Mindy Pereira            return conv;
13506642fab1bf4ab95b5dd97a65b262845cf60c865Mindy Pereira        }
13606642fab1bf4ab95b5dd97a65b262845cf60c865Mindy Pereira    }
137866d319dd23ec8b7b7d5476c65f7f83469d55d2dMindy Pereira
138866d319dd23ec8b7b7d5476c65f7f83469d55d2dMindy Pereira    /**
139866d319dd23ec8b7b7d5476c65f7f83469d55d2dMindy Pereira     * Find the conversation matching in the specified Uri.  Example
140866d319dd23ec8b7b7d5476c65f7f83469d55d2dMindy Pereira     * forms: {@value content://mms-sms/conversations/3} or
141866d319dd23ec8b7b7d5476c65f7f83469d55d2dMindy Pereira     * {@value sms:+12124797990}.
142f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira     * When called with a null Uri, equivalent to {@link createEmpty}.
143f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira     */
1443432b636fbcf7d6cc551b9705ed4dd88ae2b4f49Jin Cao    public static Conversation get(Context context, Uri uri) {
14519bf5f5a4944f5bb4e62e1f32c47dce87f662cbdMindy Pereira        if (uri == null) {
14669b440141f402669fe44dfd8924a94fd22ebccf7mindyp            return createNew(context);
14769b440141f402669fe44dfd8924a94fd22ebccf7mindyp        }
14819bf5f5a4944f5bb4e62e1f32c47dce87f662cbdMindy Pereira
149f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira        if (DEBUG) {
150f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira            Log.v(TAG, "Conversation get URI: " + uri);
151f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira        }
152f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira        // Handle a conversation URI
1533432b636fbcf7d6cc551b9705ed4dd88ae2b4f49Jin Cao        if (uri.getPathSegments().size() >= 2) {
154f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira            try {
155f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira                long threadId = Long.parseLong(uri.getPathSegments().get(1));
156f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira                if (DEBUG) {
157f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira                    Log.v(TAG, "Conversation get threadId: " + threadId);
158f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira                }
159f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira                return get(context, threadId);
160f69d0394f7de5c0681b8d5d5ca0c60b55c55f815Vikram Aggarwal            } catch (NumberFormatException exception) {
161f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira                Log.e(TAG, "Invalid URI: " + uri);
162f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira            }
163f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira        }
164f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira
165f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira        String recipient = uri.getSchemeSpecificPart();
166f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira        return get(context, ContactList.getByNumbers(recipient, false));
167f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira    }
168866d319dd23ec8b7b7d5476c65f7f83469d55d2dMindy Pereira
1691ef988f0c8be136fda75ed207c222413db1d3f0cMindy Pereira    /**
1701ef988f0c8be136fda75ed207c222413db1d3f0cMindy Pereira     * Returns a temporary Conversation (not representing one on disk) wrapping
1711ef988f0c8be136fda75ed207c222413db1d3f0cMindy Pereira     * the contents of the provided cursor.  The cursor should be the one
172866d319dd23ec8b7b7d5476c65f7f83469d55d2dMindy Pereira     * returned to your AsyncQueryHandler passed in to {@link startQueryForAll}.
173866d319dd23ec8b7b7d5476c65f7f83469d55d2dMindy Pereira     * The recipient list of this conversation can be empty if the results
174f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira     * were not in cache.
175f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira     */
176f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira    public static Conversation from(Context context, Cursor cursor) {
177f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira        return new Conversation(context, cursor, false);
178f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira    }
1796c72a787b58a0bc3afcb71093eddf8c29d1cf5edMindy Pereira
1803432b636fbcf7d6cc551b9705ed4dd88ae2b4f49Jin Cao    private void buildReadContentValues() {
181f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira        if (mReadContentValues == null) {
182f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira            mReadContentValues = new ContentValues(1);
183f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira            mReadContentValues.put("read", 1);
1846c72a787b58a0bc3afcb71093eddf8c29d1cf5edMindy Pereira        }
1850760bfbf8259e4161ce7737fc980b6b9297885ccmindyp    }
1860760bfbf8259e4161ce7737fc980b6b9297885ccmindyp
1870760bfbf8259e4161ce7737fc980b6b9297885ccmindyp    /**
188f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira     * Marks all messages in this conversation as read and updates
189f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira     * relevant notifications.  This method returns immediately;
1908937bf1552a86853efc798a4d8df34c01115cdfdMindy Pereira     * work is dispatched to a background thread.
1918937bf1552a86853efc798a4d8df34c01115cdfdMindy Pereira     */
192c6adce3cf6887c4c6dd5005724565702751843d0mindyp    public synchronized void markAsRead() {
1930fb551891757ecddccb299d9df770bb1675e3fd2Mindy Pereira        // If we have no Uri to mark (as in the case of a conversation that
1948937bf1552a86853efc798a4d8df34c01115cdfdMindy Pereira        // has not yet made its way to disk), there's nothing to do.
195c6adce3cf6887c4c6dd5005724565702751843d0mindyp        final Uri threadUri = getUri();
1968937bf1552a86853efc798a4d8df34c01115cdfdMindy Pereira
1978937bf1552a86853efc798a4d8df34c01115cdfdMindy Pereira        new Thread(new Runnable() {
1988937bf1552a86853efc798a4d8df34c01115cdfdMindy Pereira            public void run() {
199a538984fcc19e7624f2650b119ede39bf1f35846mindyp                if (threadUri != null) {
2007c411aafd9ef059f881b7532426e61d1def54a07Andrew Sapperstein                    buildReadContentValues();
2017c411aafd9ef059f881b7532426e61d1def54a07Andrew Sapperstein                    mContext.getContentResolver().update(threadUri, mReadContentValues,
2027c411aafd9ef059f881b7532426e61d1def54a07Andrew Sapperstein                            "read=0", null);
2037c411aafd9ef059f881b7532426e61d1def54a07Andrew Sapperstein                    mHasUnreadMessages = false;
2047c411aafd9ef059f881b7532426e61d1def54a07Andrew Sapperstein                }
205d3367499e56116854ed637b1036fd71057ac0f49Mindy Pereira                // Always update notifications regardless of the read state.
206866d319dd23ec8b7b7d5476c65f7f83469d55d2dMindy Pereira                MessagingNotification.updateAllNotifications(mContext);
2076a3d5ce0b18f58fcfa1af0315dc8ddc7331c2c5fScott Kennedy            }
2086a3d5ce0b18f58fcfa1af0315dc8ddc7331c2c5fScott Kennedy        }).start();
2096c72a787b58a0bc3afcb71093eddf8c29d1cf5edMindy Pereira    }
21013e6243683cc8cb998a43d06f25d6ff1aab9d93bMindy Pereira
2110fb551891757ecddccb299d9df770bb1675e3fd2Mindy Pereira    /**
21209f1ae9a9f209436c0c6b44a6e76b32b27937a01Mindy Pereira     * Returns a content:// URI referring to this conversation,
21309f1ae9a9f209436c0c6b44a6e76b32b27937a01Mindy Pereira     * or null if it does not exist on disk yet.
21409f1ae9a9f209436c0c6b44a6e76b32b27937a01Mindy Pereira     */
215370f868c834861e7732faaa9bdd07a0fa0105596Andy Huang    public synchronized Uri getUri() {
21600ffece08e94ff5774b2a53c0adeb2f3d0815d66Mindy Pereira        if (mThreadId <= 0)
2179365a826b46b0e274df88e92534f7d871eef2aa2mindyp            return null;
2185cc0ab20009100e3ef259fe3d2e3ddc357f79285mindyp
2195cc0ab20009100e3ef259fe3d2e3ddc357f79285mindyp        return ContentUris.withAppendedId(Threads.CONTENT_URI, mThreadId);
220042a530b2296487fa5899a3e871214ac4a47e3d8Andy Huang    }
2212b55549d4fbe25e91f673a0727fc89ff755d9327Andy Huang
222042a530b2296487fa5899a3e871214ac4a47e3d8Andy Huang    /**
2232b9d80eb16156173f11a02eae4f770d8c975927cScott Kennedy     * Return the Uri for all messages in the given thread ID.
2242b9d80eb16156173f11a02eae4f770d8c975927cScott Kennedy     * @deprecated
2252b9d80eb16156173f11a02eae4f770d8c975927cScott Kennedy     */
2262b9d80eb16156173f11a02eae4f770d8c975927cScott Kennedy    public static Uri getUri(long threadId) {
2272b9d80eb16156173f11a02eae4f770d8c975927cScott Kennedy        // TODO: Callers using this should really just have a Conversation
2282b9d80eb16156173f11a02eae4f770d8c975927cScott Kennedy        // and call getUri() on it, but this guarantees no blocking.
2292b9d80eb16156173f11a02eae4f770d8c975927cScott Kennedy        return ContentUris.withAppendedId(Threads.CONTENT_URI, threadId);
2302b9d80eb16156173f11a02eae4f770d8c975927cScott Kennedy    }
2312b9d80eb16156173f11a02eae4f770d8c975927cScott Kennedy
2322b9d80eb16156173f11a02eae4f770d8c975927cScott Kennedy    /**
2332b9d80eb16156173f11a02eae4f770d8c975927cScott Kennedy     * Returns the thread ID of this conversation.  Can be zero if
2342b9d80eb16156173f11a02eae4f770d8c975927cScott Kennedy     * {@link ensureThreadId} has not been called yet.
2352b9d80eb16156173f11a02eae4f770d8c975927cScott Kennedy     */
2362b9d80eb16156173f11a02eae4f770d8c975927cScott Kennedy    public synchronized long getThreadId() {
2372b9d80eb16156173f11a02eae4f770d8c975927cScott Kennedy        return mThreadId;
2382b9d80eb16156173f11a02eae4f770d8c975927cScott Kennedy    }
2392b9d80eb16156173f11a02eae4f770d8c975927cScott Kennedy
2402b9d80eb16156173f11a02eae4f770d8c975927cScott Kennedy    /**
2412b9d80eb16156173f11a02eae4f770d8c975927cScott Kennedy     * Guarantees that the conversation has been created in the database.
242512821c11d89d49908f3cfdee0b582601f500f3dJin Cao     * This will make a blocking database call if it hasn't.
243512821c11d89d49908f3cfdee0b582601f500f3dJin Cao     *
2446c72a787b58a0bc3afcb71093eddf8c29d1cf5edMindy Pereira     * @return The thread ID of this conversation in the database
2459365a826b46b0e274df88e92534f7d871eef2aa2mindyp     */
2469365a826b46b0e274df88e92534f7d871eef2aa2mindyp    public synchronized long ensureThreadId() {
2479365a826b46b0e274df88e92534f7d871eef2aa2mindyp        if (DEBUG) {
2486c72a787b58a0bc3afcb71093eddf8c29d1cf5edMindy Pereira            Log.v("TAG", "ensureThreadId before: " + mThreadId);
249a8ead90ce1e6c66e4ecacdf7cfa25c2cafc9bb3bMindy Pereira        }
250a8ead90ce1e6c66e4ecacdf7cfa25c2cafc9bb3bMindy Pereira        if (mThreadId <= 0) {
25120a97ded277fdcd3c63952a23718410c2882103fVikram Aggarwal            mThreadId = getOrCreateThreadId(mContext, mRecipients);
25254f120f6ada40c7191811dfff99b151e9e192c78mindyp        }
25354f120f6ada40c7191811dfff99b151e9e192c78mindyp        if (DEBUG) {
25454f120f6ada40c7191811dfff99b151e9e192c78mindyp            Log.v("TAG", "ensureThreadId after: " + mThreadId);
25554f120f6ada40c7191811dfff99b151e9e192c78mindyp        }
25654f120f6ada40c7191811dfff99b151e9e192c78mindyp
257cff4a2b2e92258c6c85ed4af15bc13101aa71170Mindy Pereira        return mThreadId;
258f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira    }
259f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira
260f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira    public synchronized void clearThreadId() {
261f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira        // remove ourself from the cache
262f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira        Cache.remove(mThreadId);
263f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira
264f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira        mThreadId = 0;
2653b5e82b06a0af3f5ddf3667de9a2aa26eeb5f24bmindyp    }
266e6f4ddc19c205796cde91d58483fa39c3cc13f1aAlice Yang
2677c411aafd9ef059f881b7532426e61d1def54a07Andrew Sapperstein    /**
268e6f4ddc19c205796cde91d58483fa39c3cc13f1aAlice Yang     * Sets the list of recipients associated with this conversation.
269e6f4ddc19c205796cde91d58483fa39c3cc13f1aAlice Yang     * If called, {@link ensureThreadId} must be called before the next
270e6f4ddc19c205796cde91d58483fa39c3cc13f1aAlice Yang     * operation that depends on this conversation existing in the
271f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira     * database (e.g. storing a draft message to it).
272f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira     */
273f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira    public synchronized void setRecipients(ContactList list) {
2746c72a787b58a0bc3afcb71093eddf8c29d1cf5edMindy Pereira        mRecipients = list;
2753b5e82b06a0af3f5ddf3667de9a2aa26eeb5f24bmindyp
2763b5e82b06a0af3f5ddf3667de9a2aa26eeb5f24bmindyp        // Invalidate thread ID because the recipient set has changed.
2773b5e82b06a0af3f5ddf3667de9a2aa26eeb5f24bmindyp        mThreadId = 0;
2784d4531a63cff536d2ee1a2929d0820981df8516amindyp    }
2793b5e82b06a0af3f5ddf3667de9a2aa26eeb5f24bmindyp
2807c411aafd9ef059f881b7532426e61d1def54a07Andrew Sapperstein    /**
2817c411aafd9ef059f881b7532426e61d1def54a07Andrew Sapperstein     * Returns the recipient set of this conversation.
2827c411aafd9ef059f881b7532426e61d1def54a07Andrew Sapperstein     */
2837c411aafd9ef059f881b7532426e61d1def54a07Andrew Sapperstein    public synchronized ContactList getRecipients() {
2847c411aafd9ef059f881b7532426e61d1def54a07Andrew Sapperstein        return mRecipients;
285f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira    }
286f6a6b50c6b45f36b9317e1688f8b87310a67b037Mindy Pereira
28707118a01f7183645957010779222b84930f75b4eMindy Pereira    /**
28807118a01f7183645957010779222b84930f75b4eMindy Pereira     * Returns true if a draft message exists in this conversation.
28907118a01f7183645957010779222b84930f75b4eMindy Pereira     */
290cec3e0b9c90173e0bdb2d072046b38714fc9186ePaul Westbrook    public synchronized boolean hasDraft() {
29184f7d32bdc79263004ed5241480988e02f8e618cmindyp        if (mThreadId <= 0)
29284f7d32bdc79263004ed5241480988e02f8e618cmindyp            return false;
293cec3e0b9c90173e0bdb2d072046b38714fc9186ePaul Westbrook
294cec3e0b9c90173e0bdb2d072046b38714fc9186ePaul Westbrook        return DraftCache.getInstance().hasDraft(mThreadId);
29584f7d32bdc79263004ed5241480988e02f8e618cmindyp    }
296a8e4318bb9921e2ec6045c5f7187a4b78c55fe10Vikram Aggarwal
297a8e4318bb9921e2ec6045c5f7187a4b78c55fe10Vikram Aggarwal    /**
298a8e4318bb9921e2ec6045c5f7187a4b78c55fe10Vikram Aggarwal     * Sets whether or not this conversation has a draft message.
299cec3e0b9c90173e0bdb2d072046b38714fc9186ePaul Westbrook     */
300067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira    public synchronized void setDraftState(boolean hasDraft) {
30184f7d32bdc79263004ed5241480988e02f8e618cmindyp        if (mThreadId <= 0)
302cec3e0b9c90173e0bdb2d072046b38714fc9186ePaul Westbrook            return;
303067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira
304067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira        DraftCache.getInstance().setDraftState(mThreadId, hasDraft);
305cc2f9296ad63ed681c200feb118c5caace36e72emindyp    }
306dbb587f15c723ae80edb33f65c29cc2b6c15eba0Scott Kennedy
307067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira    /**
308067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira     * Returns the time of the last update to this conversation in milliseconds,
309dbb587f15c723ae80edb33f65c29cc2b6c15eba0Scott Kennedy     * on the {@link System.currentTimeMillis} timebase.
310067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira     */
311067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira    public synchronized long getDate() {
312599e7f8bf95d2f21a966cbff1bf72adf77a90a33Andy Huang        return mDate;
313067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira    }
314067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira
315067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira    /**
316067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira     * Returns the number of messages in this conversation, excluding the draft
317067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira     * (if it exists).
318067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira     */
319067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira    public synchronized int getMessageCount() {
320067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira        return mMessageCount;
321067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira    }
322067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira
323067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira    /**
324067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira     * Returns a snippet of text from the most recent message in the conversation.
325599e7f8bf95d2f21a966cbff1bf72adf77a90a33Andy Huang     */
326067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira    public synchronized String getSnippet() {
327067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira        return mSnippet;
328067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira    }
329067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira
330c79aec78d06928b3fa1464d6aed60019f9b4843bMindy Pereira    /**
331067ef97264240a2c3f9172a9ab88ba43fef7c475Mindy Pereira     * Returns true if there are any unread messages in the conversation.
33207118a01f7183645957010779222b84930f75b4eMindy Pereira     */
33307118a01f7183645957010779222b84930f75b4eMindy Pereira    public synchronized boolean hasUnreadMessages() {
3340fb551891757ecddccb299d9df770bb1675e3fd2Mindy Pereira        return mHasUnreadMessages;
3350fb551891757ecddccb299d9df770bb1675e3fd2Mindy Pereira    }
3360fb551891757ecddccb299d9df770bb1675e3fd2Mindy Pereira
3370fb551891757ecddccb299d9df770bb1675e3fd2Mindy Pereira    /**
3388937bf1552a86853efc798a4d8df34c01115cdfdMindy Pereira     * Returns true if any messages in the conversation have attachments.
3398937bf1552a86853efc798a4d8df34c01115cdfdMindy Pereira     */
340f69d0394f7de5c0681b8d5d5ca0c60b55c55f815Vikram Aggarwal    public synchronized boolean hasAttachment() {
341f69d0394f7de5c0681b8d5d5ca0c60b55c55f815Vikram Aggarwal        return mHasAttachment;
3426126d72ae2769bd39451872f45781cadb5b90515Mark Wei    }
3436126d72ae2769bd39451872f45781cadb5b90515Mark Wei
344f69d0394f7de5c0681b8d5d5ca0c60b55c55f815Vikram Aggarwal    /**
3456126d72ae2769bd39451872f45781cadb5b90515Mark Wei     * Returns true if any messages in the conversation are in an error state.
3461fea6a3cffcc8c4afc3d877e5dc57d29f9665942Scott Kennedy     */
3476126d72ae2769bd39451872f45781cadb5b90515Mark Wei    public synchronized boolean hasError() {
3481fea6a3cffcc8c4afc3d877e5dc57d29f9665942Scott Kennedy        return mHasError;
3496126d72ae2769bd39451872f45781cadb5b90515Mark Wei    }
3506126d72ae2769bd39451872f45781cadb5b90515Mark Wei
35100ffece08e94ff5774b2a53c0adeb2f3d0815d66Mindy Pereira    private static long getOrCreateThreadId(Context context, ContactList list) {
35200ffece08e94ff5774b2a53c0adeb2f3d0815d66Mindy Pereira        HashSet<String> recipients = new HashSet<String>();
353c6adce3cf6887c4c6dd5005724565702751843d0mindyp        Contact cacheContact = null;
354dd9d3a16148b178cb95fd4db4168a67c64eb944fMindy Pereira        for (Contact c : list) {
3558937bf1552a86853efc798a4d8df34c01115cdfdMindy Pereira            cacheContact = Contact.get(c.getNumber(),true);
3565254486a8c37cdf2b579aa7a3b9c8eb7324062bcmindyp            if (cacheContact != null) {
3575254486a8c37cdf2b579aa7a3b9c8eb7324062bcmindyp                recipients.add(cacheContact.getNumber());
358d064972c4a032f3c5cfcb236aa470273f7ac2ce0mindyp            } else {
359d064972c4a032f3c5cfcb236aa470273f7ac2ce0mindyp                recipients.add(c.getNumber());
3605254486a8c37cdf2b579aa7a3b9c8eb7324062bcmindyp            }
3619365a826b46b0e274df88e92534f7d871eef2aa2mindyp        }
3629365a826b46b0e274df88e92534f7d871eef2aa2mindyp        return Threads.getOrCreateThreadId(context, recipients);
3639365a826b46b0e274df88e92534f7d871eef2aa2mindyp    }
3649365a826b46b0e274df88e92534f7d871eef2aa2mindyp
3659365a826b46b0e274df88e92534f7d871eef2aa2mindyp    /*
3669365a826b46b0e274df88e92534f7d871eef2aa2mindyp     * The primary key of a conversation is its recipient set; override
3679365a826b46b0e274df88e92534f7d871eef2aa2mindyp     * equals() and hashCode() to just pass through to the internal
3689365a826b46b0e274df88e92534f7d871eef2aa2mindyp     * recipient sets.
36969b440141f402669fe44dfd8924a94fd22ebccf7mindyp     */
37069b440141f402669fe44dfd8924a94fd22ebccf7mindyp    @Override
37179c3e1ed9cb1e1660c5d34b7bf3f3ab5c5dd5162Andy Huang    public synchronized boolean equals(Object obj) {
372479505d71969e26b0785d8e0e1b81108731cf827Mark Wei        try {
37369b440141f402669fe44dfd8924a94fd22ebccf7mindyp            Conversation other = (Conversation)obj;
37469b440141f402669fe44dfd8924a94fd22ebccf7mindyp            return (mRecipients.equals(other.mRecipients));
37569b440141f402669fe44dfd8924a94fd22ebccf7mindyp        } catch (ClassCastException e) {
37681aea35d45b3d0191ec595562a2fcf67009845d5Mark Wei            return false;
377479505d71969e26b0785d8e0e1b81108731cf827Mark Wei        }
378479505d71969e26b0785d8e0e1b81108731cf827Mark Wei    }
37948ccbc53ef90bf6420f831f63e6243008e02a346Andy Huang
38048ccbc53ef90bf6420f831f63e6243008e02a346Andy Huang    @Override
38148ccbc53ef90bf6420f831f63e6243008e02a346Andy Huang    public synchronized int hashCode() {
38248ccbc53ef90bf6420f831f63e6243008e02a346Andy Huang        return mRecipients.hashCode();
38348ccbc53ef90bf6420f831f63e6243008e02a346Andy Huang    }
38448ccbc53ef90bf6420f831f63e6243008e02a346Andy Huang
38548ccbc53ef90bf6420f831f63e6243008e02a346Andy Huang    @Override
38648ccbc53ef90bf6420f831f63e6243008e02a346Andy Huang    public synchronized String toString() {
38748ccbc53ef90bf6420f831f63e6243008e02a346Andy Huang        return String.format("[%s] (tid %d)", mRecipients.serialize(), mThreadId);
38848ccbc53ef90bf6420f831f63e6243008e02a346Andy Huang    }
38948ccbc53ef90bf6420f831f63e6243008e02a346Andy Huang
39048ccbc53ef90bf6420f831f63e6243008e02a346Andy Huang    /**
39148ccbc53ef90bf6420f831f63e6243008e02a346Andy Huang     * Remove any obsolete conversations sitting around on disk.
39269b440141f402669fe44dfd8924a94fd22ebccf7mindyp     * @deprecated
3933b5e82b06a0af3f5ddf3667de9a2aa26eeb5f24bmindyp     */
3943b5e82b06a0af3f5ddf3667de9a2aa26eeb5f24bmindyp    public static void cleanup(Context context) {
3953b5e82b06a0af3f5ddf3667de9a2aa26eeb5f24bmindyp        // TODO: Get rid of this awful hack.
3963b5e82b06a0af3f5ddf3667de9a2aa26eeb5f24bmindyp        context.getContentResolver().delete(Threads.OBSOLETE_THREADS_URI, null, null);
3973b5e82b06a0af3f5ddf3667de9a2aa26eeb5f24bmindyp    }
3983b5e82b06a0af3f5ddf3667de9a2aa26eeb5f24bmindyp
3993b5e82b06a0af3f5ddf3667de9a2aa26eeb5f24bmindyp    /**
4003b5e82b06a0af3f5ddf3667de9a2aa26eeb5f24bmindyp     * Start a query for all conversations in the database on the specified
4017af43c93d4ca90bb0a43ec790a434c95128cc99fmindyp     * AsyncQueryHandler.
4027af43c93d4ca90bb0a43ec790a434c95128cc99fmindyp     *
4037af43c93d4ca90bb0a43ec790a434c95128cc99fmindyp     * @param handler An AsyncQueryHandler that will receive onQueryComplete
4047af43c93d4ca90bb0a43ec790a434c95128cc99fmindyp     *                upon completion of the query
4057af43c93d4ca90bb0a43ec790a434c95128cc99fmindyp     * @param token   The token that will be passed to onQueryComplete
4067af43c93d4ca90bb0a43ec790a434c95128cc99fmindyp     */
4077af43c93d4ca90bb0a43ec790a434c95128cc99fmindyp    public static void startQueryForAll(AsyncQueryHandler handler, int token) {
4087af43c93d4ca90bb0a43ec790a434c95128cc99fmindyp        handler.cancelOperation(token);
4097af43c93d4ca90bb0a43ec790a434c95128cc99fmindyp        handler.startQuery(token, null, sAllThreadsUri,
410e6f4ddc19c205796cde91d58483fa39c3cc13f1aAlice Yang                ALL_THREADS_PROJECTION, null, null, Conversations.DEFAULT_SORT_ORDER);
411e6f4ddc19c205796cde91d58483fa39c3cc13f1aAlice Yang    }
412e6f4ddc19c205796cde91d58483fa39c3cc13f1aAlice Yang
413e6f4ddc19c205796cde91d58483fa39c3cc13f1aAlice Yang    /**
414e6f4ddc19c205796cde91d58483fa39c3cc13f1aAlice Yang     * Start a delete of the conversation with the specified thread ID.
415e6f4ddc19c205796cde91d58483fa39c3cc13f1aAlice Yang     *
416e6f4ddc19c205796cde91d58483fa39c3cc13f1aAlice Yang     * @param handler An AsyncQueryHandler that will receive onDeleteComplete
4177c411aafd9ef059f881b7532426e61d1def54a07Andrew Sapperstein     *                upon completion of the conversation being deleted
418e6f4ddc19c205796cde91d58483fa39c3cc13f1aAlice Yang     * @param token   The token that will be passed to onDeleteComplete
41900ffece08e94ff5774b2a53c0adeb2f3d0815d66Mindy Pereira     * @param deleteAll Delete the whole thread including locked messages
420     * @param threadId Thread ID of the conversation to be deleted
421     */
422    public static void startDelete(AsyncQueryHandler handler, int token, boolean deleteAll,
423            long threadId) {
424        Uri uri = ContentUris.withAppendedId(Threads.CONTENT_URI, threadId);
425        String selection = deleteAll ? null : "locked=0";
426        handler.startDelete(token, null, uri, selection, null);
427    }
428
429    /**
430     * Start deleting all conversations in the database.
431     * @param handler An AsyncQueryHandler that will receive onDeleteComplete
432     *                upon completion of all conversations being deleted
433     * @param token   The token that will be passed to onDeleteComplete
434     * @param deleteAll Delete the whole thread including locked messages
435     */
436    public static void startDeleteAll(AsyncQueryHandler handler, int token, boolean deleteAll) {
437        String selection = deleteAll ? null : "locked=0";
438        handler.startDelete(token, null, Threads.CONTENT_URI, selection, null);
439    }
440
441    /**
442     * Check for locked messages in all threads or a specified thread.
443     * @param handler An AsyncQueryHandler that will receive onQueryComplete
444     *                upon completion of looking for locked messages
445     * @param threadId   The threadId of the thread to search. -1 means all threads
446     * @param token   The token that will be passed to onQueryComplete
447     */
448    public static void startQueryHaveLockedMessages(AsyncQueryHandler handler, long threadId,
449            int token) {
450        handler.cancelOperation(token);
451        Uri uri = MmsSms.CONTENT_LOCKED_URI;
452        if (threadId != -1) {
453            uri = ContentUris.withAppendedId(uri, threadId);
454        }
455        handler.startQuery(token, new Long(threadId), uri,
456                ALL_THREADS_PROJECTION, null, null, Conversations.DEFAULT_SORT_ORDER);
457    }
458
459    /**
460     * Fill the specified conversation with the values from the specified
461     * cursor, possibly setting recipients to empty if {@value allowQuery}
462     * is false and the recipient IDs are not in cache.  The cursor should
463     * be one made via {@link startQueryForAll}.
464     */
465    private static void fillFromCursor(Context context, Conversation conv,
466                                       Cursor c, boolean allowQuery) {
467        synchronized (conv) {
468            conv.mThreadId = c.getLong(ID);
469            conv.mDate = c.getLong(DATE);
470            conv.mMessageCount = c.getInt(MESSAGE_COUNT);
471
472            // Replace the snippet with a default value if it's empty.
473            String snippet = MessageUtils.extractEncStrFromCursor(c, SNIPPET, SNIPPET_CS);
474            if (TextUtils.isEmpty(snippet)) {
475                snippet = context.getString(R.string.no_subject_view);
476            }
477            conv.mSnippet = snippet;
478
479            conv.mHasUnreadMessages = (c.getInt(READ) == 0);
480            conv.mHasError = (c.getInt(ERROR) != 0);
481            conv.mHasAttachment = (c.getInt(HAS_ATTACHMENT) != 0);
482
483            String recipientIds = c.getString(RECIPIENT_IDS);
484            conv.mRecipients = ContactList.getByIds(recipientIds, allowQuery);
485        }
486    }
487
488    /**
489     * Private cache for the use of the various forms of Conversation.get.
490     */
491    private static class Cache {
492        private static Cache sInstance = new Cache();
493        static Cache getInstance() { return sInstance; }
494        private final HashSet<Conversation> mCache;
495        private Cache() {
496            mCache = new HashSet<Conversation>(10);
497        }
498
499        /**
500         * Return the conversation with the specified thread ID, or
501         * null if it's not in cache.
502         */
503        static Conversation get(long threadId) {
504            synchronized (sInstance) {
505                if (DEBUG) {
506                    Log.v(TAG, "Conversation get with threadId: " + threadId);
507                }
508                dumpCache();
509                for (Conversation c : sInstance.mCache) {
510                    if (DEBUG) {
511                        Log.v(TAG, "Conversation get() threadId: " + threadId +
512                                " c.getThreadId(): " + c.getThreadId());
513                    }
514                    if (c.getThreadId() == threadId) {
515                        return c;
516                    }
517                }
518            }
519            return null;
520        }
521
522        /**
523         * Return the conversation with the specified recipient
524         * list, or null if it's not in cache.
525         */
526        static Conversation get(ContactList list) {
527            synchronized (sInstance) {
528                if (DEBUG) {
529                    Log.v(TAG, "Conversation get with ContactList: " + list);
530                    dumpCache();
531                }
532                for (Conversation c : sInstance.mCache) {
533                    if (c.getRecipients().equals(list)) {
534                        return c;
535                    }
536                }
537            }
538            return null;
539        }
540
541        /**
542         * Put the specified conversation in the cache.  The caller
543         * should not place an already-existing conversation in the
544         * cache, but rather update it in place.
545         */
546        static void put(Conversation c) {
547            synchronized (sInstance) {
548                // We update cache entries in place so people with long-
549                // held references get updated.
550                if (DEBUG) {
551                    Log.v(TAG, "Conversation c: " + c + " put with threadid: " + c.getThreadId() +
552                            " c.hash: " + c.hashCode());
553                    dumpCache();
554                }
555
556                if (sInstance.mCache.contains(c)) {
557                    throw new IllegalStateException("cache already contains " + c +
558                            " threadId: " + c.mThreadId);
559                }
560                sInstance.mCache.add(c);
561            }
562        }
563
564        static void remove(long threadId) {
565            if (DEBUG) {
566                Log.v(TAG, "remove threadid: " + threadId);
567                dumpCache();
568            }
569            for (Conversation c : sInstance.mCache) {
570                if (c.getThreadId() == threadId) {
571                    sInstance.mCache.remove(c);
572                    return;
573                }
574            }
575        }
576
577        static void dumpCache() {
578            if (DEBUG) {
579                synchronized (sInstance) {
580                    Log.v(TAG, "Conversation dumpCache: ");
581                    for (Conversation c : sInstance.mCache) {
582                        Log.v(TAG, "   c: " + c + " c.getThreadId(): " + c.getThreadId() + " hash: " + c.hashCode());
583                    }
584                }
585            }
586        }
587
588        /**
589         * Remove all conversations from the cache that are not in
590         * the provided set of thread IDs.
591         */
592        static void keepOnly(Set<Long> threads) {
593            synchronized (sInstance) {
594                Iterator<Conversation> iter = sInstance.mCache.iterator();
595                while (iter.hasNext()) {
596                    Conversation c = iter.next();
597                    if (!threads.contains(c.getThreadId())) {
598                        iter.remove();
599                    }
600                }
601            }
602        }
603    }
604
605    /**
606     * Set up the conversation cache.  To be called once at application
607     * startup time.
608     */
609    public static void init(final Context context) {
610        new Thread(new Runnable() {
611            public void run() {
612                cacheAllThreads(context);
613            }
614        }).start();
615    }
616
617    private static void cacheAllThreads(Context context) {
618        synchronized (Cache.getInstance()) {
619            if (DEBUG) {
620                Log.v(TAG, "cacheAllThreads called");
621            }
622            // Keep track of what threads are now on disk so we
623            // can discard anything removed from the cache.
624            HashSet<Long> threadsOnDisk = new HashSet<Long>();
625
626            // Query for all conversations.
627            Cursor c = context.getContentResolver().query(sAllThreadsUri,
628                    ALL_THREADS_PROJECTION, null, null, null);
629            try {
630                while (c.moveToNext()) {
631                    long threadId = c.getLong(ID);
632                    threadsOnDisk.add(threadId);
633
634                    // Try to find this thread ID in the cache.
635                    Conversation conv = Cache.get(threadId);
636
637                    if (conv == null) {
638                        // Make a new Conversation and put it in
639                        // the cache if necessary.
640                        conv = new Conversation(context, c, true);
641                        try {
642                            Cache.put(conv);
643                        } catch (IllegalStateException e) {
644                            Log.e(TAG, "Tried to add duplicate Conversation to Cache");
645                        }
646                    } else {
647                        // Or update in place so people with references
648                        // to conversations get updated too.
649                        fillFromCursor(context, conv, c, true);
650                    }
651                }
652            } finally {
653                c.close();
654            }
655
656            // Purge the cache of threads that no longer exist on disk.
657            Cache.keepOnly(threadsOnDisk);
658        }
659    }
660
661    private boolean loadFromThreadId(long threadId) {
662        Cursor c = mContext.getContentResolver().query(sAllThreadsUri, ALL_THREADS_PROJECTION,
663                "_id=" + Long.toString(threadId), null, null);
664        try {
665            if (c.moveToFirst()) {
666                fillFromCursor(mContext, this, c, true);
667            } else {
668                Log.e(TAG, "loadFromThreadId: Can't find thread ID " + threadId);
669                return false;
670            }
671        } finally {
672            c.close();
673        }
674        return true;
675    }
676}
677