ConversationCursor.java revision 5d8b1fbb708eaa0e1b38e67e0485d31a40b936cc
1/*******************************************************************************
2 *      Copyright (C) 2012 Google Inc.
3 *      Licensed to The Android Open Source Project.
4 *
5 *      Licensed under the Apache License, Version 2.0 (the "License");
6 *      you may not use this file except in compliance with the License.
7 *      You may obtain a copy of the License at
8 *
9 *           http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *      Unless required by applicable law or agreed to in writing, software
12 *      distributed under the License is distributed on an "AS IS" BASIS,
13 *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *      See the License for the specific language governing permissions and
15 *      limitations under the License.
16 *******************************************************************************/
17
18package com.android.mail.browse;
19
20import android.app.Activity;
21import android.content.ContentProvider;
22import android.content.ContentProviderOperation;
23import android.content.ContentResolver;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.OperationApplicationException;
27import android.database.CharArrayBuffer;
28import android.database.ContentObserver;
29import android.database.Cursor;
30import android.database.CursorWrapper;
31import android.database.DataSetObservable;
32import android.database.DataSetObserver;
33import android.net.Uri;
34import android.os.AsyncTask;
35import android.os.Bundle;
36import android.os.Looper;
37import android.os.RemoteException;
38
39import com.android.mail.providers.Conversation;
40import com.android.mail.providers.UIProvider;
41import com.android.mail.providers.UIProvider.ConversationColumns;
42import com.android.mail.providers.UIProvider.ConversationListQueryParameters;
43import com.android.mail.providers.UIProvider.ConversationOperations;
44import com.android.mail.utils.LogUtils;
45import com.google.common.annotations.VisibleForTesting;
46import com.google.common.collect.Lists;
47
48import java.util.ArrayList;
49import java.util.Arrays;
50import java.util.Collection;
51import java.util.HashMap;
52import java.util.Iterator;
53import java.util.List;
54
55/**
56 * ConversationCursor is a wrapper around a conversation list cursor that provides update/delete
57 * caching for quick UI response. This is effectively a singleton class, as the cache is
58 * implemented as a static HashMap.
59 */
60public final class ConversationCursor implements Cursor {
61    private static final String TAG = "ConversationCursor";
62
63    private static final boolean DEBUG = true;  // STOPSHIP Set to false before shipping
64    // A deleted row is indicated by the presence of DELETED_COLUMN in the cache map
65    private static final String DELETED_COLUMN = "__deleted__";
66    // An row cached during a requery is indicated by the presence of REQUERY_COLUMN in the map
67    private static final String REQUERY_COLUMN = "__requery__";
68    // A sentinel value for the "index" of the deleted column; it's an int that is otherwise invalid
69    private static final int DELETED_COLUMN_INDEX = -1;
70    // Empty deletion list
71    private static final Collection<Conversation> EMPTY_DELETION_LIST = Lists.newArrayList();
72    // The index of the Uri whose data is reflected in the cached row
73    // Updates/Deletes to this Uri are cached
74    private static int sUriColumnIndex;
75    // Our sequence count (for changes sent to underlying provider)
76    private static int sSequence = 0;
77    // The resolver for the cursor instantiator's context
78    private static ContentResolver sResolver;
79    @VisibleForTesting
80    static ConversationProvider sProvider;
81
82    // The cursor instantiator's activity
83    private Activity mActivity;
84    // The cursor underlying the caching cursor
85    @VisibleForTesting
86    Wrapper mUnderlyingCursor;
87    // The new cursor obtained via a requery
88    private volatile Wrapper mRequeryCursor;
89    // A mapping from Uri to updated ContentValues
90    private HashMap<String, ContentValues> mCacheMap = new HashMap<String, ContentValues>();
91    // Cache map lock (will be used only very briefly - few ms at most)
92    private Object mCacheMapLock = new Object();
93    // The listeners registered for this cursor
94    private List<ConversationListener> mListeners = Lists.newArrayList();
95    // The ConversationProvider instance
96    // The runnable executing a refresh (query of underlying provider)
97    private RefreshTask mRefreshTask;
98    // Set when we've sent refreshReady() to listeners
99    private boolean mRefreshReady = false;
100    // Set when we've sent refreshRequired() to listeners
101    private boolean mRefreshRequired = false;
102    // Whether our first query on this cursor should include a limit
103    private boolean mInitialConversationLimit = false;
104    // A list of mostly-dead items
105    private List<Conversation> sMostlyDead = Lists.newArrayList();
106    // The name of the loader
107    private final String mName;
108    // Column names for this cursor
109    private String[] mColumnNames;
110    // An observer on the underlying cursor (so we can detect changes from outside the UI)
111    private final CursorObserver mCursorObserver;
112    // Whether our observer is currently registered with the underlying cursor
113    private boolean mCursorObserverRegistered = false;
114    // Whether our loader is paused
115    private boolean mPaused = false;
116    // Whether or not sync from underlying provider should be deferred
117    private boolean mDeferSync = false;
118
119    // The current position of the cursor
120    private int mPosition = -1;
121
122    /**
123     * Allow UI elements to subscribe to changes that other UI elements might make to this data.
124     * This short circuits the usual DB round-trip needed for data to propagate across disparate
125     * UI elements.
126     * <p>
127     * A UI element that receives a notification on this channel should just update its existing
128     * view, and should not trigger a full refresh.
129     */
130    private final DataSetObservable mDataSetObservable = new DataSetObservable();
131
132    // The number of cached deletions from this cursor (used to quickly generate an accurate count)
133    private int mDeletedCount = 0;
134
135    // Parameters passed to the underlying query
136    private Uri qUri;
137    private String[] qProjection;
138
139    private void setCursor(Wrapper cursor) {
140        // If we have an existing underlying cursor, make sure it's closed
141        if (mUnderlyingCursor != null) {
142            mUnderlyingCursor.close();
143        }
144        mColumnNames = cursor.getColumnNames();
145        mRefreshRequired = false;
146        mRefreshReady = false;
147        mRefreshTask = null;
148        resetCursor(cursor);
149    }
150
151    public ConversationCursor(Activity activity, Uri uri, boolean initialConversationLimit,
152            String name) {
153        //sActivity = activity;
154        mInitialConversationLimit = initialConversationLimit;
155        sResolver = activity.getContentResolver();
156        sUriColumnIndex = UIProvider.CONVERSATION_URI_COLUMN;
157        qUri = uri;
158        mName = name;
159        qProjection = UIProvider.CONVERSATION_PROJECTION;
160        mCursorObserver = new CursorObserver();
161    }
162
163    /**
164     * Create a ConversationCursor; this should be called by the ListActivity using that cursor
165     * @param activity the activity creating the cursor
166     * @param messageListColumn the column used for individual cursor items
167     * @param uri the query uri
168     * @param projection the query projecion
169     * @param selection the query selection
170     * @param selectionArgs the query selection args
171     * @param sortOrder the query sort order
172     * @return a ConversationCursor
173     */
174    public void load() {
175        synchronized (mCacheMapLock) {
176            try {
177                // Create new ConversationCursor
178                LogUtils.i(TAG, "Create: initial creation");
179                Wrapper c = doQuery(mInitialConversationLimit);
180                setCursor(c);
181            } finally {
182                // If we used a limit, queue up a query without limit
183                if (mInitialConversationLimit) {
184                    mInitialConversationLimit = false;
185                    refresh();
186                }
187            }
188        }
189    }
190
191    /**
192     * Pause notifications to UI
193     */
194    public void pause() {
195        if (DEBUG) {
196            LogUtils.i(TAG, "[Paused: " + mName + "]");
197        }
198        mPaused = true;
199    }
200
201    /**
202     * Resume notifications to UI; if any are pending, send them
203     */
204    public void resume() {
205        if (DEBUG) {
206            LogUtils.i(TAG, "[Resumed: " + mName + "]");
207        }
208        mPaused = false;
209        checkNotifyUI();
210    }
211
212    private void checkNotifyUI() {
213        if (!mPaused && !mDeferSync) {
214            if (mRefreshRequired && (mRefreshTask == null)) {
215                notifyRefreshRequired();
216            } else if (mRefreshReady) {
217                notifyRefreshReady();
218            }
219        } else {
220            LogUtils.i(TAG, "[checkNotifyUI: " + (mPaused ? "Paused " : "") +
221                    (mDeferSync ? "Defer" : ""));
222        }
223    }
224
225    /**
226     * Runnable that performs the query on the underlying provider
227     */
228    private class RefreshTask extends AsyncTask<Void, Void, Void> {
229        private Wrapper mCursor = null;
230
231        private RefreshTask() {
232        }
233
234        @Override
235        protected Void doInBackground(Void... params) {
236            if (DEBUG) {
237                LogUtils.i(TAG, "[Start refresh of " + mName + ": %d]", hashCode());
238            }
239            // Get new data
240            mCursor = doQuery(false);
241            return null;
242        }
243
244        @Override
245        protected void onPostExecute(Void param) {
246            synchronized(mCacheMapLock) {
247                mRequeryCursor = mCursor;
248                // Make sure window is full
249                mRequeryCursor.getCount();
250                mRefreshReady = true;
251                if (DEBUG) {
252                    LogUtils.i(TAG, "[Query done " + mName + ": %d]", hashCode());
253                }
254                if (!mDeferSync && !mPaused) {
255                    notifyRefreshReady();
256                }
257            }
258        }
259
260        @Override
261        protected void onCancelled() {
262            if (DEBUG) {
263                LogUtils.i(TAG, "[Ignoring refresh result: %d]", hashCode());
264            }
265            if (mCursor != null) {
266                mCursor.close();
267            }
268        }
269    }
270
271    /**
272     * Wrapper that includes the Uri used to create the cursor
273     */
274    private static class Wrapper extends CursorWrapper {
275        private final Uri mUri;
276
277        Wrapper(Cursor cursor, Uri uri) {
278            super(cursor);
279            mUri = uri;
280        }
281    }
282
283    private Wrapper doQuery(boolean withLimit) {
284        if (sResolver == null) {
285            sResolver = mActivity.getContentResolver();
286        }
287        Uri uri = qUri;
288        if (withLimit) {
289            uri = uri.buildUpon().appendQueryParameter(ConversationListQueryParameters.LIMIT,
290                    ConversationListQueryParameters.DEFAULT_LIMIT).build();
291        }
292        long time = System.currentTimeMillis();
293
294        Wrapper result = new Wrapper(sResolver.query(uri, qProjection, null, null, null), uri);
295        if (DEBUG) {
296            time = System.currentTimeMillis() - time;
297            LogUtils.i(TAG, "ConversationCursor query: %s, %dms, %d results",
298                    uri, time, result.getCount());
299        }
300        return result;
301    }
302
303    /**
304     * Return whether the uri string (message list uri) is in the underlying cursor
305     * @param uriString the uri string we're looking for
306     * @return true if the uri string is in the cursor; false otherwise
307     */
308    private boolean isInUnderlyingCursor(String uriString) {
309        mUnderlyingCursor.moveToPosition(-1);
310        while (mUnderlyingCursor.moveToNext()) {
311            if (uriString.equals(mUnderlyingCursor.getString(sUriColumnIndex))) {
312                return true;
313            }
314        }
315        return false;
316    }
317
318    static boolean offUiThread() {
319        return Looper.getMainLooper().getThread() != Thread.currentThread();
320    }
321
322    /**
323     * Reset the cursor; this involves clearing out our cache map and resetting our various counts
324     * The cursor should be reset whenever we get fresh data from the underlying cursor. The cache
325     * is locked during the reset, which will block the UI, but for only a very short time
326     * (estimated at a few ms, but we can profile this; remember that the cache will usually
327     * be empty or have a few entries)
328     */
329    private void resetCursor(Wrapper newCursor) {
330        synchronized (mCacheMapLock) {
331            // Walk through the cache.  Here are the cases:
332            // 1) The entry isn't marked with REQUERY - remove it from the cache. If DELETED is
333            //    set, decrement the deleted count
334            // 2) The REQUERY entry is still in the UP
335            //    2a) The REQUERY entry isn't DELETED; we're good, and the client change will remain
336            //    (i.e. client wins, it's on its way to the UP)
337            //    2b) The REQUERY entry is DELETED; we're good (client change remains, it's on
338            //        its way to the UP)
339            // 3) the REQUERY was deleted on the server (sheesh; this would be bizarre timing!) -
340            //    we need to throw the item out of the cache
341            // So ... the only interesting case is #3, we need to look for remaining deleted items
342            // and see if they're still in the UP
343            Iterator<HashMap.Entry<String, ContentValues>> iter = mCacheMap.entrySet().iterator();
344            while (iter.hasNext()) {
345                HashMap.Entry<String, ContentValues> entry = iter.next();
346                ContentValues values = entry.getValue();
347                if (values.containsKey(REQUERY_COLUMN) && isInUnderlyingCursor(entry.getKey())) {
348                    // If we're in a requery and we're still around, remove the requery key
349                    // We're good here, the cached change (delete/update) is on its way to UP
350                    values.remove(REQUERY_COLUMN);
351                    LogUtils.i(TAG, new Error(),
352                            "IN resetCursor, remove requery column from %s", entry.getKey());
353                } else {
354                    // Keep the deleted count up-to-date; remove the cache entry
355                    if (values.containsKey(DELETED_COLUMN)) {
356                        mDeletedCount--;
357                        LogUtils.i(TAG, new Error(),
358                                "IN resetCursor, sDeletedCount decremented to: %d by %s",
359                                mDeletedCount, entry.getKey());
360                    }
361                    // Remove the entry
362                    iter.remove();
363                }
364            }
365
366            // Swap cursor
367            if (mUnderlyingCursor != null) {
368                close();
369            }
370            mUnderlyingCursor = newCursor;
371
372            mPosition = -1;
373            mUnderlyingCursor.moveToPosition(mPosition);
374            if (!mCursorObserverRegistered) {
375                mUnderlyingCursor.registerContentObserver(mCursorObserver);
376                mCursorObserverRegistered = true;
377            }
378            mRefreshRequired = false;
379        }
380    }
381
382    /**
383     * Add a listener for this cursor; we'll notify it when our data changes
384     */
385    public void addListener(ConversationListener listener) {
386        synchronized (mListeners) {
387            if (!mListeners.contains(listener)) {
388                mListeners.add(listener);
389            } else {
390                LogUtils.i(TAG, "Ignoring duplicate add of listener");
391            }
392        }
393    }
394
395    /**
396     * Remove a listener for this cursor
397     */
398    public void removeListener(ConversationListener listener) {
399        synchronized(mListeners) {
400            mListeners.remove(listener);
401        }
402    }
403
404    /**
405     * Generate a forwarding Uri to ConversationProvider from an original Uri.  We do this by
406     * changing the authority to ours, but otherwise leaving the Uri intact.
407     * NOTE: This won't handle query parameters, so the functionality will need to be added if
408     * parameters are used in the future
409     * @param uri the uri
410     * @return a forwarding uri to ConversationProvider
411     */
412    private static String uriToCachingUriString (Uri uri) {
413        String provider = uri.getAuthority();
414        return uri.getScheme() + "://" + ConversationProvider.AUTHORITY
415                + "/" + provider + uri.getPath();
416    }
417
418    /**
419     * Regenerate the original Uri from a forwarding (ConversationProvider) Uri
420     * NOTE: See note above for uriToCachingUri
421     * @param uri the forwarding Uri
422     * @return the original Uri
423     */
424    private static Uri uriFromCachingUri(Uri uri) {
425        String authority = uri.getAuthority();
426        // Don't modify uri's that aren't ours
427        if (!authority.equals(ConversationProvider.AUTHORITY)) {
428            return uri;
429        }
430        List<String> path = uri.getPathSegments();
431        Uri.Builder builder = new Uri.Builder().scheme(uri.getScheme()).authority(path.get(0));
432        for (int i = 1; i < path.size(); i++) {
433            builder.appendPath(path.get(i));
434        }
435        return builder.build();
436    }
437
438    private static String uriStringFromCachingUri(Uri uri) {
439        Uri underlyingUri = uriFromCachingUri(uri);
440        // Remember to decode the underlying Uri as it might be encoded (as w/ Gmail)
441        return Uri.decode(underlyingUri.toString());
442    }
443
444    public void setConversationColumn(String uriString, String columnName, Object value) {
445        synchronized (mCacheMapLock) {
446            cacheValue(uriString, columnName, value);
447        }
448        notifyDataChanged();
449    }
450
451    /**
452     * Cache a column name/value pair for a given Uri
453     * @param uriString the Uri for which the column name/value pair applies
454     * @param columnName the column name
455     * @param value the value to be cached
456     */
457    private void cacheValue(String uriString, String columnName, Object value) {
458        // Calling this method off the UI thread will mess with ListView's reading of the cursor's
459        // count
460        if (offUiThread()) {
461            LogUtils.e(TAG, new Error(), "cacheValue incorrectly being called from non-UI thread");
462        }
463
464        synchronized (mCacheMapLock) {
465            // Get the map for our uri
466            ContentValues map = mCacheMap.get(uriString);
467            // Create one if necessary
468            if (map == null) {
469                map = new ContentValues();
470                mCacheMap.put(uriString, map);
471            }
472            // If we're caching a deletion, add to our count
473            if (columnName == DELETED_COLUMN) {
474                final boolean state = (Boolean)value;
475                final boolean hasValue = map.get(columnName) != null;
476                if (state && !hasValue) {
477                    mDeletedCount++;
478                    if (DEBUG) {
479                        LogUtils.i(TAG, "Deleted %s, incremented deleted count=%d", uriString,
480                                mDeletedCount);
481                    }
482                } else if (!state && hasValue) {
483                    mDeletedCount--;
484                    map.remove(columnName);
485                    if (DEBUG) {
486                        LogUtils.i(TAG, "Undeleted %s, decremented deleted count=%d", uriString,
487                                mDeletedCount);
488                    }
489                    return;
490                } else if (!state) {
491                    // Trying to undelete, but it's not deleted; just return
492                    if (DEBUG) {
493                        LogUtils.i(TAG, "Undeleted %s, IGNORING, deleted count=%d", uriString,
494                                mDeletedCount);
495                    }
496                    return;
497                }
498            }
499            // ContentValues has no generic "put", so we must test.  For now, the only classes
500            // of values implemented are Boolean/Integer/String, though others are trivially
501            // added
502            if (value instanceof Boolean) {
503                map.put(columnName, ((Boolean) value).booleanValue() ? 1 : 0);
504            } else if (value instanceof Integer) {
505                map.put(columnName, (Integer) value);
506            } else if (value instanceof String) {
507                map.put(columnName, (String) value);
508            } else {
509                final String cname = value.getClass().getName();
510                throw new IllegalArgumentException("Value class not compatible with cache: "
511                        + cname);
512            }
513            if (mRefreshTask != null) {
514                map.put(REQUERY_COLUMN, 1);
515            }
516            if (DEBUG && (columnName != DELETED_COLUMN)) {
517                LogUtils.i(TAG, "Caching value for %s: %s", uriString, columnName);
518            }
519        }
520    }
521
522    /**
523     * Get the cached value for the provided column; we special case -1 as the "deleted" column
524     * @param columnIndex the index of the column whose cached value we want to retrieve
525     * @return the cached value for this column, or null if there is none
526     */
527    private Object getCachedValue(int columnIndex) {
528        String uri = mUnderlyingCursor.getString(sUriColumnIndex);
529        return getCachedValue(uri, columnIndex);
530    }
531
532    private Object getCachedValue(String uri, int columnIndex) {
533        ContentValues uriMap = mCacheMap.get(uri);
534        if (uriMap != null) {
535            String columnName;
536            if (columnIndex == DELETED_COLUMN_INDEX) {
537                columnName = DELETED_COLUMN;
538            } else {
539                columnName = mColumnNames[columnIndex];
540            }
541            return uriMap.get(columnName);
542        }
543        return null;
544    }
545
546    /**
547     * When the underlying cursor changes, we want to alert the listener
548     */
549    private void underlyingChanged() {
550        synchronized(mCacheMapLock) {
551            if (mCursorObserverRegistered) {
552                try {
553                    mUnderlyingCursor.unregisterContentObserver(mCursorObserver);
554                } catch (IllegalStateException e) {
555                    // Maybe the cursor was GC'd?
556                }
557                mCursorObserverRegistered = false;
558            }
559            mRefreshRequired = true;
560            if (!mPaused) {
561                notifyRefreshRequired();
562            }
563        }
564    }
565
566    /**
567     * Must be called on UI thread; notify listeners that a refresh is required
568     */
569    private void notifyRefreshRequired() {
570        if (DEBUG) {
571            LogUtils.i(TAG, "[Notify " + mName + ": onRefreshRequired()]");
572        }
573        if (!mDeferSync) {
574            synchronized(mListeners) {
575                for (ConversationListener listener: mListeners) {
576                    listener.onRefreshRequired();
577                }
578            }
579        }
580    }
581
582    /**
583     * Must be called on UI thread; notify listeners that a new cursor is ready
584     */
585    private void notifyRefreshReady() {
586        if (DEBUG) {
587            LogUtils.i(TAG, "[Notify " + mName + ": onRefreshReady(), " + mListeners.size() +
588                    " listeners]");
589        }
590        synchronized(mListeners) {
591            for (ConversationListener listener: mListeners) {
592                listener.onRefreshReady();
593            }
594        }
595    }
596
597    /**
598     * Must be called on UI thread; notify listeners that data has changed
599     */
600    private void notifyDataChanged() {
601        if (DEBUG) {
602            LogUtils.i(TAG, "[Notify " + mName + ": onDataSetChanged()]");
603        }
604        synchronized(mListeners) {
605            for (ConversationListener listener: mListeners) {
606                listener.onDataSetChanged();
607            }
608        }
609    }
610
611    /**
612     * Put the refreshed cursor in place (called by the UI)
613     */
614    public void sync() {
615        if (mRequeryCursor == null) {
616            // This can happen during an animated deletion, if the UI isn't keeping track, or
617            // if a new query intervened (i.e. user changed folders)
618            if (DEBUG) {
619                LogUtils.i(TAG, "[sync() " + mName + "; no requery cursor]");
620            }
621            return;
622        }
623        synchronized(mCacheMapLock) {
624            if (DEBUG) {
625                LogUtils.i(TAG, "[sync() " + mName+ "]");
626            }
627            resetCursor(mRequeryCursor);
628            mRequeryCursor = null;
629            mRefreshTask = null;
630            mRefreshReady = false;
631        }
632        notifyDataChanged();
633    }
634
635    public boolean isRefreshRequired() {
636        return mRefreshRequired;
637    }
638
639    public boolean isRefreshReady() {
640        return mRefreshReady;
641    }
642
643    /**
644     * Cancel a refresh in progress
645     */
646    public void cancelRefresh() {
647        if (DEBUG) {
648            LogUtils.i(TAG, "[cancelRefresh() " + mName + "]");
649        }
650        synchronized(mCacheMapLock) {
651            if (mRefreshTask != null) {
652                mRefreshTask.cancel(true);
653                mRefreshTask = null;
654            }
655            mRefreshReady = false;
656            // If we have the cursor, close it; otherwise, it will get closed when the query
657            // finishes (it checks sRefreshInProgress)
658            if (mRequeryCursor != null) {
659                mRequeryCursor.close();
660                mRequeryCursor = null;
661            }
662        }
663    }
664
665    /**
666     * Get a list of deletions from ConversationCursor to the refreshed cursor that hasn't yet
667     * been swapped into place; this allows the UI to animate these away if desired
668     * @return a list of positions deleted in ConversationCursor
669     */
670    public Collection<Conversation> getRefreshDeletions () {
671        return EMPTY_DELETION_LIST;
672    }
673
674    /**
675     * When we get a requery from the UI, we'll do it, but also clear the cache. The listener is
676     * notified when the requery is complete
677     * NOTE: This will have to change, of course, when we start using loaders...
678     */
679    public boolean refresh() {
680        if (DEBUG) {
681            LogUtils.i(TAG, "[refresh() " + mName + "]");
682        }
683        synchronized(mCacheMapLock) {
684            if (mRefreshTask != null) {
685                if (DEBUG) {
686                    LogUtils.i(TAG, "[refresh() " + mName + " returning; already running %d]",
687                            mRefreshTask.hashCode());
688                }
689                return false;
690            }
691            mRefreshTask = new RefreshTask();
692            mRefreshTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
693        }
694        return true;
695    }
696
697    public void disable() {
698        close();
699        mCacheMap.clear();
700        mListeners.clear();
701        mUnderlyingCursor = null;
702    }
703
704    @Override
705    public void close() {
706        if (mUnderlyingCursor != null && !mUnderlyingCursor.isClosed()) {
707            // Unregister our observer on the underlying cursor and close as usual
708            if (mCursorObserverRegistered) {
709                try {
710                    mUnderlyingCursor.unregisterContentObserver(mCursorObserver);
711                } catch (IllegalStateException e) {
712                    // Maybe the cursor got GC'd?
713                }
714                mCursorObserverRegistered = false;
715            }
716            mUnderlyingCursor.close();
717        }
718    }
719
720    /**
721     * Move to the next not-deleted item in the conversation
722     */
723    @Override
724    public boolean moveToNext() {
725        while (true) {
726            boolean ret = mUnderlyingCursor.moveToNext();
727            if (!ret) {
728                // Make sure we're still in sync (mPosition++ should also work)
729                mPosition = mUnderlyingCursor.getPosition();
730                return false;
731            }
732            if (getCachedValue(DELETED_COLUMN_INDEX) instanceof Integer) continue;
733            mPosition++;
734            return true;
735        }
736    }
737
738    /**
739     * Move to the previous not-deleted item in the conversation
740     */
741    @Override
742    public boolean moveToPrevious() {
743        while (true) {
744            boolean ret = mUnderlyingCursor.moveToPrevious();
745            if (!ret) {
746                // Make sure we're before the first position
747                mPosition = -1;
748                return false;
749            }
750            if (getCachedValue(DELETED_COLUMN_INDEX) instanceof Integer) continue;
751            mPosition--;
752            return true;
753        }
754    }
755
756    @Override
757    public int getPosition() {
758        return mPosition;
759    }
760
761    /**
762     * The actual cursor's count must be decremented by the number we've deleted from the UI
763     */
764    @Override
765    public int getCount() {
766        if (mUnderlyingCursor == null) {
767            throw new IllegalStateException(
768                    "getCount() on disabled cursor: " + mName + "(" + qUri + ")");
769        }
770        return mUnderlyingCursor.getCount() - mDeletedCount;
771    }
772
773    @Override
774    public boolean moveToFirst() {
775        if (mUnderlyingCursor == null) {
776            throw new IllegalStateException(
777                    "moveToFirst() on disabled cursor: " + mName + "(" + qUri + ")");
778        }
779        mUnderlyingCursor.moveToPosition(-1);
780        mPosition = -1;
781        return moveToNext();
782    }
783
784    @Override
785    public boolean moveToPosition(int pos) {
786        if (mUnderlyingCursor == null) {
787            throw new IllegalStateException(
788                    "moveToPosition() on disabled cursor: " + mName + "(" + qUri + ")");
789        }
790        if (pos == mPosition) return true;
791        if (pos > mPosition) {
792            while (pos > mPosition) {
793                if (!moveToNext()) {
794                    return false;
795                }
796            }
797            return true;
798        } else if (pos == 0) {
799            return moveToFirst();
800        } else {
801            while (pos < mPosition) {
802                if (!moveToPrevious()) {
803                    return false;
804                }
805            }
806            return true;
807        }
808    }
809
810    /**
811     * Make sure mPosition is correct after locally deleting/undeleting items
812     */
813    private void recalibratePosition() {
814        int pos = mPosition;
815        moveToFirst();
816        moveToPosition(pos);
817    }
818
819    @Override
820    public boolean moveToLast() {
821        throw new UnsupportedOperationException("moveToLast unsupported!");
822    }
823
824    @Override
825    public boolean move(int offset) {
826        throw new UnsupportedOperationException("move unsupported!");
827    }
828
829    /**
830     * We need to override all of the getters to make sure they look at cached values before using
831     * the values in the underlying cursor
832     */
833    @Override
834    public double getDouble(int columnIndex) {
835        Object obj = getCachedValue(columnIndex);
836        if (obj != null) return (Double)obj;
837        return mUnderlyingCursor.getDouble(columnIndex);
838    }
839
840    @Override
841    public float getFloat(int columnIndex) {
842        Object obj = getCachedValue(columnIndex);
843        if (obj != null) return (Float)obj;
844        return mUnderlyingCursor.getFloat(columnIndex);
845    }
846
847    @Override
848    public int getInt(int columnIndex) {
849        Object obj = getCachedValue(columnIndex);
850        if (obj != null) return (Integer)obj;
851        return mUnderlyingCursor.getInt(columnIndex);
852    }
853
854    @Override
855    public long getLong(int columnIndex) {
856        Object obj = getCachedValue(columnIndex);
857        if (obj != null) return (Long)obj;
858        return mUnderlyingCursor.getLong(columnIndex);
859    }
860
861    @Override
862    public short getShort(int columnIndex) {
863        Object obj = getCachedValue(columnIndex);
864        if (obj != null) return (Short)obj;
865        return mUnderlyingCursor.getShort(columnIndex);
866    }
867
868    @Override
869    public String getString(int columnIndex) {
870        // If we're asking for the Uri for the conversation list, we return a forwarding URI
871        // so that we can intercept update/delete and handle it ourselves
872        if (columnIndex == sUriColumnIndex) {
873            Uri uri = Uri.parse(mUnderlyingCursor.getString(columnIndex));
874            return uriToCachingUriString(uri);
875        }
876        Object obj = getCachedValue(columnIndex);
877        if (obj != null) return (String)obj;
878        return mUnderlyingCursor.getString(columnIndex);
879    }
880
881    @Override
882    public byte[] getBlob(int columnIndex) {
883        Object obj = getCachedValue(columnIndex);
884        if (obj != null) return (byte[])obj;
885        return mUnderlyingCursor.getBlob(columnIndex);
886    }
887
888    /**
889     * Observer of changes to underlying data
890     */
891    private class CursorObserver extends ContentObserver {
892        public CursorObserver() {
893            super(null);
894        }
895
896        @Override
897        public void onChange(boolean selfChange) {
898            // If we're here, then something outside of the UI has changed the data, and we
899            // must query the underlying provider for that data
900            ConversationCursor.this.underlyingChanged();
901        }
902    }
903
904    /**
905     * ConversationProvider is the ContentProvider for our forwarding Uri's; it passes queries
906     * and inserts directly, and caches updates/deletes before passing them through.  The caching
907     * will cause a redraw of the list with updated values.
908     */
909    public abstract static class ConversationProvider extends ContentProvider {
910        public static String AUTHORITY;
911
912        /**
913         * Allows the implementing provider to specify the authority that should be used.
914         */
915        protected abstract String getAuthority();
916
917        @Override
918        public boolean onCreate() {
919            sProvider = this;
920            AUTHORITY = getAuthority();
921            return true;
922        }
923
924        @Override
925        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
926                String sortOrder) {
927            return sResolver.query(
928                    uriFromCachingUri(uri), projection, selection, selectionArgs, sortOrder);
929        }
930
931        @Override
932        public Uri insert(Uri uri, ContentValues values) {
933            insertLocal(uri, values);
934            return ProviderExecute.opInsert(uri, values);
935        }
936
937        @Override
938        public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
939            throw new IllegalStateException("Unexpected call to ConversationProvider.delete");
940//            updateLocal(uri, values);
941           // return ProviderExecute.opUpdate(uri, values);
942        }
943
944        @Override
945        public int delete(Uri uri, String selection, String[] selectionArgs) {
946            throw new IllegalStateException("Unexpected call to ConversationProvider.delete");
947            //deleteLocal(uri);
948           // return ProviderExecute.opDelete(uri);
949        }
950
951        @Override
952        public String getType(Uri uri) {
953            return null;
954        }
955
956        /**
957         * Quick and dirty class that executes underlying provider CRUD operations on a background
958         * thread.
959         */
960        static class ProviderExecute implements Runnable {
961            static final int DELETE = 0;
962            static final int INSERT = 1;
963            static final int UPDATE = 2;
964
965            final int mCode;
966            final Uri mUri;
967            final ContentValues mValues; //HEHEH
968
969            ProviderExecute(int code, Uri uri, ContentValues values) {
970                mCode = code;
971                mUri = uriFromCachingUri(uri);
972                mValues = values;
973            }
974
975            ProviderExecute(int code, Uri uri) {
976                this(code, uri, null);
977            }
978
979            static Uri opInsert(Uri uri, ContentValues values) {
980                ProviderExecute e = new ProviderExecute(INSERT, uri, values);
981                if (offUiThread()) return (Uri)e.go();
982                new Thread(e).start();
983                return null;
984            }
985
986            static int opDelete(Uri uri) {
987                ProviderExecute e = new ProviderExecute(DELETE, uri);
988                if (offUiThread()) return (Integer)e.go();
989                new Thread(new ProviderExecute(DELETE, uri)).start();
990                return 0;
991            }
992
993            static int opUpdate(Uri uri, ContentValues values) {
994                ProviderExecute e = new ProviderExecute(UPDATE, uri, values);
995                if (offUiThread()) return (Integer)e.go();
996                new Thread(e).start();
997                return 0;
998            }
999
1000            @Override
1001            public void run() {
1002                go();
1003            }
1004
1005            public Object go() {
1006                switch(mCode) {
1007                    case DELETE:
1008                        return sResolver.delete(mUri, null, null);
1009                    case INSERT:
1010                        return sResolver.insert(mUri, mValues);
1011                    case UPDATE:
1012                        return sResolver.update(mUri,  mValues, null, null);
1013                    default:
1014                        return null;
1015                }
1016            }
1017        }
1018
1019        private void insertLocal(Uri uri, ContentValues values) {
1020            // Placeholder for now; there's no local insert
1021        }
1022
1023        private int mUndoSequence = 0;
1024        private ArrayList<Uri> mUndoDeleteUris = new ArrayList<Uri>();
1025
1026        void addToUndoSequence(Uri uri) {
1027            if (sSequence != mUndoSequence) {
1028                mUndoSequence = sSequence;
1029                mUndoDeleteUris.clear();
1030            }
1031            mUndoDeleteUris.add(uri);
1032        }
1033
1034        @VisibleForTesting
1035        void deleteLocal(Uri uri, ConversationCursor conversationCursor) {
1036            String uriString = uriStringFromCachingUri(uri);
1037            conversationCursor.cacheValue(uriString, DELETED_COLUMN, true);
1038            addToUndoSequence(uri);
1039        }
1040
1041        @VisibleForTesting
1042        void undeleteLocal(Uri uri, ConversationCursor conversationCursor) {
1043            String uriString = uriStringFromCachingUri(uri);
1044            conversationCursor.cacheValue(uriString, DELETED_COLUMN, false);
1045        }
1046
1047        void setMostlyDead(Conversation conv, ConversationCursor conversationCursor) {
1048            Uri uri = conv.uri;
1049            String uriString = uriStringFromCachingUri(uri);
1050            conversationCursor.setMostlyDead(uriString, conv);
1051            addToUndoSequence(uri);
1052        }
1053
1054        void commitMostlyDead(Conversation conv, ConversationCursor conversationCursor) {
1055            conversationCursor.commitMostlyDead(conv);
1056        }
1057
1058        boolean clearMostlyDead(Uri uri, ConversationCursor conversationCursor) {
1059            String uriString =  uriStringFromCachingUri(uri);
1060            return conversationCursor.clearMostlyDead(uriString);
1061        }
1062
1063        public void undo(ConversationCursor conversationCursor) {
1064            if (sSequence == mUndoSequence) {
1065                for (Uri uri: mUndoDeleteUris) {
1066                    if (!clearMostlyDead(uri, conversationCursor)) {
1067                        undeleteLocal(uri, conversationCursor);
1068                    }
1069                }
1070                mUndoSequence = 0;
1071                conversationCursor.recalibratePosition();
1072            }
1073        }
1074
1075        @VisibleForTesting
1076        void updateLocal(Uri uri, ContentValues values, ConversationCursor conversationCursor) {
1077            if (values == null) {
1078                return;
1079            }
1080            String uriString = uriStringFromCachingUri(uri);
1081            for (String columnName: values.keySet()) {
1082                conversationCursor.cacheValue(uriString, columnName, values.get(columnName));
1083            }
1084        }
1085
1086        public int apply(ArrayList<ConversationOperation> ops,
1087                ConversationCursor conversationCursor) {
1088            final HashMap<String, ArrayList<ContentProviderOperation>> batchMap =
1089                    new HashMap<String, ArrayList<ContentProviderOperation>>();
1090            // Increment sequence count
1091            sSequence++;
1092
1093            // Execute locally and build CPO's for underlying provider
1094            boolean recalibrateRequired = false;
1095            for (ConversationOperation op: ops) {
1096                Uri underlyingUri = uriFromCachingUri(op.mUri);
1097                String authority = underlyingUri.getAuthority();
1098                ArrayList<ContentProviderOperation> authOps = batchMap.get(authority);
1099                if (authOps == null) {
1100                    authOps = new ArrayList<ContentProviderOperation>();
1101                    batchMap.put(authority, authOps);
1102                }
1103                ContentProviderOperation cpo = op.execute(underlyingUri);
1104                if (cpo != null) {
1105                    authOps.add(cpo);
1106                }
1107                // Keep track of whether our operations require recalibrating the cursor position
1108                if (op.mRecalibrateRequired) {
1109                    recalibrateRequired = true;
1110                }
1111            }
1112
1113            // Recalibrate cursor position if required
1114            if (recalibrateRequired) {
1115                conversationCursor.recalibratePosition();
1116            }
1117
1118            // Notify listeners that data has changed
1119            conversationCursor.notifyDataChanged();
1120
1121            // Send changes to underlying provider
1122            for (String authority: batchMap.keySet()) {
1123                try {
1124                    if (offUiThread()) {
1125                        sResolver.applyBatch(authority, batchMap.get(authority));
1126                    } else {
1127                        final String auth = authority;
1128                        new Thread(new Runnable() {
1129                            @Override
1130                            public void run() {
1131                                try {
1132                                    sResolver.applyBatch(auth, batchMap.get(auth));
1133                                } catch (RemoteException e) {
1134                                } catch (OperationApplicationException e) {
1135                                }
1136                           }
1137                        }).start();
1138                    }
1139                } catch (RemoteException e) {
1140                } catch (OperationApplicationException e) {
1141                }
1142            }
1143            return sSequence;
1144        }
1145    }
1146
1147    void setMostlyDead(String uriString, Conversation conv) {
1148        LogUtils.i(TAG, "[Mostly dead, deferring: %s] ", uriString);
1149        cacheValue(uriString,
1150                UIProvider.ConversationColumns.FLAGS, Conversation.FLAG_MOSTLY_DEAD);
1151        conv.convFlags |= Conversation.FLAG_MOSTLY_DEAD;
1152        sMostlyDead.add(conv);
1153        mDeferSync = true;
1154    }
1155
1156    void commitMostlyDead(Conversation conv) {
1157        conv.convFlags &= ~Conversation.FLAG_MOSTLY_DEAD;
1158        sMostlyDead.remove(conv);
1159        LogUtils.i(TAG, "[All dead: %s]", conv.uri);
1160        if (sMostlyDead.isEmpty()) {
1161            mDeferSync = false;
1162            checkNotifyUI();
1163        }
1164    }
1165
1166    boolean clearMostlyDead(String uriString) {
1167        Object val = getCachedValue(uriString,
1168                UIProvider.CONVERSATION_FLAGS_COLUMN);
1169        if (val != null) {
1170            int flags = ((Integer)val).intValue();
1171            if ((flags & Conversation.FLAG_MOSTLY_DEAD) != 0) {
1172                cacheValue(uriString, UIProvider.ConversationColumns.FLAGS,
1173                        flags &= ~Conversation.FLAG_MOSTLY_DEAD);
1174                return true;
1175            }
1176        }
1177        return false;
1178    }
1179
1180
1181
1182
1183    /**
1184     * ConversationOperation is the encapsulation of a ContentProvider operation to be performed
1185     * atomically as part of a "batch" operation.
1186     */
1187    public class ConversationOperation {
1188        private static final int MOSTLY = 0x80;
1189        public static final int DELETE = 0;
1190        public static final int INSERT = 1;
1191        public static final int UPDATE = 2;
1192        public static final int ARCHIVE = 3;
1193        public static final int MUTE = 4;
1194        public static final int REPORT_SPAM = 5;
1195        public static final int REPORT_NOT_SPAM = 6;
1196        public static final int REPORT_PHISHING = 7;
1197        public static final int MOSTLY_ARCHIVE = MOSTLY | ARCHIVE;
1198        public static final int MOSTLY_DELETE = MOSTLY | DELETE;
1199        public static final int MOSTLY_DESTRUCTIVE_UPDATE = MOSTLY | UPDATE;
1200
1201        private final int mType;
1202        private final Uri mUri;
1203        private final Conversation mConversation;
1204        private final ContentValues mValues;
1205        // True if an updated item should be removed locally (from ConversationCursor)
1206        // This would be the case for a folder change in which the conversation is no longer
1207        // in the folder represented by the ConversationCursor
1208        private final boolean mLocalDeleteOnUpdate;
1209        // After execution, this indicates whether or not the operation requires recalibration of
1210        // the current cursor position (i.e. it removed or added items locally)
1211        private boolean mRecalibrateRequired = true;
1212        // Whether this item is already mostly dead
1213        private final boolean mMostlyDead;
1214
1215        /**
1216         * Set to true to immediately notify any {@link DataSetObserver}s watching the global
1217         * {@link ConversationCursor} upon applying the change to the data cache. You would not
1218         * want to do this if a change you make is being handled specially, like an animated delete.
1219         *
1220         * TODO: move this to the application Controller, or whoever has a canonical reference
1221         * to a {@link ConversationCursor} to notify on.
1222         */
1223        private final boolean mAutoNotify;
1224
1225        public ConversationOperation(int type, Conversation conv) {
1226            this(type, conv, null, false /* autoNotify */);
1227        }
1228
1229        public ConversationOperation(int type, Conversation conv, ContentValues values,
1230                boolean autoNotify) {
1231            mType = type;
1232            mUri = conv.uri;
1233            mConversation = conv;
1234            mValues = values;
1235            mLocalDeleteOnUpdate = conv.localDeleteOnUpdate;
1236            mAutoNotify = autoNotify;
1237            mMostlyDead = conv.isMostlyDead();
1238        }
1239
1240        private ContentProviderOperation execute(Uri underlyingUri) {
1241            Uri uri = underlyingUri.buildUpon()
1242                    .appendQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER,
1243                            Integer.toString(sSequence))
1244                    .build();
1245            ContentProviderOperation op = null;
1246            switch(mType) {
1247                case UPDATE:
1248                    if (mLocalDeleteOnUpdate) {
1249                        sProvider.deleteLocal(mUri, ConversationCursor.this);
1250                    } else {
1251                        sProvider.updateLocal(mUri, mValues, ConversationCursor.this);
1252                        mRecalibrateRequired = false;
1253                    }
1254                    if (!mMostlyDead) {
1255                        op = ContentProviderOperation.newUpdate(uri)
1256                                .withValues(mValues)
1257                                .build();
1258                    } else {
1259                        sProvider.commitMostlyDead(mConversation, ConversationCursor.this);
1260                    }
1261                    break;
1262                case MOSTLY_DESTRUCTIVE_UPDATE:
1263                    sProvider.setMostlyDead(mConversation, ConversationCursor.this);
1264                    op = ContentProviderOperation.newUpdate(uri).withValues(mValues).build();
1265                    break;
1266                case INSERT:
1267                    sProvider.insertLocal(mUri, mValues);
1268                    op = ContentProviderOperation.newInsert(uri)
1269                            .withValues(mValues).build();
1270                    break;
1271                // Destructive actions below!
1272                // "Mostly" operations are reflected globally, but not locally, except to set
1273                // FLAG_MOSTLY_DEAD in the conversation itself
1274                case DELETE:
1275                    sProvider.deleteLocal(mUri, ConversationCursor.this);
1276                    if (!mMostlyDead) {
1277                        op = ContentProviderOperation.newDelete(uri).build();
1278                    } else {
1279                        sProvider.commitMostlyDead(mConversation, ConversationCursor.this);
1280                    }
1281                    break;
1282                case MOSTLY_DELETE:
1283                    sProvider.setMostlyDead(mConversation,ConversationCursor.this);
1284                    op = ContentProviderOperation.newDelete(uri).build();
1285                    break;
1286                case ARCHIVE:
1287                    sProvider.deleteLocal(mUri, ConversationCursor.this);
1288                    if (!mMostlyDead) {
1289                        // Create an update operation that represents archive
1290                        op = ContentProviderOperation.newUpdate(uri).withValue(
1291                                ConversationOperations.OPERATION_KEY,
1292                                ConversationOperations.ARCHIVE)
1293                                .build();
1294                    } else {
1295                        sProvider.commitMostlyDead(mConversation, ConversationCursor.this);
1296                    }
1297                    break;
1298                case MOSTLY_ARCHIVE:
1299                    sProvider.setMostlyDead(mConversation, ConversationCursor.this);
1300                    // Create an update operation that represents archive
1301                    op = ContentProviderOperation.newUpdate(uri).withValue(
1302                            ConversationOperations.OPERATION_KEY, ConversationOperations.ARCHIVE)
1303                            .build();
1304                    break;
1305                case MUTE:
1306                    if (mLocalDeleteOnUpdate) {
1307                        sProvider.deleteLocal(mUri, ConversationCursor.this);
1308                    }
1309
1310                    // Create an update operation that represents mute
1311                    op = ContentProviderOperation.newUpdate(uri).withValue(
1312                            ConversationOperations.OPERATION_KEY, ConversationOperations.MUTE)
1313                            .build();
1314                    break;
1315                case REPORT_SPAM:
1316                case REPORT_NOT_SPAM:
1317                    sProvider.deleteLocal(mUri, ConversationCursor.this);
1318
1319                    final String operation = mType == REPORT_SPAM ?
1320                            ConversationOperations.REPORT_SPAM :
1321                            ConversationOperations.REPORT_NOT_SPAM;
1322
1323                    // Create an update operation that represents report spam
1324                    op = ContentProviderOperation.newUpdate(uri).withValue(
1325                            ConversationOperations.OPERATION_KEY, operation).build();
1326                    break;
1327                case REPORT_PHISHING:
1328                    sProvider.deleteLocal(mUri, ConversationCursor.this);
1329
1330                    // Create an update operation that represents report spam
1331                    op = ContentProviderOperation.newUpdate(uri).withValue(
1332                            ConversationOperations.OPERATION_KEY,
1333                            ConversationOperations.REPORT_PHISHING).build();
1334                    break;
1335                default:
1336                    throw new UnsupportedOperationException(
1337                            "No such ConversationOperation type: " + mType);
1338            }
1339
1340            // FIXME: this is a hack to notify conversation list of changes from conversation view.
1341            // The proper way to do this is to have the Controller handle the 'mark read' action.
1342            // It has a reference to this ConversationCursor so it can notify without using global
1343            // magic.
1344            if (mAutoNotify) {
1345                notifyDataSetChanged();
1346            }
1347
1348            return op;
1349        }
1350    }
1351
1352    /**
1353     * For now, a single listener can be associated with the cursor, and for now we'll just
1354     * notify on deletions
1355     */
1356    public interface ConversationListener {
1357        /**
1358         * Data in the underlying provider has changed; a refresh is required to sync up
1359         */
1360        public void onRefreshRequired();
1361        /**
1362         * We've completed a requested refresh of the underlying cursor
1363         */
1364        public void onRefreshReady();
1365        /**
1366         * The data underlying the cursor has changed; the UI should redraw the list
1367         */
1368        public void onDataSetChanged();
1369    }
1370
1371    @Override
1372    public boolean isFirst() {
1373        throw new UnsupportedOperationException();
1374    }
1375
1376    @Override
1377    public boolean isLast() {
1378        throw new UnsupportedOperationException();
1379    }
1380
1381    @Override
1382    public boolean isBeforeFirst() {
1383        throw new UnsupportedOperationException();
1384    }
1385
1386    @Override
1387    public boolean isAfterLast() {
1388        throw new UnsupportedOperationException();
1389    }
1390
1391    @Override
1392    public int getColumnIndex(String columnName) {
1393        return mUnderlyingCursor.getColumnIndex(columnName);
1394    }
1395
1396    @Override
1397    public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
1398        return mUnderlyingCursor.getColumnIndexOrThrow(columnName);
1399    }
1400
1401    @Override
1402    public String getColumnName(int columnIndex) {
1403        return mUnderlyingCursor.getColumnName(columnIndex);
1404    }
1405
1406    @Override
1407    public String[] getColumnNames() {
1408        return mUnderlyingCursor.getColumnNames();
1409    }
1410
1411    @Override
1412    public int getColumnCount() {
1413        return mUnderlyingCursor.getColumnCount();
1414    }
1415
1416    @Override
1417    public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
1418        throw new UnsupportedOperationException();
1419    }
1420
1421    @Override
1422    public int getType(int columnIndex) {
1423        return mUnderlyingCursor.getType(columnIndex);
1424    }
1425
1426    @Override
1427    public boolean isNull(int columnIndex) {
1428        throw new UnsupportedOperationException();
1429    }
1430
1431    @Override
1432    public void deactivate() {
1433        throw new UnsupportedOperationException();
1434    }
1435
1436    @Override
1437    public boolean isClosed() {
1438        return mUnderlyingCursor == null || mUnderlyingCursor.isClosed();
1439    }
1440
1441    @Override
1442    public void registerContentObserver(ContentObserver observer) {
1443        // Nope. We never notify of underlying changes on this channel, since the cursor watches
1444        // internally and offers onRefreshRequired/onRefreshReady to accomplish the same thing.
1445    }
1446
1447    @Override
1448    public void unregisterContentObserver(ContentObserver observer) {
1449        // See above.
1450    }
1451
1452    @Override
1453    public void registerDataSetObserver(DataSetObserver observer) {
1454        mDataSetObservable.registerObserver(observer);
1455    }
1456
1457    @Override
1458    public void unregisterDataSetObserver(DataSetObserver observer) {
1459        mDataSetObservable.unregisterObserver(observer);
1460    }
1461
1462    public void notifyDataSetChanged() {
1463        mDataSetObservable.notifyChanged();
1464    }
1465
1466    @Override
1467    public void setNotificationUri(ContentResolver cr, Uri uri) {
1468        throw new UnsupportedOperationException();
1469    }
1470
1471    @Override
1472    public boolean getWantsAllOnMoveCalls() {
1473        throw new UnsupportedOperationException();
1474    }
1475
1476    @Override
1477    public Bundle getExtras() {
1478        throw new UnsupportedOperationException();
1479    }
1480
1481    @Override
1482    public Bundle respond(Bundle extras) {
1483        if (mUnderlyingCursor != null) {
1484            return mUnderlyingCursor.respond(extras);
1485        }
1486        return Bundle.EMPTY;
1487    }
1488
1489    @Override
1490    public boolean requery() {
1491        return true;
1492    }
1493
1494    // Below are methods that update Conversation data (update/delete)
1495
1496    public int updateBoolean(Context context, Conversation conversation, String columnName,
1497            boolean value) {
1498        return updateBoolean(context, Arrays.asList(conversation), columnName, value);
1499    }
1500
1501    /**
1502     * Update an integer column for a group of conversations (see updateValues below)
1503     */
1504    public int updateInt(Context context, Collection<Conversation> conversations,
1505            String columnName, int value) {
1506        ContentValues cv = new ContentValues();
1507        cv.put(columnName, value);
1508        return updateValues(context, conversations, cv);
1509    }
1510
1511    /**
1512     * Update a string column for a group of conversations (see updateValues below)
1513     */
1514    public int updateBoolean(Context context, Collection<Conversation> conversations,
1515            String columnName, boolean value) {
1516        ContentValues cv = new ContentValues();
1517        cv.put(columnName, value);
1518        return updateValues(context, conversations, cv);
1519    }
1520
1521    /**
1522     * Update a string column for a group of conversations (see updateValues below)
1523     */
1524    public int updateString(Context context, Collection<Conversation> conversations,
1525            String columnName, String value) {
1526        return updateStrings(context, conversations, new String[] {
1527            columnName
1528        }, new String[] {
1529            value
1530        });
1531    }
1532
1533    /**
1534     * Update a string columns for a group of conversations (see updateValues below)
1535     */
1536    public int updateStrings(Context context, Collection<Conversation> conversations,
1537            String[] columnNames, String[] values) {
1538        ContentValues cv = new ContentValues();
1539        for (int i = 0; i < columnNames.length; i++) {
1540            cv.put(columnNames[i], values[i]);
1541        }
1542        return updateValues(context, conversations, cv);
1543    }
1544
1545    public int updateBoolean(Context context, String conversationUri, String columnName,
1546            boolean value) {
1547        Conversation conv = new Conversation();
1548        conv.uri = Uri.parse(conversationUri);
1549        return updateBoolean(context, conv, columnName, value);
1550    }
1551
1552    /**
1553     * Update a boolean column for a group of conversations, immediately in the UI and in a single
1554     * transaction in the underlying provider
1555     * @param conversations a collection of conversations
1556     * @param context the caller's context
1557     * @param columnName the column to update
1558     * @param value the new value
1559     * @return the sequence number of the operation (for undo)
1560     */
1561    private int updateValues(Context context, Collection<Conversation> conversations,
1562            ContentValues values) {
1563        return apply(context,
1564                getOperationsForConversations(conversations, ConversationOperation.UPDATE, values));
1565    }
1566
1567    private ArrayList<ConversationOperation> getOperationsForConversations(
1568            Collection<Conversation> conversations, int op, ContentValues values) {
1569        return getOperationsForConversations(conversations, op, values, false /* autoNotify */);
1570    }
1571
1572    private ArrayList<ConversationOperation> getOperationsForConversations(
1573            Collection<Conversation> conversations, int type, ContentValues values,
1574            boolean autoNotify) {
1575        final ArrayList<ConversationOperation> ops = Lists.newArrayList();
1576        for (Conversation conv: conversations) {
1577            ConversationOperation op = new ConversationOperation(type, conv, values, autoNotify);
1578            ops.add(op);
1579        }
1580        return ops;
1581    }
1582
1583    /**
1584     * Delete a single conversation
1585     * @param context the caller's context
1586     * @return the sequence number of the operation (for undo)
1587     */
1588    public int delete(Context context, Conversation conversation) {
1589        ArrayList<Conversation> conversations = new ArrayList<Conversation>();
1590        conversations.add(conversation);
1591        return delete(context, conversations);
1592    }
1593
1594    /**
1595     * Delete a single conversation
1596     * @param context the caller's context
1597     * @return the sequence number of the operation (for undo)
1598     */
1599    public int mostlyArchive(Context context, Conversation conversation) {
1600        ArrayList<Conversation> conversations = new ArrayList<Conversation>();
1601        conversations.add(conversation);
1602        return archive(context, conversations);
1603    }
1604
1605    /**
1606     * Delete a single conversation
1607     * @param context the caller's context
1608     * @return the sequence number of the operation (for undo)
1609     */
1610    public int mostlyDelete(Context context, Conversation conversation) {
1611        ArrayList<Conversation> conversations = new ArrayList<Conversation>();
1612        conversations.add(conversation);
1613        return delete(context, conversations);
1614    }
1615
1616    /**
1617     * Mark a single conversation read/unread.
1618     * @param context the caller's context
1619     * @param read true for read, false for unread
1620     * @return the sequence number of the operation (for undo)
1621     */
1622    public int markRead(Context context, boolean read, Conversation conversation) {
1623        ContentValues values = new ContentValues();
1624        values.put(ConversationColumns.READ, read);
1625        ArrayList<Conversation> conversations = new ArrayList<Conversation>();
1626        conversations.add(conversation);
1627        return apply(
1628                context,
1629                getOperationsForConversations(conversations, ConversationOperation.UPDATE,
1630                        values, true /* autoNotify */));
1631    }
1632
1633    // Convenience methods
1634    private int apply(Context context, ArrayList<ConversationOperation> operations) {
1635        return sProvider.apply(operations, this);
1636    }
1637
1638    private void undoLocal() {
1639        sProvider.undo(this);
1640    }
1641
1642    public void undo(final Context context, final Uri undoUri) {
1643        new Thread(new Runnable() {
1644            @Override
1645            public void run() {
1646                Cursor c = context.getContentResolver().query(undoUri, UIProvider.UNDO_PROJECTION,
1647                        null, null, null);
1648                if (c != null) {
1649                    c.close();
1650                }
1651            }
1652        }).start();
1653        undoLocal();
1654    }
1655
1656    /**
1657     * Delete a group of conversations immediately in the UI and in a single transaction in the
1658     * underlying provider. See applyAction for argument descriptions
1659     */
1660    public int delete(Context context, Collection<Conversation> conversations) {
1661        return applyAction(context, conversations, ConversationOperation.DELETE);
1662    }
1663
1664    /**
1665     * As above, for archive
1666     */
1667    public int archive(Context context, Collection<Conversation> conversations) {
1668        return applyAction(context, conversations, ConversationOperation.ARCHIVE);
1669    }
1670
1671    /**
1672     * As above, for mute
1673     */
1674    public int mute(Context context, Collection<Conversation> conversations) {
1675        return applyAction(context, conversations, ConversationOperation.MUTE);
1676    }
1677
1678    /**
1679     * As above, for report spam
1680     */
1681    public int reportSpam(Context context, Collection<Conversation> conversations) {
1682        return applyAction(context, conversations, ConversationOperation.REPORT_SPAM);
1683    }
1684
1685    /**
1686     * As above, for report not spam
1687     */
1688    public int reportNotSpam(Context context, Collection<Conversation> conversations) {
1689        return applyAction(context, conversations, ConversationOperation.REPORT_NOT_SPAM);
1690    }
1691
1692    /**
1693     * As above, for report phishing
1694     */
1695    public int reportPhishing(Context context, Collection<Conversation> conversations) {
1696        return applyAction(context, conversations, ConversationOperation.REPORT_PHISHING);
1697    }
1698
1699    /**
1700     * As above, for mostly archive
1701     */
1702    public int mostlyArchive(Context context, Collection<Conversation> conversations) {
1703        return applyAction(context, conversations, ConversationOperation.MOSTLY_ARCHIVE);
1704    }
1705
1706    /**
1707     * As above, for mostly delete
1708     */
1709    public int mostlyDelete(Context context, Collection<Conversation> conversations) {
1710        return applyAction(context, conversations, ConversationOperation.MOSTLY_DELETE);
1711    }
1712
1713    /**
1714     * As above, for mostly destructive updates
1715     */
1716    public int mostlyDestructiveUpdate(Context context, Collection<Conversation> conversations,
1717            String column, String value) {
1718        return mostlyDestructiveUpdate(context, conversations, new String[] {
1719            column
1720        }, new String[] {
1721            value
1722        });
1723    }
1724
1725    /**
1726     * As above, for mostly destructive updates.
1727     */
1728    public int mostlyDestructiveUpdate(Context context, Collection<Conversation> conversations,
1729            String[] columnNames, String[] values) {
1730        ContentValues cv = new ContentValues();
1731        for (int i = 0; i < columnNames.length; i++) {
1732            cv.put(columnNames[i], values[i]);
1733        }
1734        return apply(
1735                context,
1736                getOperationsForConversations(conversations,
1737                        ConversationOperation.MOSTLY_DESTRUCTIVE_UPDATE, cv));
1738    }
1739
1740    /**
1741     * Convenience method for performing an operation on a group of conversations
1742     * @param context the caller's context
1743     * @param conversations the conversations to be affected
1744     * @param opAction the action to take
1745     * @return the sequence number of the operation applied in CC
1746     */
1747    private int applyAction(Context context, Collection<Conversation> conversations,
1748            int opAction) {
1749        ArrayList<ConversationOperation> ops = Lists.newArrayList();
1750        for (Conversation conv: conversations) {
1751            ConversationOperation op =
1752                    new ConversationOperation(opAction, conv);
1753            ops.add(op);
1754        }
1755        return apply(context, ops);
1756    }
1757
1758}