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 java.util.HashSet;
20 import java.util.Set;
21 
22 import android.content.Context;
23 import android.database.Cursor;
24 import android.database.sqlite.SqliteWrapper;
25 import android.provider.Telephony.MmsSms;
26 import android.provider.Telephony.Sms.Conversations;
27 import android.util.Log;
28 
29 import com.android.mms.LogTag;
30 
31 /**
32  * Cache for information about draft messages on conversations.
33  */
34 public class DraftCache {
35     private static final String TAG = LogTag.TAG;
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 {
onDraftChanged(long threadId, boolean hasDraft)52         void onDraftChanged(long threadId, boolean hasDraft);
53     }
54 
DraftCache(Context context)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      */
refresh()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      */
rebuildCache()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      */
setDraftState(long threadId, boolean hasDraft)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      */
hasDraft(long threadId)196     public boolean hasDraft(long threadId) {
197         synchronized (mDraftSetLock) {
198             return mDraftSet.contains(threadId);
199         }
200     }
201 
addOnDraftChangedListener(OnDraftChangedListener l)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 
removeOnDraftChangedListener(OnDraftChangedListener l)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 
setSavingDraft(final boolean savingDraft)220     public void setSavingDraft(final boolean savingDraft) {
221         synchronized (mSavingDraftLock) {
222             mSavingDraft = savingDraft;
223         }
224     }
225 
getSavingDraft()226     public boolean getSavingDraft() {
227         synchronized (mSavingDraftLock) {
228             return mSavingDraft;
229         }
230     }
231 
232     /**
233      * Initialize the global instance. Should call only once.
234      */
init(Context context)235     public static void init(Context context) {
236         sInstance = new DraftCache(context);
237     }
238 
239     /**
240      * Get the global instance.
241      */
getInstance()242     public static DraftCache getInstance() {
243         return sInstance;
244     }
245 
dump()246     public void dump() {
247         Log.i(TAG, "dump:");
248         for (Long threadId : mDraftSet) {
249             Log.i(TAG, "  tid: " + threadId);
250         }
251     }
252 
log(String format, Object... args)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