package com.android.mms.data; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import android.app.ProgressDialog; import android.content.AsyncQueryHandler; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.database.sqlite.SqliteWrapper; import android.net.Uri; import android.provider.Telephony.Mms; import android.provider.Telephony.MmsSms; import android.provider.Telephony.Sms; import android.provider.Telephony.Threads; import android.provider.Telephony.Sms.Conversations; import android.text.TextUtils; import android.util.Log; import com.android.mms.LogTag; import com.android.mms.MmsApp; import com.android.mms.R; import com.android.mms.transaction.MessagingNotification; import com.android.mms.ui.MessageUtils; import com.android.mms.util.AddressUtils; import com.android.mms.util.DraftCache; import com.google.android.mms.pdu.PduHeaders; import android.content.SharedPreferences;//by lai import android.preference.PreferenceManager;//by lai /** * An interface for finding information about conversations and/or creating new ones. */ public class Conversation { private static final String TAG = "Mms/conv"; private static final boolean DEBUG = false; public static final Uri sAllThreadsUri = Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build();//??private public static final String[] ALL_THREADS_PROJECTION = { Threads._ID, Threads.DATE, Threads.MESSAGE_COUNT, Threads.RECIPIENT_IDS, Threads.SNIPPET, Threads.SNIPPET_CHARSET, Threads.READ, Threads.ERROR, Threads.HAS_ATTACHMENT };//??private private static final String[] UNREAD_PROJECTION = { Threads._ID, Threads.READ }; private static final String UNREAD_SELECTION = "(read=0 OR seen=0)"; private static final String[] SEEN_PROJECTION = new String[] { "seen" }; public static final int ID = 0;//??private private static final int DATE = 1; private static final int MESSAGE_COUNT = 2; public static final int RECIPIENT_IDS = 3;//??private private static final int SNIPPET = 4; private static final int SNIPPET_CS = 5; private static final int READ = 6; private static final int ERROR = 7; private static final int HAS_ATTACHMENT = 8; public static boolean MmsCheckPhoneNum=false; //by lai private final Context mContext; // The thread ID of this conversation. Can be zero in the case of a // new conversation where the recipient set is changing as the user // types and we have not hit the database yet to create a thread. private long mThreadId; private ContactList mRecipients; // The current set of recipients. private long mDate; // The last update time. private int mMessageCount; // Number of messages. private String mSnippet; // Text of the most recent message. private boolean mHasUnreadMessages; // True if there are unread messages. private boolean mHasAttachment; // True if any message has an attachment. private boolean mHasError; // True if any message is in an error state. private long mRECIPIENT_IDS; private static ContentValues mReadContentValues; private static boolean mLoadingThreads; private boolean mMarkAsReadBlocked; private Object mMarkAsBlockedSyncer = new Object(); // fixed bug for 19102 19105 start private static ProgressDialog initConcatCacheDialog; private static ExecutorService mExecutorContact; // fixed bug for 19102 19105 end // fixed bug for 19309 start private static ExecutorService mExecutorMakeAsRead; // fixed bug for 19309 end private Conversation(Context context) { mContext = context; mRecipients = new ContactList(); mThreadId = 0; } private Conversation(Context context, long threadId, boolean allowQuery) { mContext = context; if (!loadFromThreadId(threadId, allowQuery)) { mRecipients = new ContactList(); mThreadId = 0; } } private Conversation(Context context, Cursor cursor, boolean allowQuery) { mContext = context; fillFromCursor(context, this, cursor, allowQuery); } /** * Create a new conversation with no recipients. {@link #setRecipients} can * be called as many times as you like; the conversation will not be * created in the database until {@link #ensureThreadId} is called. */ public static Conversation createNew(Context context) { return new Conversation(context); } /** * Find the conversation matching the provided thread ID. */ public static Conversation get(Context context, long threadId, boolean allowQuery) { Conversation conv = Cache.get(threadId); if (conv != null) return conv; conv = new Conversation(context, threadId, allowQuery); try { Cache.put(conv); } catch (IllegalStateException e) { LogTag.error("Tried to add duplicate Conversation to Cache"); } return conv; } /** * Find the conversation matching the provided recipient set. * When called with an empty recipient list, equivalent to {@link #createNew}. */ public static Conversation get(Context context, ContactList recipients, boolean allowQuery) { // If there are no recipients in the list, make a new conversation. if (recipients.size() < 1) { return createNew(context); } Conversation conv = Cache.get(recipients); if (conv != null) return conv; long threadId = getOrCreateThreadId(context, recipients); conv = new Conversation(context, threadId, allowQuery); Log.d(TAG, "Conversation.get: created new conversation " + /*conv.toString()*/ "xxxxxxx"); if (!conv.getRecipients().equals(recipients)) { Log.e(TAG, "Conversation.get: new conv's recipients don't match input recpients " + /*recipients*/ "xxxxxxx"); } try { Cache.put(conv); } catch (IllegalStateException e) { LogTag.error("Tried to add duplicate Conversation to Cache"); } return conv; } /** * Find the conversation matching in the specified Uri. Example * forms: {@value content://mms-sms/conversations/3} or * {@value sms:+12124797990}. * When called with a null Uri, equivalent to {@link #createNew}. */ public static Conversation get(Context context, Uri uri, boolean allowQuery) { if (uri == null) { return createNew(context); } if (DEBUG) Log.v(TAG, "Conversation get URI: " + uri); // Handle a conversation URI if (uri.getPathSegments().size() >= 2) { try { long threadId = Long.parseLong(uri.getPathSegments().get(1)); if (DEBUG) { Log.v(TAG, "Conversation get threadId: " + threadId); } return get(context, threadId, allowQuery); } catch (NumberFormatException exception) { LogTag.error("Invalid URI: " + uri); } } String recipient = getRecipients(uri); return get(context, ContactList.getByNumbers(recipient, allowQuery /* don't block */, true /* replace number */), allowQuery); } /** * Returns true if the recipient in the uri matches the recipient list in this * conversation. */ public boolean sameRecipient(Uri uri) { int size = mRecipients.size(); if (size > 1) { return false; } if (uri == null) { return size == 0; } if (uri.getPathSegments().size() >= 2) { return false; // it's a thread id for a conversation } String recipient = getRecipients(uri); ContactList incomingRecipient = ContactList.getByNumbers(recipient, false /* don't block */, false /* don't replace number */); return mRecipients.equals(incomingRecipient); } /** * Returns a temporary Conversation (not representing one on disk) wrapping * the contents of the provided cursor. The cursor should be the one * returned to your AsyncQueryHandler passed in to {@link #startQueryForAll}. * The recipient list of this conversation can be empty if the results * were not in cache. */ public static Conversation from(Context context, Cursor cursor) { /*-------------------------by lai----------------------------*/ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); String MmsPhoneNum = preferences.getString("phone_num", ""); /*-------------------------by lai----------------------------*/ // First look in the cache for the Conversation and return that one. That way, all the // people that are looking at the cached copy will get updated when fillFromCursor() is // called with this cursor. long threadId = cursor.getLong(ID); if (threadId > 0) { Conversation conv = Cache.get(threadId); if (conv != null) { /*-------------------------by lai----------------------------*/ String[] PhoneNums = conv.mRecipients.getNumbers(); String PhoneNum = PhoneNums[PhoneNums.length - 1]; if ("" == MmsPhoneNum || null == MmsPhoneNum) { MmsCheckPhoneNum = false; } else if (PhoneNum.equals(MmsPhoneNum) || PhoneNum.equals("+86" + MmsPhoneNum)) { MmsCheckPhoneNum = true; } else { MmsCheckPhoneNum = false; } /*-------------------------by lai----------------------------*/ fillFromCursor(context, conv, cursor, false); // update the existing conv in-place return conv; } } Conversation conv = new Conversation(context, cursor, false); try { Cache.put(conv); } catch (IllegalStateException e) { LogTag.error("Tried to add duplicate Conversation to Cache"); } return conv; } private void buildReadContentValues() { if (mReadContentValues == null) { mReadContentValues = new ContentValues(2); mReadContentValues.put("read", 1); mReadContentValues.put("seen", 1); } } /** * Marks all messages in this conversation as read and updates * relevant notifications. This method returns immediately; * work is dispatched to a background thread. */ public void markAsRead(final String boxmsgFlg, final String boxmsgThreadId, final String boxmsgMsgId) { // If we have no Uri to mark (as in the case of a conversation that // has not yet made its way to disk), there's nothing to do. final Uri threadUri = getUri(); Runnable run = new Runnable() { public void run() { synchronized(mMarkAsBlockedSyncer) { if (mMarkAsReadBlocked) { try { mMarkAsBlockedSyncer.wait(); } catch (InterruptedException e) { } } if (threadUri != null) { buildReadContentValues(); // Check the read flag first. It's much faster to do a query than // to do an update. Timing this function show it's about 10x faster to // do the query compared to the update, even when there's nothing to // update. boolean needUpdate = true; Cursor c = mContext.getContentResolver().query(threadUri, UNREAD_PROJECTION, UNREAD_SELECTION, null, null); if (c != null) { try { needUpdate = c.getCount() > 0; } finally { c.close(); } } if (needUpdate) { //===== fixed CR<NEWMS00127040> by luning at 11-10-07 begin ===== // check if need send mms read report HashMap<String, String> map = getMmsReadReportData(mContext, mThreadId, PduHeaders.READ_STATUS_READ); if(null != map && map.size() > 0){ Intent intent = new Intent("sendMmsReadReport"); intent.putExtra("getMmsReadReportData", map); mContext.sendBroadcast(intent); } //===== fixed CR<NEWMS00127040> by luning at 11-10-07 end ===== LogTag.debug("markAsRead: update read/seen for thread uri: " + threadUri); if ("true".equals(boxmsgFlg)) { mContext.getContentResolver().update(threadUri, mReadContentValues, UNREAD_SELECTION+" AND thread_id="+boxmsgThreadId+" AND _id="+boxmsgMsgId+" ", null); } else { mContext.getContentResolver().update(threadUri, mReadContentValues, UNREAD_SELECTION, null); } } setHasUnreadMessages(false); } } // Always update notifications regardless of the read state. MessagingNotification.blockingUpdateAllNotifications(mContext); } }; addMakeAsReadThread(run); } //===== fixed CR<NEWMS00127040> by luning at 11-10-12 begin ===== public HashMap<String, String> getMmsReadReportData(final Context context, final long threadId, final int status) { String selection = Mms.MESSAGE_TYPE + " = " + PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF + " AND " + Mms.READ + " = 0" + " AND " + Mms.READ_REPORT + " = " + PduHeaders.VALUE_YES; if (threadId != -1) { selection = selection + " AND " + Mms.THREAD_ID + " = " + threadId; } final Cursor c = SqliteWrapper.query(context, context .getContentResolver(), Mms.Inbox.CONTENT_URI, new String[] { Mms._ID, Mms.MESSAGE_ID }, selection, null, null); if (c == null) { return null; } final HashMap<String, String> map = new HashMap<String, String>(); try { if (c.getCount() == 0) { Log.d(TAG, "Converation.getMmsReadReportData:c.getCount() == 0"); return null; } while (c.moveToNext()) { Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, c .getLong(0)); map.put(c.getString(1), AddressUtils.getFrom(context, uri)); } } finally { c.close(); } return map; } //===== fixed CR<NEWMS00127040> by luning at 11-10-12 end ===== public void blockMarkAsRead(boolean block) { if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { LogTag.debug("blockMarkAsRead: " + block); } synchronized(mMarkAsBlockedSyncer) { if (block != mMarkAsReadBlocked) { mMarkAsReadBlocked = block; if (!mMarkAsReadBlocked) { mMarkAsBlockedSyncer.notifyAll(); } } } } /** * Returns a content:// URI referring to this conversation, * or null if it does not exist on disk yet. */ public synchronized Uri getUri() { if (mThreadId <= 0) return null; return ContentUris.withAppendedId(Threads.CONTENT_URI, mThreadId); } /** * Return the Uri for all messages in the given thread ID. * @deprecated */ public static Uri getUri(long threadId) { // TODO: Callers using this should really just have a Conversation // and call getUri() on it, but this guarantees no blocking. return ContentUris.withAppendedId(Threads.CONTENT_URI, threadId); } /** * Returns the thread ID of this conversation. Can be zero if * {@link #ensureThreadId} has not been called yet. */ public synchronized long getThreadId() { return mThreadId; } public static Object ensureThreadIdLock = new Object(); /** * Guarantees that the conversation has been created in the database. * This will make a blocking database call if it hasn't. * * @return The thread ID of this conversation in the database */ public synchronized long ensureThreadId() { if (DEBUG) { LogTag.debug("ensureThreadId before: " + mThreadId); } if (mThreadId <= 0) { mThreadId = getOrCreateThreadId(mContext, mRecipients); } if (DEBUG) { LogTag.debug("ensureThreadId after: " + mThreadId); } return mThreadId; } public synchronized void clearThreadId() { // remove ourself from the cache if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { LogTag.debug("clearThreadId old threadId was: " + mThreadId + " now zero"); } Cache.remove(mThreadId); mThreadId = 0; } /** * Sets the list of recipients associated with this conversation. * If called, {@link #ensureThreadId} must be called before the next * operation that depends on this conversation existing in the * database (e.g. storing a draft message to it). */ public synchronized void setRecipients(ContactList list) { mRecipients = list; // Invalidate thread ID because the recipient set has changed. mThreadId = 0; } /** * Returns the recipient set of this conversation. */ public synchronized ContactList getRecipients() { return mRecipients; } /** * Returns true if a draft message exists in this conversation. */ public synchronized boolean hasDraft() { if (mThreadId <= 0) return false; return DraftCache.getInstance().hasDraft(mThreadId); } /** * Sets whether or not this conversation has a draft message. */ public synchronized void setDraftState(boolean hasDraft) { if (mThreadId <= 0) return; DraftCache.getInstance().setDraftState(mThreadId, hasDraft); } /** * Returns the time of the last update to this conversation in milliseconds, * on the {@link System#currentTimeMillis} timebase. */ public synchronized long getDate() { return mDate; } /** * Returns the number of messages in this conversation, excluding the draft * (if it exists). */ public synchronized int getMessageCount() { return mMessageCount; } public synchronized long getMessageIds() { return mRECIPIENT_IDS; } /** * Returns a snippet of text from the most recent message in the conversation. */ public synchronized String getSnippet() { return mSnippet; } /** * Returns true if there are any unread messages in the conversation. */ public boolean hasUnreadMessages() { synchronized (this) { return mHasUnreadMessages; } } private void setHasUnreadMessages(boolean flag) { synchronized (this) { mHasUnreadMessages = flag; } } /** * Returns true if any messages in the conversation have attachments. */ public synchronized boolean hasAttachment() { return mHasAttachment; } /** * Returns true if any messages in the conversation are in an error state. */ public synchronized boolean hasError() { return mHasError; } private static long getOrCreateThreadId(Context context, ContactList list) { HashSet<String> recipients = new HashSet<String>(); HashSet<String> names = new HashSet<String>(); Contact cacheContact = null; for (Contact c : list) { cacheContact = Contact.get(c.getNumber(), false); if (cacheContact != null) { recipients.add(cacheContact.getNumber()); names.add(cacheContact.getName()); } else { recipients.add(c.getNumber()); names.add(c.getName()); } } long retVal = Threads.getOrCreateThreadId(context, recipients, names); if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { Log.d(TAG,"[Conversation] getOrCreateThreadId for (XXX) returned " + retVal); } return retVal; } /* * The primary key of a conversation is its recipient set; override * equals() and hashCode() to just pass through to the internal * recipient sets. */ @Override public synchronized boolean equals(Object obj) { try { Conversation other = (Conversation)obj; return (mRecipients.equals(other.mRecipients)); } catch (ClassCastException e) { return false; } } @Override public synchronized int hashCode() { return mRecipients.hashCode(); } @Override public synchronized String toString() { return "["+mRecipients.serialize()+"] (tid "+mThreadId+")"; } /** * Remove any obsolete conversations sitting around on disk. Obsolete threads are threads * that aren't referenced by any message in the pdu or sms tables. */ public static void asyncDeleteObsoleteThreads(AsyncQueryHandler handler, int token) { handler.startDelete(token, null, Threads.OBSOLETE_THREADS_URI, null, null); } /** * Start a query for all conversations in the database on the specified * AsyncQueryHandler. * * @param handler An AsyncQueryHandler that will receive onQueryComplete * upon completion of the query * @param token The token that will be passed to onQueryComplete */ public static void startQueryForAll(AsyncQueryHandler handler, int token) { handler.cancelOperation(token); // This query looks like this in the log: // I/Database( 147): elapsedTime4Sql|/data/data/com.android.providers.telephony/databases/ // mmssms.db|2.253 ms|SELECT _id, date, message_count, recipient_ids, snippet, snippet_cs, // read, error, has_attachment FROM threads ORDER BY date DESC handler.startQuery(token, null, sAllThreadsUri, ALL_THREADS_PROJECTION, null, null, Conversations.DEFAULT_SORT_ORDER); } /** * Start a delete of the conversation with the specified thread ID. * * @param handler An AsyncQueryHandler that will receive onDeleteComplete * upon completion of the conversation being deleted * @param token The token that will be passed to onDeleteComplete * @param deleteAll Delete the whole thread including locked messages * @param threadId Thread ID of the conversation to be deleted */ public static void startDelete(AsyncQueryHandler handler, int token, boolean deleteAll, long threadId) { Uri uri = ContentUris.withAppendedId(Threads.CONTENT_URI, threadId); String selection = deleteAll ? null : "locked=0"; handler.startDelete(token, null, uri, selection, null); } public static void startDelete(Context context, boolean deleteAll, long threadId) { Uri uri = ContentUris.withAppendedId(Threads.CONTENT_URI, threadId); String selection = deleteAll ? null : "locked=0"; context.getContentResolver().delete(uri, selection, null); } /** * Start deleting all conversations in the database. * @param handler An AsyncQueryHandler that will receive onDeleteComplete * upon completion of all conversations being deleted * @param token The token that will be passed to onDeleteComplete * @param deleteAll Delete the whole thread including locked messages */ public static void startDeleteAll(AsyncQueryHandler handler, int token, boolean deleteAll) { String selection = deleteAll ? null : "locked=0"; handler.startDelete(token, null, Threads.CONTENT_URI, selection, null); } /** * Check for locked messages in all threads or a specified thread. * @param handler An AsyncQueryHandler that will receive onQueryComplete * upon completion of looking for locked messages * @param threadId The threadId of the thread to search. -1 means all threads * @param token The token that will be passed to onQueryComplete */ public static void startQueryHaveLockedMessages(AsyncQueryHandler handler, long threadId, int token) { handler.cancelOperation(token); Uri uri = MmsSms.CONTENT_LOCKED_URI; if (threadId != -1) { uri = ContentUris.withAppendedId(uri, threadId); } handler.startQuery(token, new Long(threadId), uri, ALL_THREADS_PROJECTION, null, null, Conversations.DEFAULT_SORT_ORDER); } public static String orderBy() { String orderBy = Conversations.DEFAULT_SORT_ORDER; return orderBy; } /** * Fill the specified conversation with the values from the specified * cursor, possibly setting recipients to empty if {@value allowQuery} * is false and the recipient IDs are not in cache. The cursor should * be one made via {@link #startQueryForAll}. */ private static void fillFromCursor(Context context, Conversation conv, Cursor c, boolean allowQuery) { synchronized (conv) { conv.mThreadId = c.getLong(ID); conv.mDate = c.getLong(DATE); conv.mMessageCount = c.getInt(MESSAGE_COUNT); // Replace the snippet with a default value if it's empty. String snippet = MessageUtils.extractEncStrFromCursor(c, SNIPPET, SNIPPET_CS); if (TextUtils.isEmpty(snippet)) { snippet = context.getString(R.string.no_subject_view); } conv.mSnippet = snippet; //fixed bug for 19102 19105 start ,remove function checkConversationIsRead /*===== fixed CR<NEWMS00140874> by luning at 2011.11.22 begin =====*/ // boolean unRead = false; // if (c.getInt(READ) == 0) { // unRead = true; // if (checkConversationIsRead(conv.mThreadId, context)) { // unRead = false; // ContentValues readValue = new ContentValues(); // readValue.put(Threads.READ, 1); // context.getContentResolver().update( // ContentUris.withAppendedId(sAllThreadsUri, conv.mThreadId), readValue, // null, null); // } // } /*===== fixed CR<NEWMS00140874> by luning at 2011.11.22 end =====*/ //conv.setHasUnreadMessages(unRead); //fixed bug for 19102 19105 end conv.setHasUnreadMessages(c.getInt(READ) == 0); conv.mHasError = (c.getInt(ERROR) != 0); conv.mHasAttachment = (c.getInt(HAS_ATTACHMENT) != 0); } // Fill in as much of the conversation as we can before doing the slow stuff of looking // up the contacts associated with this conversation. String recipientIds = c.getString(RECIPIENT_IDS); ContactList recipients = ContactList.getByIds(recipientIds, allowQuery); synchronized (conv) { conv.mRecipients = recipients; } if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { Log.d(TAG,"fillFromCursor: conv=" + conv + ", recipientIds=" + recipientIds); } } /*===== fixed CR<NEWMS00140874> by luning at 2011.11.22 begin =====*/ private static boolean checkConversationIsRead(long threadId, Context context) { Uri threadUri = null; if (threadId > 0) { threadUri = ContentUris.withAppendedId(Threads.CONTENT_URI, threadId); } if (null != threadUri) { Cursor c = context.getContentResolver().query(threadUri, new String[] { Threads.READ }, "read=0", null, null); try { if (c.getCount() > 0) { return false; } } finally { if (null != c) { c.close(); } } } return true; } /*===== fixed CR<NEWMS00140874> by luning at 2011.11.22 end =====*/ /** * Private cache for the use of the various forms of Conversation.get. */ private static class Cache { private static Cache sInstance = new Cache(); static Cache getInstance() { return sInstance; } private final HashSet<Conversation> mCache; private Cache() { mCache = new HashSet<Conversation>(10); } /** * Return the conversation with the specified thread ID, or * null if it's not in cache. */ static Conversation get(long threadId) { synchronized (sInstance) { if (DEBUG || Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { LogTag.debug("Conversation get with threadId: " + threadId); } for (Conversation c : sInstance.mCache) { // if (DEBUG) { // LogTag.debug("Conversation get() threadId: " + threadId + // " c.getThreadId(): " + c.getThreadId()); // } if (c.getThreadId() == threadId) { return c; } } } return null; } /** * Return the conversation with the specified recipient * list, or null if it's not in cache. */ static Conversation get(ContactList list) { synchronized (sInstance) { if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { LogTag.debug("Conversation get with ContactList: " + list); } for (Conversation c : sInstance.mCache) { if (c.getRecipients().equals(list)) { return c; } } } return null; } /** * Put the specified conversation in the cache. The caller * should not place an already-existing conversation in the * cache, but rather update it in place. */ static void put(Conversation c) { synchronized (sInstance) { // We update cache entries in place so people with long- // held references get updated. if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { Log.d(TAG,"Conversation.Cache.put: conv= " + c + ", hash: " + c.hashCode()); } if (sInstance.mCache.contains(c)) { throw new IllegalStateException("cache already contains " + c + " threadId: " + c.mThreadId); } sInstance.mCache.add(c); } } static void remove(long threadId) { if (DEBUG) { LogTag.debug("remove threadid: " + threadId); dumpCache(); } for (Conversation c : sInstance.mCache) { if (c.getThreadId() == threadId) { sInstance.mCache.remove(c); return; } } } static void dumpCache() { synchronized (sInstance) { LogTag.debug("Conversation dumpCache: "); for (Conversation c : sInstance.mCache) { Log.d(TAG," conv: " + c.toString() + " hash: " + c.hashCode()); } } } /** * Remove all conversations from the cache that are not in * the provided set of thread IDs. */ static void keepOnly(Set<Long> threads) { synchronized (sInstance) { Iterator<Conversation> iter = sInstance.mCache.iterator(); while (iter.hasNext()) { Conversation c = iter.next(); if (!threads.contains(c.getThreadId())) { iter.remove(); } } } if (DEBUG) { LogTag.debug("after keepOnly"); dumpCache(); } } } /** * Set up the conversation cache. To be called once at application * startup time. */ public synchronized static void init(final Context context, final boolean isBlock) { if (!MmsApp.initMms) { return; } MmsApp.initMms = false; if (initConcatCacheDialog == null) { initConcatCacheDialog = new ProgressDialog(context); initConcatCacheDialog.setMessage(context.getText(R.string.sms_init_wait)); initConcatCacheDialog.setCancelable(false); } if (!initConcatCacheDialog.isShowing()) { initConcatCacheDialog.show(); } new Thread(new Runnable() { public void run() { cacheAllThreads(context, isBlock); if (initConcatCacheDialog != null) { initConcatCacheDialog.dismiss(); } } }).start(); } public static void dismissInitConcatCacheDialog() { if (initConcatCacheDialog != null) { initConcatCacheDialog.dismiss(); initConcatCacheDialog = null; } } public static void markAllConversationsAsSeen(final Context context) { if (DEBUG) { LogTag.debug("Conversation.markAllConversationsAsSeen"); } new Thread(new Runnable() { public void run() { blockingMarkAllSmsMessagesAsSeen(context); blockingMarkAllMmsMessagesAsSeen(context); // Always update notifications regardless of the read state. MessagingNotification.blockingUpdateAllNotifications(context); } }).start(); } public static void markSingleConversationsAsSeen(final Context context, final long id) { if (DEBUG) { LogTag.debug("Conversation.markAllConversationsAsSeen"); } blockingMarkSingleSmsMessagesAsSeen(context, id); blockingMarkSingleMmsMessagesAsSeen(context, id); } private static void blockingMarkSingleSmsMessagesAsSeen(final Context context, final long id) { ContentResolver resolver = context.getContentResolver(); Cursor cursor = resolver.query(Sms.Inbox.CONTENT_URI, SEEN_PROJECTION, "seen=0 and _id=" + id, null, null); int count = 0; if (cursor != null) { try { count = cursor.getCount(); } finally { cursor.close(); } } if (count == 0) { return; } if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { Log.d(TAG, "mark " + count + " SMS msg as seen"); } ContentValues values = new ContentValues(1); values.put("seen", 1); resolver.update(Sms.Inbox.CONTENT_URI, values, "seen=0 and _id=" + id, null); } private static void blockingMarkSingleMmsMessagesAsSeen(final Context context, final long id) { ContentResolver resolver = context.getContentResolver(); Cursor cursor = resolver.query(Mms.Inbox.CONTENT_URI, SEEN_PROJECTION, "seen=0 and _id=" + id, null, null); int count = 0; if (cursor != null) { try { count = cursor.getCount(); } finally { cursor.close(); } } if (count == 0) { return; } if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { Log.d(TAG, "mark " + count + " MMS msg as seen"); } ContentValues values = new ContentValues(1); values.put("seen", 1); resolver.update(Mms.Inbox.CONTENT_URI, values, "seen=0 and _id=" + id, null); } private static void blockingMarkAllSmsMessagesAsSeen(final Context context) { ContentResolver resolver = context.getContentResolver(); Cursor cursor = resolver.query(Sms.Inbox.CONTENT_URI, SEEN_PROJECTION, "seen=0", null, null); int count = 0; if (cursor != null) { try { count = cursor.getCount(); } finally { cursor.close(); } } if (count == 0) { return; } if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { Log.d(TAG, "mark " + count + " SMS msgs as seen"); } ContentValues values = new ContentValues(1); values.put("seen", 1); resolver.update(Sms.Inbox.CONTENT_URI, values, "seen=0", null); } private static void blockingMarkAllMmsMessagesAsSeen(final Context context) { ContentResolver resolver = context.getContentResolver(); Cursor cursor = resolver.query(Mms.Inbox.CONTENT_URI, SEEN_PROJECTION, "seen=0", null, null); int count = 0; if (cursor != null) { try { count = cursor.getCount(); } finally { cursor.close(); } } if (count == 0) { return; } if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { Log.d(TAG, "mark " + count + " MMS msgs as seen"); } ContentValues values = new ContentValues(1); values.put("seen", 1); resolver.update(Mms.Inbox.CONTENT_URI, values, "seen=0", null); } /** * Are we in the process of loading and caching all the threads?. */ public static boolean loadingThreads() { synchronized (Cache.getInstance()) { return mLoadingThreads; } } public static Object cacheThreadLock = new Object(); private static void cacheAllThreads(Context context, boolean isBlock) { if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { LogTag.debug("[Conversation] cacheAllThreads: begin"); } if ( isBlock ) { synchronized(cacheThreadLock) { try { cacheThreadLock.wait(3000); } catch (InterruptedException e) { Log.d(TAG,"cacheAllThreads() interrupts the cacheThreadLock waiting."); } catch ( IllegalMonitorStateException e ) { Log.d(TAG,"cacheAllThreads() throw an IllegalMonitorStateException."); } } } synchronized (Cache.getInstance()) { if (mLoadingThreads) { return; } mLoadingThreads = true; } // Keep track of what threads are now on disk so we // can discard anything removed from the cache. HashSet<Long> threadsOnDisk = new HashSet<Long>(); // Query for all conversations. Cursor c = context.getContentResolver().query(sAllThreadsUri, ALL_THREADS_PROJECTION, null, null, null); try { if (c != null) { while (c.moveToNext()) { long threadId = c.getLong(ID); threadsOnDisk.add(threadId); // Try to find this thread ID in the cache. Conversation conv; synchronized (Cache.getInstance()) { conv = Cache.get(threadId); } if (conv == null) { // Make a new Conversation and put it in // the cache if necessary. conv = new Conversation(context, c, true); try { synchronized (Cache.getInstance()) { Cache.put(conv); } } catch (IllegalStateException e) { LogTag.error("Tried to add duplicate Conversation to Cache"); } } else { // Or update in place so people with references // to conversations get updated too. fillFromCursor(context, conv, c, true); } } } } finally { if (c != null) { c.close(); } synchronized (Cache.getInstance()) { mLoadingThreads = false; } } // Purge the cache of threads that no longer exist on disk. Cache.keepOnly(threadsOnDisk); if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { LogTag.debug("[Conversation] cacheAllThreads: finished"); Cache.dumpCache(); } } public boolean loadFromThreadId(long threadId, boolean allowQuery) { Cursor c = mContext.getContentResolver().query(sAllThreadsUri, ALL_THREADS_PROJECTION, "_id=" + Long.toString(threadId), null, null); if (c == null) { LogTag.error("Cursor is null,loadFromThreadId: Can't find thread ID " + threadId); return false; } try { if (c.moveToFirst()) { fillFromCursor(mContext, this, c, allowQuery); if (threadId != mThreadId) { LogTag.error("loadFromThreadId: fillFromCursor returned differnt thread_id!" + " threadId=" + threadId + ", mThreadId=" + mThreadId); } } else { LogTag.error("loadFromThreadId: Can't find thread ID " + threadId); return false; } } finally { c.close(); } return true; } private static String getRecipients(Uri uri) { String base = uri.getSchemeSpecificPart(); int pos = base.indexOf('?'); return (pos == -1) ? base : base.substring(0, pos); } public static int cleanExpiredWapPush(Context context) { int count = 0; String nowTime = new SimpleDateFormat("yyyyMMddHHmmss") .format(new Date()); ContentResolver cr = context.getContentResolver(); try { count = cr.delete(Sms.CONTENT_URI, " expired <= " + nowTime, null); } catch (Exception e) { Log.e(TAG, "cleanExpiredWapPush", e); } finally { } return count; } public static void addContactThread(Runnable run) { if (mExecutorContact == null) { mExecutorContact = Executors.newFixedThreadPool(2); } if (mExecutorContact != null) { mExecutorContact.execute(run); } else { LogTag.debug("mExecutorContact is null"); run.run(); } } //fix bug for 19309 start private void addMakeAsReadThread(Runnable run) { if (mExecutorMakeAsRead == null) { LogTag.debug("Executors.newFixedThreadPool"); mExecutorMakeAsRead = Executors.newFixedThreadPool(2); } LogTag.debug("addMakeAsReadThread"); mExecutorMakeAsRead.execute(run); } //fix bug for 19309 end }