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 HashSet<Long> mDraftSet = new HashSet<Long>(4); 42 private final HashSet<OnDraftChangedListener> mChangeListeners 43 = new HashSet<OnDraftChangedListener>(1); 44 45 public interface OnDraftChangedListener { 46 void onDraftChanged(long threadId, boolean hasDraft); 47 } 48 49 private DraftCache(Context context) { 50 if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { 51 log("DraftCache.constructor"); 52 } 53 54 mContext = context; 55 refresh(); 56 } 57 58 static final String[] DRAFT_PROJECTION = new String[] { 59 Conversations.THREAD_ID // 0 60 }; 61 62 static final int COLUMN_DRAFT_THREAD_ID = 0; 63 64 /** To be called whenever the draft state might have changed. 65 * Dispatches work to a thread and returns immediately. 66 */ 67 public void refresh() { 68 if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { 69 log("refresh"); 70 } 71 72 new Thread(new Runnable() { 73 public void run() { 74 rebuildCache(); 75 } 76 }).start(); 77 } 78 79 /** Does the actual work of rebuilding the draft cache. 80 */ 81 private synchronized void rebuildCache() { 82 if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { 83 log("rebuildCache"); 84 } 85 86 HashSet<Long> oldDraftSet = mDraftSet; 87 HashSet<Long> newDraftSet = new HashSet<Long>(oldDraftSet.size()); 88 89 Cursor cursor = SqliteWrapper.query( 90 mContext, 91 mContext.getContentResolver(), 92 MmsSms.CONTENT_DRAFT_URI, 93 DRAFT_PROJECTION, null, null, null); 94 95 try { 96 if (cursor.moveToFirst()) { 97 for (; !cursor.isAfterLast(); cursor.moveToNext()) { 98 long threadId = cursor.getLong(COLUMN_DRAFT_THREAD_ID); 99 newDraftSet.add(threadId); 100 if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { 101 log("rebuildCache: add tid=" + threadId); 102 } 103 } 104 } 105 } finally { 106 cursor.close(); 107 } 108 109 mDraftSet = newDraftSet; 110 111 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 112 dump(); 113 } 114 115 // If nobody's interested in finding out about changes, 116 // just bail out early. 117 if (mChangeListeners.size() < 1) { 118 return; 119 } 120 121 // Find out which drafts were removed and added and notify 122 // listeners. 123 Set<Long> added = new HashSet<Long>(newDraftSet); 124 added.removeAll(oldDraftSet); 125 Set<Long> removed = new HashSet<Long>(oldDraftSet); 126 removed.removeAll(newDraftSet); 127 128 for (OnDraftChangedListener l : mChangeListeners) { 129 for (long threadId : added) { 130 l.onDraftChanged(threadId, true); 131 } 132 for (long threadId : removed) { 133 l.onDraftChanged(threadId, false); 134 } 135 } 136 } 137 138 /** Updates the has-draft status of a particular thread on 139 * a piecemeal basis, to be called when a draft has appeared 140 * or disappeared. 141 */ 142 public synchronized void setDraftState(long threadId, boolean hasDraft) { 143 if (threadId <= 0) { 144 return; 145 } 146 147 boolean changed; 148 if (hasDraft) { 149 changed = mDraftSet.add(threadId); 150 } else { 151 changed = mDraftSet.remove(threadId); 152 } 153 154 if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { 155 log("setDraftState: tid=" + threadId + ", value=" + hasDraft + ", changed=" + changed); 156 } 157 158 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 159 dump(); 160 } 161 162 // Notify listeners if there was a change. 163 if (changed) { 164 for (OnDraftChangedListener l : mChangeListeners) { 165 l.onDraftChanged(threadId, hasDraft); 166 } 167 } 168 } 169 170 /** Returns true if the given thread ID has a draft associated 171 * with it, false if not. 172 */ 173 public synchronized boolean hasDraft(long threadId) { 174 return mDraftSet.contains(threadId); 175 } 176 177 public synchronized void addOnDraftChangedListener(OnDraftChangedListener l) { 178 if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { 179 log("addOnDraftChangedListener " + l); 180 } 181 mChangeListeners.add(l); 182 } 183 184 public synchronized void removeOnDraftChangedListener(OnDraftChangedListener l) { 185 if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { 186 log("removeOnDraftChangedListener " + l); 187 } 188 mChangeListeners.remove(l); 189 } 190 191 /** 192 * Initialize the global instance. Should call only once. 193 */ 194 public static void init(Context context) { 195 sInstance = new DraftCache(context); 196 } 197 198 /** 199 * Get the global instance. 200 */ 201 public static DraftCache getInstance() { 202 return sInstance; 203 } 204 205 public void dump() { 206 Log.i(TAG, "dump:"); 207 for (Long threadId : mDraftSet) { 208 Log.i(TAG, " tid: " + threadId); 209 } 210 } 211 212 private void log(String format, Object... args) { 213 String s = String.format(format, args); 214 Log.d(TAG, "[DraftCache/" + Thread.currentThread().getId() + "] " + s); 215 } 216 } 217