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
17package com.android.mms.util;
18
19import java.util.HashSet;
20import java.util.Set;
21
22import android.content.Context;
23import android.database.Cursor;
24import android.database.sqlite.SqliteWrapper;
25import android.provider.Telephony.MmsSms;
26import android.provider.Telephony.Sms.Conversations;
27import android.util.Log;
28
29import com.android.mms.LogTag;
30
31/**
32 * Cache for information about draft messages on conversations.
33 */
34public 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