| /* |
| * 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); |
| } |
| } |