Home | History | Annotate | Download | only in util
      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