/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.mms.util; import java.util.HashSet; import java.util.Set; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SqliteWrapper; import android.provider.Telephony.MmsSms; import android.provider.Telephony.Sms.Conversations; import android.util.Log; import com.android.mms.LogTag; /** * Cache for information about draft messages on conversations. */ public class DraftCache { private static final String TAG = "Mms/draft"; private static DraftCache sInstance; private final Context mContext; private boolean mSavingDraft; // true when we're in the process of saving a draft. Check this // before deleting any empty threads from the db. private final Object mSavingDraftLock = new Object(); private HashSet<Long> mDraftSet = new HashSet<Long>(4); private final Object mDraftSetLock = new Object(); private final HashSet<OnDraftChangedListener> mChangeListeners = new HashSet<OnDraftChangedListener>(1); private final Object mChangeListenersLock = new Object(); public interface OnDraftChangedListener { void onDraftChanged(long threadId, boolean hasDraft); } private DraftCache(Context context) { if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { log("DraftCache.constructor"); } mContext = context; refresh(); } static final String[] DRAFT_PROJECTION = new String[] { Conversations.THREAD_ID // 0 }; static final int COLUMN_DRAFT_THREAD_ID = 0; /** To be called whenever the draft state might have changed. * Dispatches work to a thread and returns immediately. */ public void refresh() { if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { log("refresh"); } Thread thread = new Thread(new Runnable() { @Override public void run() { rebuildCache(); } }, "DraftCache.refresh"); thread.setPriority(Thread.MIN_PRIORITY); thread.start(); } /** Does the actual work of rebuilding the draft cache. */ private void rebuildCache() { if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { log("rebuildCache"); } HashSet<Long> newDraftSet = new HashSet<Long>(); Cursor cursor = SqliteWrapper.query( mContext, mContext.getContentResolver(), MmsSms.CONTENT_DRAFT_URI, DRAFT_PROJECTION, null, null, null); if (cursor != null) { try { if (cursor.moveToFirst()) { for (; !cursor.isAfterLast(); cursor.moveToNext()) { long threadId = cursor.getLong(COLUMN_DRAFT_THREAD_ID); newDraftSet.add(threadId); if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { log("rebuildCache: add tid=" + threadId); } } } } finally { cursor.close(); } } Set<Long> added; Set<Long> removed; synchronized (mDraftSetLock) { HashSet<Long> oldDraftSet = mDraftSet; mDraftSet = newDraftSet; if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { dump(); } // If nobody's interested in finding out about changes, // just bail out early. synchronized (mChangeListenersLock) { if (mChangeListeners.size() < 1) { return; } } // Find out which drafts were removed and added and notify // listeners. added = new HashSet<Long>(newDraftSet); added.removeAll(oldDraftSet); removed = new HashSet<Long>(oldDraftSet); removed.removeAll(newDraftSet); } synchronized (mChangeListenersLock) { for (OnDraftChangedListener l : mChangeListeners) { for (long threadId : added) { l.onDraftChanged(threadId, true); } for (long threadId : removed) { l.onDraftChanged(threadId, false); } } } } /** Updates the has-draft status of a particular thread on * a piecemeal basis, to be called when a draft has appeared * or disappeared. */ public void setDraftState(long threadId, boolean hasDraft) { if (threadId <= 0) { return; } boolean changed; synchronized (mDraftSetLock) { if (hasDraft) { changed = mDraftSet.add(threadId); } else { changed = mDraftSet.remove(threadId); } } if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { log("setDraftState: tid=" + threadId + ", value=" + hasDraft + ", changed=" + changed); } if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { dump(); } // Notify listeners if there was a change. if (changed) { synchronized (mChangeListenersLock) { for (OnDraftChangedListener l : mChangeListeners) { l.onDraftChanged(threadId, hasDraft); } } } } /** Returns true if the given thread ID has a draft associated * with it, false if not. */ public boolean hasDraft(long threadId) { synchronized (mDraftSetLock) { return mDraftSet.contains(threadId); } } public void addOnDraftChangedListener(OnDraftChangedListener l) { if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { log("addOnDraftChangedListener " + l); } synchronized (mChangeListenersLock) { mChangeListeners.add(l); } } public void removeOnDraftChangedListener(OnDraftChangedListener l) { if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { log("removeOnDraftChangedListener " + l); } synchronized (mChangeListenersLock) { mChangeListeners.remove(l); } } public void setSavingDraft(final boolean savingDraft) { synchronized (mSavingDraftLock) { mSavingDraft = savingDraft; } } public boolean getSavingDraft() { synchronized (mSavingDraftLock) { return mSavingDraft; } } /** * Initialize the global instance. Should call only once. */ public static void init(Context context) { sInstance = new DraftCache(context); } /** * Get the global instance. */ public static DraftCache getInstance() { return sInstance; } public void dump() { Log.i(TAG, "dump:"); for (Long threadId : mDraftSet) { Log.i(TAG, " tid: " + threadId); } } private void log(String format, Object... args) { String s = String.format(format, args); Log.d(TAG, "[DraftCache/" + Thread.currentThread().getId() + "] " + s); } }