1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.mms.util; 18 19 import android.database.sqlite.SqliteWrapper; 20 import com.android.mms.LogTag; 21 22 import java.util.HashSet; 23 import java.util.Set; 24 25 import android.content.Context; 26 import android.database.Cursor; 27 import android.provider.Telephony.MmsSms; 28 import android.provider.Telephony.Sms.Conversations; 29 import android.util.Log; 30 31 /** 32 * Cache for information about draft messages on conversations. 33 */ 34 public class DraftCache { 35 private static final String TAG = "Mms/draft"; 36 37 private static DraftCache sInstance; 38 39 private final Context mContext; 40 41 private boolean mSavingDraft; // true when we're in the process of saving a draft. Check this 42 // before deleting any empty threads from the db. 43 private final Object mSavingDraftLock = new Object(); 44 45 private HashSet<Long> mDraftSet = new HashSet<Long>(4); 46 private final Object mDraftSetLock = new Object(); 47 private final HashSet<OnDraftChangedListener> mChangeListeners 48 = new HashSet<OnDraftChangedListener>(1); 49 private final Object mChangeListenersLock = new Object(); 50 51 public interface OnDraftChangedListener { 52 void onDraftChanged(long threadId, boolean hasDraft); 53 } 54 55 private DraftCache(Context context) { 56 if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { 57 log("DraftCache.constructor"); 58 } 59 60 mContext = context; 61 refresh(); 62 } 63 64 static final String[] DRAFT_PROJECTION = new String[] { 65 Conversations.THREAD_ID // 0 66 }; 67 68 static final int COLUMN_DRAFT_THREAD_ID = 0; 69 70 /** To be called whenever the draft state might have changed. 71 * Dispatches work to a thread and returns immediately. 72 */ 73 public void refresh() { 74 if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { 75 log("refresh"); 76 } 77 78 Thread thread = new Thread(new Runnable() { 79 @Override 80 public void run() { 81 rebuildCache(); 82 } 83 }, "DraftCache.refresh"); 84 thread.setPriority(Thread.MIN_PRIORITY); 85 thread.start(); 86 } 87 88 /** Does the actual work of rebuilding the draft cache. 89 */ 90 private void rebuildCache() { 91 if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { 92 log("rebuildCache"); 93 } 94 95 HashSet<Long> newDraftSet = new HashSet<Long>(); 96 97 Cursor cursor = SqliteWrapper.query( 98 mContext, 99 mContext.getContentResolver(), 100 MmsSms.CONTENT_DRAFT_URI, 101 DRAFT_PROJECTION, null, null, null); 102 103 if (cursor != null) { 104 try { 105 if (cursor.moveToFirst()) { 106 for (; !cursor.isAfterLast(); cursor.moveToNext()) { 107 long threadId = cursor.getLong(COLUMN_DRAFT_THREAD_ID); 108 newDraftSet.add(threadId); 109 if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { 110 log("rebuildCache: add tid=" + threadId); 111 } 112 } 113 } 114 } finally { 115 cursor.close(); 116 } 117 } 118 119 Set<Long> added; 120 Set<Long> removed; 121 synchronized (mDraftSetLock) { 122 HashSet<Long> oldDraftSet = mDraftSet; 123 mDraftSet = newDraftSet; 124 125 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 126 dump(); 127 } 128 129 // If nobody's interested in finding out about changes, 130 // just bail out early. 131 synchronized (mChangeListenersLock) { 132 if (mChangeListeners.size() < 1) { 133 return; 134 } 135 } 136 137 // Find out which drafts were removed and added and notify 138 // listeners. 139 added = new HashSet<Long>(newDraftSet); 140 added.removeAll(oldDraftSet); 141 removed = new HashSet<Long>(oldDraftSet); 142 removed.removeAll(newDraftSet); 143 } 144 145 synchronized (mChangeListenersLock) { 146 for (OnDraftChangedListener l : mChangeListeners) { 147 for (long threadId : added) { 148 l.onDraftChanged(threadId, true); 149 } 150 for (long threadId : removed) { 151 l.onDraftChanged(threadId, false); 152 } 153 } 154 } 155 } 156 157 /** Updates the has-draft status of a particular thread on 158 * a piecemeal basis, to be called when a draft has appeared 159 * or disappeared. 160 */ 161 public void setDraftState(long threadId, boolean hasDraft) { 162 if (threadId <= 0) { 163 return; 164 } 165 166 boolean changed; 167 synchronized (mDraftSetLock) { 168 if (hasDraft) { 169 changed = mDraftSet.add(threadId); 170 } else { 171 changed = mDraftSet.remove(threadId); 172 } 173 } 174 175 if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { 176 log("setDraftState: tid=" + threadId + ", value=" + hasDraft + ", changed=" + changed); 177 } 178 179 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 180 dump(); 181 } 182 183 // Notify listeners if there was a change. 184 if (changed) { 185 synchronized (mChangeListenersLock) { 186 for (OnDraftChangedListener l : mChangeListeners) { 187 l.onDraftChanged(threadId, hasDraft); 188 } 189 } 190 } 191 } 192 193 /** Returns true if the given thread ID has a draft associated 194 * with it, false if not. 195 */ 196 public boolean hasDraft(long threadId) { 197 synchronized (mDraftSetLock) { 198 return mDraftSet.contains(threadId); 199 } 200 } 201 202 public void addOnDraftChangedListener(OnDraftChangedListener l) { 203 if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { 204 log("addOnDraftChangedListener " + l); 205 } 206 synchronized (mChangeListenersLock) { 207 mChangeListeners.add(l); 208 } 209 } 210 211 public void removeOnDraftChangedListener(OnDraftChangedListener l) { 212 if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { 213 log("removeOnDraftChangedListener " + l); 214 } 215 synchronized (mChangeListenersLock) { 216 mChangeListeners.remove(l); 217 } 218 } 219 220 public void setSavingDraft(final boolean savingDraft) { 221 synchronized (mSavingDraftLock) { 222 mSavingDraft = savingDraft; 223 } 224 } 225 226 public boolean getSavingDraft() { 227 synchronized (mSavingDraftLock) { 228 return mSavingDraft; 229 } 230 } 231 232 /** 233 * Initialize the global instance. Should call only once. 234 */ 235 public static void init(Context context) { 236 sInstance = new DraftCache(context); 237 } 238 239 /** 240 * Get the global instance. 241 */ 242 public static DraftCache getInstance() { 243 return sInstance; 244 } 245 246 public void dump() { 247 Log.i(TAG, "dump:"); 248 for (Long threadId : mDraftSet) { 249 Log.i(TAG, " tid: " + threadId); 250 } 251 } 252 253 private void log(String format, Object... args) { 254 String s = String.format(format, args); 255 Log.d(TAG, "[DraftCache/" + Thread.currentThread().getId() + "] " + s); 256 } 257 } 258