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