ConversationCursor.java revision 334e64af904085984cdcbecbcbc18cf488a9ceae
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.OperationApplicationException;
26import android.database.CharArrayBuffer;
27import android.database.ContentObserver;
28import android.database.Cursor;
29import android.database.DataSetObserver;
30import android.net.Uri;
31import android.os.Bundle;
32import android.os.Looper;
33import android.os.RemoteException;
34import android.util.Log;
35
36import com.android.mail.providers.Conversation;
37import com.android.mail.providers.UIProvider;
38import com.android.mail.providers.UIProvider.ConversationOperations;
39import com.google.common.annotations.VisibleForTesting;
40
41import java.util.ArrayList;
42import java.util.HashMap;
43import java.util.Iterator;
44import java.util.List;
45
46/**
47 * ConversationCursor is a wrapper around a conversation list cursor that provides update/delete
48 * caching for quick UI response. This is effectively a singleton class, as the cache is
49 * implemented as a static HashMap.
50 */
51public final class ConversationCursor implements Cursor {
52    private static final String TAG = "ConversationCursor";
53    private static final boolean DEBUG = true;  // STOPSHIP Set to false before shipping
54
55    // The cursor instantiator's activity
56    private static Activity sActivity;
57    // The cursor underlying the caching cursor
58    @VisibleForTesting
59    static Cursor sUnderlyingCursor;
60    // The new cursor obtained via a requery
61    private static Cursor sRequeryCursor;
62    // A mapping from Uri to updated ContentValues
63    private static HashMap<String, ContentValues> sCacheMap = new HashMap<String, ContentValues>();
64    // Cache map lock (will be used only very briefly - few ms at most)
65    private static Object sCacheMapLock = new Object();
66    // A deleted row is indicated by the presence of DELETED_COLUMN in the cache map
67    private static final String DELETED_COLUMN = "__deleted__";
68    // An row cached during a requery is indicated by the presence of REQUERY_COLUMN in the map
69    private static final String REQUERY_COLUMN = "__requery__";
70    // A sentinel value for the "index" of the deleted column; it's an int that is otherwise invalid
71    private static final int DELETED_COLUMN_INDEX = -1;
72    // The current conversation cursor
73    private static ConversationCursor sConversationCursor;
74    // The index of the Uri whose data is reflected in the cached row
75    // Updates/Deletes to this Uri are cached
76    private static int sUriColumnIndex;
77    // The listeners registered for this cursor
78    private static ArrayList<ConversationListener> sListeners =
79        new ArrayList<ConversationListener>();
80    // The ConversationProvider instance
81    @VisibleForTesting
82    static ConversationProvider sProvider;
83    // Set when we're in the middle of a refresh of the underlying cursor
84    private static boolean sRefreshInProgress = false;
85    // Set when we've sent refreshReady() to listeners
86    private static boolean sRefreshReady = false;
87    // Set when we've sent refreshRequired() to listeners
88    private static boolean sRefreshRequired = false;
89    // Our sequence count (for changes sent to underlying provider)
90    private static int sSequence = 0;
91
92    // Column names for this cursor
93    private final String[] mColumnNames;
94    // The resolver for the cursor instantiator's context
95    private static ContentResolver mResolver;
96    // An observer on the underlying cursor (so we can detect changes from outside the UI)
97    private final CursorObserver mCursorObserver;
98    // Whether our observer is currently registered with the underlying cursor
99    private boolean mCursorObserverRegistered = false;
100
101    // The current position of the cursor
102    private int mPosition = -1;
103    // The number of cached deletions from this cursor (used to quickly generate an accurate count)
104    private static int sDeletedCount = 0;
105
106    // Parameters passed to the underlying query
107    private static Uri qUri;
108    private static String[] qProjection;
109    private static String qSelection;
110    private static String[] qSelectionArgs;
111    private static String qSortOrder;
112
113    private ConversationCursor(Cursor cursor, Activity activity, String messageListColumn) {
114        sActivity = activity;
115        mResolver = activity.getContentResolver();
116        sConversationCursor = this;
117        sUnderlyingCursor = cursor;
118        sListeners.clear();
119        mCursorObserver = new CursorObserver();
120        resetCursor(null);
121        mColumnNames = cursor.getColumnNames();
122        sUriColumnIndex = cursor.getColumnIndex(messageListColumn);
123        if (sUriColumnIndex < 0) {
124            throw new IllegalArgumentException("Cursor must include a message list column");
125        }
126    }
127
128    /**
129     * Create a ConversationCursor; this should be called by the ListActivity using that cursor
130     * @param activity the activity creating the cursor
131     * @param messageListColumn the column used for individual cursor items
132     * @param uri the query uri
133     * @param projection the query projecion
134     * @param selection the query selection
135     * @param selectionArgs the query selection args
136     * @param sortOrder the query sort order
137     * @return a ConversationCursor
138     */
139    public static ConversationCursor create(Activity activity, String messageListColumn, Uri uri,
140            String[] projection, String selection, String[] selectionArgs, String sortOrder) {
141        qUri = uri;
142        qProjection = projection;
143        qSelection = selection;
144        qSelectionArgs = selectionArgs;
145        qSortOrder = sortOrder;
146        Cursor cursor = activity.getContentResolver().query(uri, projection, selection,
147                selectionArgs, sortOrder);
148        return new ConversationCursor(cursor, activity, messageListColumn);
149    }
150
151    /**
152     * Return whether the uri string (message list uri) is in the underlying cursor
153     * @param uriString the uri string we're looking for
154     * @return true if the uri string is in the cursor; false otherwise
155     */
156    private boolean isInUnderlyingCursor(String uriString) {
157        sUnderlyingCursor.moveToPosition(-1);
158        while (sUnderlyingCursor.moveToNext()) {
159            if (uriString.equals(sUnderlyingCursor.getString(sUriColumnIndex))) {
160                return true;
161            }
162        }
163        return false;
164    }
165
166    /**
167     * Reset the cursor; this involves clearing out our cache map and resetting our various counts
168     * The cursor should be reset whenever we get fresh data from the underlying cursor. The cache
169     * is locked during the reset, which will block the UI, but for only a very short time
170     * (estimated at a few ms, but we can profile this; remember that the cache will usually
171     * be empty or have a few entries)
172     */
173    private void resetCursor(Cursor newCursor) {
174        // Temporary, log time for reset
175        long startTime = System.currentTimeMillis();
176        if (DEBUG) {
177            Log.d(TAG, "[--resetCursor--]");
178        }
179        synchronized (sCacheMapLock) {
180            // Walk through the cache.  Here are the cases:
181            // 1) The entry isn't marked with REQUERY - remove it from the cache. If DELETED is
182            //    set, decrement the deleted count
183            // 2) The REQUERY entry is still in the UP
184            //    2a) The REQUERY entry isn't DELETED; we're good, and the client change will remain
185            //    (i.e. client wins, it's on its way to the UP)
186            //    2b) The REQUERY entry is DELETED; we're good (client change remains, it's on
187            //        its way to the UP)
188            // 3) the REQUERY was deleted on the server (sheesh; this would be bizarre timing!) -
189            //    we need to throw the item out of the cache
190            // So ... the only interesting case is #3, we need to look for remaining deleted items
191            // and see if they're still in the UP
192            Iterator<HashMap.Entry<String, ContentValues>> iter = sCacheMap.entrySet().iterator();
193            while (iter.hasNext()) {
194                HashMap.Entry<String, ContentValues> entry = iter.next();
195                ContentValues values = entry.getValue();
196                if (values.containsKey(REQUERY_COLUMN) && isInUnderlyingCursor(entry.getKey())) {
197                    // If we're in a requery and we're still around, remove the requery key
198                    // We're good here, the cached change (delete/update) is on its way to UP
199                    values.remove(REQUERY_COLUMN);
200                } else {
201                    // Keep the deleted count up-to-date; remove the cache entry
202                    if (values.containsKey(DELETED_COLUMN)) {
203                        sDeletedCount--;
204                    }
205                    // Remove the entry
206                    iter.remove();
207                }
208            }
209
210            // Swap cursor
211            if (newCursor != null) {
212                close();
213                sUnderlyingCursor = newCursor;
214            }
215
216            mPosition = -1;
217            sUnderlyingCursor.moveToPosition(mPosition);
218            if (!mCursorObserverRegistered) {
219                sUnderlyingCursor.registerContentObserver(mCursorObserver);
220                mCursorObserverRegistered = true;
221            }
222            sRefreshRequired = false;
223        }
224        Log.d(TAG, "resetCache time: " + ((System.currentTimeMillis() - startTime)) + "ms");
225    }
226
227    /**
228     * Add a listener for this cursor; we'll notify it when our data changes
229     */
230    public void addListener(ConversationListener listener) {
231        sListeners.add(listener);
232    }
233
234    /**
235     * Remove a listener for this cursor
236     */
237    public void removeListener(ConversationListener listener) {
238        sListeners.remove(listener);
239    }
240
241    /**
242     * Generate a forwarding Uri to ConversationProvider from an original Uri.  We do this by
243     * changing the authority to ours, but otherwise leaving the Uri intact.
244     * NOTE: This won't handle query parameters, so the functionality will need to be added if
245     * parameters are used in the future
246     * @param uri the uri
247     * @return a forwarding uri to ConversationProvider
248     */
249    private static String uriToCachingUriString (Uri uri) {
250        String provider = uri.getAuthority();
251        return uri.getScheme() + "://" + ConversationProvider.AUTHORITY
252                + "/" + provider + uri.getPath();
253    }
254
255    /**
256     * Regenerate the original Uri from a forwarding (ConversationProvider) Uri
257     * NOTE: See note above for uriToCachingUri
258     * @param uri the forwarding Uri
259     * @return the original Uri
260     */
261    private static Uri uriFromCachingUri(Uri uri) {
262        List<String> path = uri.getPathSegments();
263        Uri.Builder builder = new Uri.Builder().scheme(uri.getScheme()).authority(path.get(0));
264        for (int i = 1; i < path.size(); i++) {
265            builder.appendPath(path.get(i));
266        }
267        return builder.build();
268    }
269
270    /**
271     * Cache a column name/value pair for a given Uri
272     * @param uriString the Uri for which the column name/value pair applies
273     * @param columnName the column name
274     * @param value the value to be cached
275     */
276    private static void cacheValue(String uriString, String columnName, Object value) {
277        synchronized (sCacheMapLock) {
278            // Get the map for our uri
279            ContentValues map = sCacheMap.get(uriString);
280            // Create one if necessary
281            if (map == null) {
282                map = new ContentValues();
283                sCacheMap.put(uriString, map);
284            }
285            // If we're caching a deletion, add to our count
286            if ((columnName == DELETED_COLUMN) && (map.get(columnName) == null)) {
287                sDeletedCount++;
288                if (DEBUG) {
289                    Log.d(TAG, "Deleted " + uriString);
290                }
291            }
292            // ContentValues has no generic "put", so we must test.  For now, the only classes of
293            // values implemented are Boolean/Integer/String, though others are trivially added
294            if (value instanceof Boolean) {
295                map.put(columnName, ((Boolean) value).booleanValue() ? 1 : 0);
296            } else if (value instanceof Integer) {
297                map.put(columnName, (Integer) value);
298            } else if (value instanceof String) {
299                map.put(columnName, (String) value);
300            } else {
301                String cname = value.getClass().getName();
302                throw new IllegalArgumentException("Value class not compatible with cache: "
303                        + cname);
304            }
305            if (sRefreshInProgress) {
306                map.put(REQUERY_COLUMN, 1);
307            }
308            if (DEBUG && (columnName != DELETED_COLUMN)) {
309                Log.d(TAG, "Caching value for " + uriString + ": " + columnName);
310            }
311        }
312    }
313
314    /**
315     * Get the cached value for the provided column; we special case -1 as the "deleted" column
316     * @param columnIndex the index of the column whose cached value we want to retrieve
317     * @return the cached value for this column, or null if there is none
318     */
319    private Object getCachedValue(int columnIndex) {
320        String uri = sUnderlyingCursor.getString(sUriColumnIndex);
321        ContentValues uriMap = sCacheMap.get(uri);
322        if (uriMap != null) {
323            String columnName;
324            if (columnIndex == DELETED_COLUMN_INDEX) {
325                columnName = DELETED_COLUMN;
326            } else {
327                columnName = mColumnNames[columnIndex];
328            }
329            return uriMap.get(columnName);
330        }
331        return null;
332    }
333
334    /**
335     * When the underlying cursor changes, we want to alert the listener
336     */
337    private void underlyingChanged() {
338        if (mCursorObserverRegistered) {
339            try {
340                sUnderlyingCursor.unregisterContentObserver(mCursorObserver);
341            } catch (IllegalStateException e) {
342                // Maybe the cursor was GC'd?
343            }
344            mCursorObserverRegistered = false;
345        }
346        if (DEBUG) {
347            Log.d(TAG, "[Notify: onRefreshRequired()]");
348        }
349        for (ConversationListener listener: sListeners) {
350            listener.onRefreshRequired();
351        }
352        sRefreshRequired = true;
353    }
354
355    /**
356     * Put the refreshed cursor in place (called by the UI)
357     */
358    // NOTE: We don't like the name (it implies syncing with the server); suggestions gladly
359    // taken - reset? syncToUnderlying? completeRefresh? align?
360    public void sync() {
361        if (DEBUG) {
362            Log.d(TAG, "[sync() called]");
363        }
364        if (sRequeryCursor == null) {
365            throw new IllegalStateException("Can't swap cursors; no requery done");
366        }
367        resetCursor(sRequeryCursor);
368        sRequeryCursor = null;
369        sRefreshInProgress = false;
370        sRefreshReady = false;
371    }
372
373    public boolean isRefreshRequired() {
374        return sRefreshRequired;
375    }
376
377    public boolean isRefreshReady() {
378        return sRefreshReady;
379    }
380
381    /**
382     * Cancel a refresh in progress
383     */
384    public void cancelRefresh() {
385        if (DEBUG) {
386            Log.d(TAG, "[cancelRefresh() called]");
387        }
388        synchronized(sCacheMapLock) {
389            // Mark the requery closed
390            sRefreshInProgress = false;
391            // If we have the cursor, close it; otherwise, it will get closed when the query
392            // finishes (it checks sRequeryInProgress)
393            if (sRequeryCursor != null) {
394                sRequeryCursor.close();
395                sRequeryCursor = null;
396            }
397        }
398    }
399
400    /**
401     * Get a list of deletions from ConversationCursor to the refreshed cursor that hasn't yet
402     * been swapped into place; this allows the UI to animate these away if desired
403     * @return a list of positions deleted in ConversationCursor
404     */
405    public ArrayList<Integer> getRefreshDeletions () {
406        if (DEBUG) {
407            Log.d(TAG, "[getRefreshDeletions() called]");
408        }
409        Cursor deviceCursor = sConversationCursor;
410        Cursor serverCursor = sRequeryCursor;
411        // TODO: (mindyp) saw some instability here. Adding an assert to try to
412        // catch it.
413        assert(sRequeryCursor != null);
414        ArrayList<Integer> deleteList = new ArrayList<Integer>();
415        int serverCount = serverCursor.getCount();
416        int deviceCount = deviceCursor.getCount();
417        deviceCursor.moveToFirst();
418        serverCursor.moveToFirst();
419        while (serverCount > 0 || deviceCount > 0) {
420            if (serverCount == 0) {
421                for (; deviceCount > 0; deviceCount--)
422                    deleteList.add(deviceCursor.getPosition());
423                break;
424            } else if (deviceCount == 0) {
425                break;
426            }
427            long deviceMs = deviceCursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN);
428            long serverMs = serverCursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN);
429            String deviceUri = deviceCursor.getString(UIProvider.CONVERSATION_URI_COLUMN);
430            String serverUri = serverCursor.getString(UIProvider.CONVERSATION_URI_COLUMN);
431            deviceCursor.moveToNext();
432            serverCursor.moveToNext();
433            serverCount--;
434            deviceCount--;
435            if (serverMs == deviceMs) {
436                // Check for duplicates here; if our identical dates refer to different messages,
437                // we'll just quit here for now (at worst, this will cause a non-animating delete)
438                // My guess is that this happens VERY rarely, if at all
439                if (!deviceUri.equals(serverUri)) {
440                    // To do this right, we'd find all of the rows with the same ms (date), etc...
441                    //return deleteList;
442                }
443                continue;
444            } else if (deviceMs > serverMs) {
445                deleteList.add(deviceCursor.getPosition() - 1);
446                // Move back because we've already advanced cursor (that's why we subtract 1 above)
447                serverCount++;
448                serverCursor.moveToPrevious();
449            } else if (serverMs > deviceMs) {
450                // If we wanted to track insertions, we'd so so here
451                // Move back because we've already advanced cursor
452                deviceCount++;
453                deviceCursor.moveToPrevious();
454            }
455        }
456        Log.d(TAG, "Deletions: " + deleteList);
457        return deleteList;
458    }
459
460    /**
461     * When we get a requery from the UI, we'll do it, but also clear the cache. The listener is
462     * notified when the requery is complete
463     * NOTE: This will have to change, of course, when we start using loaders...
464     */
465    public boolean refresh() {
466        if (DEBUG) {
467            Log.d(TAG, "[refresh() called]");
468        }
469        if (sRefreshInProgress) {
470            return false;
471        }
472        // Say we're starting a requery
473        sRefreshInProgress = true;
474        new Thread(new Runnable() {
475            @Override
476            public void run() {
477                // Get new data
478                sRequeryCursor =
479                        mResolver.query(qUri, qProjection, qSelection, qSelectionArgs, qSortOrder);
480                // Make sure window is full
481                synchronized(sCacheMapLock) {
482                    if (sRefreshInProgress) {
483                        sRequeryCursor.getCount();
484                        sActivity.runOnUiThread(new Runnable() {
485                            @Override
486                            public void run() {
487                                if (DEBUG) {
488                                    Log.d(TAG, "[Notify: onRefreshReady()]");
489                                }
490                                for (ConversationListener listener: sListeners) {
491                                    listener.onRefreshReady();
492                                }
493                                sRefreshReady = true;
494                            }});
495                    } else {
496                        cancelRefresh();
497                    }
498                }
499            }
500        }).start();
501        return true;
502    }
503
504    @Override
505    public void close() {
506        if (!sUnderlyingCursor.isClosed()) {
507            // Unregister our observer on the underlying cursor and close as usual
508            if (mCursorObserverRegistered) {
509                try {
510                    sUnderlyingCursor.unregisterContentObserver(mCursorObserver);
511                } catch (IllegalStateException e) {
512                    // Maybe the cursor got GC'd?
513                }
514                mCursorObserverRegistered = false;
515            }
516            sUnderlyingCursor.close();
517        }
518    }
519
520    /**
521     * Move to the next not-deleted item in the conversation
522     */
523    @Override
524    public boolean moveToNext() {
525        while (true) {
526            boolean ret = sUnderlyingCursor.moveToNext();
527            if (!ret) return false;
528            if (getCachedValue(DELETED_COLUMN_INDEX) instanceof Integer) continue;
529            mPosition++;
530            return true;
531        }
532    }
533
534    /**
535     * Move to the previous not-deleted item in the conversation
536     */
537    @Override
538    public boolean moveToPrevious() {
539        while (true) {
540            boolean ret = sUnderlyingCursor.moveToPrevious();
541            if (!ret) return false;
542            if (getCachedValue(-1) instanceof Integer) continue;
543            mPosition--;
544            return true;
545        }
546    }
547
548    @Override
549    public int getPosition() {
550        return mPosition;
551    }
552
553    /**
554     * The actual cursor's count must be decremented by the number we've deleted from the UI
555     */
556    @Override
557    public int getCount() {
558        return sUnderlyingCursor.getCount() - sDeletedCount;
559    }
560
561    @Override
562    public boolean moveToFirst() {
563        sUnderlyingCursor.moveToPosition(-1);
564        mPosition = -1;
565        return moveToNext();
566    }
567
568    @Override
569    public boolean moveToPosition(int pos) {
570        if (pos < -1 || pos >= getCount()) return false;
571        if (pos == mPosition) return true;
572        if (pos > mPosition) {
573            while (pos > mPosition) {
574                if (!moveToNext()) {
575                    return false;
576                }
577            }
578            return true;
579        } else if (pos == 0) {
580            return moveToFirst();
581        } else {
582            while (pos < mPosition) {
583                if (!moveToPrevious()) {
584                    return false;
585                }
586            }
587            return true;
588        }
589    }
590
591    @Override
592    public boolean moveToLast() {
593        throw new UnsupportedOperationException("moveToLast unsupported!");
594    }
595
596    @Override
597    public boolean move(int offset) {
598        throw new UnsupportedOperationException("move unsupported!");
599    }
600
601    /**
602     * We need to override all of the getters to make sure they look at cached values before using
603     * the values in the underlying cursor
604     */
605    @Override
606    public double getDouble(int columnIndex) {
607        Object obj = getCachedValue(columnIndex);
608        if (obj != null) return (Double)obj;
609        return sUnderlyingCursor.getDouble(columnIndex);
610    }
611
612    @Override
613    public float getFloat(int columnIndex) {
614        Object obj = getCachedValue(columnIndex);
615        if (obj != null) return (Float)obj;
616        return sUnderlyingCursor.getFloat(columnIndex);
617    }
618
619    @Override
620    public int getInt(int columnIndex) {
621        Object obj = getCachedValue(columnIndex);
622        if (obj != null) return (Integer)obj;
623        return sUnderlyingCursor.getInt(columnIndex);
624    }
625
626    @Override
627    public long getLong(int columnIndex) {
628        Object obj = getCachedValue(columnIndex);
629        if (obj != null) return (Long)obj;
630        return sUnderlyingCursor.getLong(columnIndex);
631    }
632
633    @Override
634    public short getShort(int columnIndex) {
635        Object obj = getCachedValue(columnIndex);
636        if (obj != null) return (Short)obj;
637        return sUnderlyingCursor.getShort(columnIndex);
638    }
639
640    @Override
641    public String getString(int columnIndex) {
642        // If we're asking for the Uri for the conversation list, we return a forwarding URI
643        // so that we can intercept update/delete and handle it ourselves
644        if (columnIndex == sUriColumnIndex) {
645            Uri uri = Uri.parse(sUnderlyingCursor.getString(columnIndex));
646            return uriToCachingUriString(uri);
647        }
648        Object obj = getCachedValue(columnIndex);
649        if (obj != null) return (String)obj;
650        return sUnderlyingCursor.getString(columnIndex);
651    }
652
653    @Override
654    public byte[] getBlob(int columnIndex) {
655        Object obj = getCachedValue(columnIndex);
656        if (obj != null) return (byte[])obj;
657        return sUnderlyingCursor.getBlob(columnIndex);
658    }
659
660    /**
661     * Observer of changes to underlying data
662     */
663    private class CursorObserver extends ContentObserver {
664        public CursorObserver() {
665            super(null);
666        }
667
668        @Override
669        public void onChange(boolean selfChange) {
670            // If we're here, then something outside of the UI has changed the data, and we
671            // must query the underlying provider for that data
672            if (DEBUG) {
673                Log.d(TAG, "Underlying conversation cursor changed; requerying");
674            }
675            // It's not at all obvious to me why we must unregister/re-register after the requery
676            // However, if we don't we'll only get one notification and no more...
677            ConversationCursor.this.underlyingChanged();
678        }
679    }
680
681    /**
682     * ConversationProvider is the ContentProvider for our forwarding Uri's; it passes queries
683     * and inserts directly, and caches updates/deletes before passing them through.  The caching
684     * will cause a redraw of the list with updated values.
685     */
686    public abstract static class ConversationProvider extends ContentProvider {
687        public static String AUTHORITY;
688
689        /**
690         * Allows the implmenting provider to specify the authority that should be used.
691         */
692        protected abstract String getAuthority();
693
694        @Override
695        public boolean onCreate() {
696            sProvider = this;
697            AUTHORITY = getAuthority();
698            return true;
699        }
700
701        @Override
702        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
703                String sortOrder) {
704            return mResolver.query(
705                    uriFromCachingUri(uri), projection, selection, selectionArgs, sortOrder);
706        }
707
708        @Override
709        public Uri insert(Uri uri, ContentValues values) {
710            insertLocal(uri, values);
711            return ProviderExecute.opInsert(uri, values);
712        }
713
714        @Override
715        public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
716            updateLocal(uri, values);
717            return ProviderExecute.opUpdate(uri, values);
718        }
719
720        @Override
721        public int delete(Uri uri, String selection, String[] selectionArgs) {
722            deleteLocal(uri);
723            return ProviderExecute.opDelete(uri);
724        }
725
726        @Override
727        public String getType(Uri uri) {
728            return null;
729        }
730
731        /**
732         * Quick and dirty class that executes underlying provider CRUD operations on a background
733         * thread.
734         */
735        static class ProviderExecute implements Runnable {
736            static final int DELETE = 0;
737            static final int INSERT = 1;
738            static final int UPDATE = 2;
739
740            final int mCode;
741            final Uri mUri;
742            final ContentValues mValues; //HEHEH
743
744            ProviderExecute(int code, Uri uri, ContentValues values) {
745                mCode = code;
746                mUri = uriFromCachingUri(uri);
747                mValues = values;
748            }
749
750            ProviderExecute(int code, Uri uri) {
751                this(code, uri, null);
752            }
753
754            static Uri opInsert(Uri uri, ContentValues values) {
755                ProviderExecute e = new ProviderExecute(INSERT, uri, values);
756                if (offUiThread()) return (Uri)e.go();
757                new Thread(e).start();
758                return null;
759            }
760
761            static int opDelete(Uri uri) {
762                ProviderExecute e = new ProviderExecute(DELETE, uri);
763                if (offUiThread()) return (Integer)e.go();
764                new Thread(new ProviderExecute(DELETE, uri)).start();
765                return 0;
766            }
767
768            static int opUpdate(Uri uri, ContentValues values) {
769                ProviderExecute e = new ProviderExecute(UPDATE, uri, values);
770                if (offUiThread()) return (Integer)e.go();
771                new Thread(e).start();
772                return 0;
773            }
774
775            @Override
776            public void run() {
777                go();
778            }
779
780            public Object go() {
781                switch(mCode) {
782                    case DELETE:
783                        return mResolver.delete(mUri, null, null);
784                    case INSERT:
785                        return mResolver.insert(mUri, mValues);
786                    case UPDATE:
787                        return mResolver.update(mUri,  mValues, null, null);
788                    default:
789                        return null;
790                }
791            }
792        }
793
794        private void insertLocal(Uri uri, ContentValues values) {
795            // Placeholder for now; there's no local insert
796        }
797
798        @VisibleForTesting
799        void deleteLocal(Uri uri) {
800            Uri underlyingUri = uriFromCachingUri(uri);
801            // Remember to decode the underlying Uri as it might be encoded (as w/ Gmail)
802            String uriString =  Uri.decode(underlyingUri.toString());
803            cacheValue(uriString, DELETED_COLUMN, true);
804        }
805
806        @VisibleForTesting
807        void updateLocal(Uri uri, ContentValues values) {
808            if (values == null) {
809                return;
810            }
811            Uri underlyingUri = uriFromCachingUri(uri);
812            // Remember to decode the underlying Uri as it might be encoded (as w/ Gmail)
813            String uriString =  Uri.decode(underlyingUri.toString());
814            for (String columnName: values.keySet()) {
815                cacheValue(uriString, columnName, values.get(columnName));
816            }
817        }
818
819        static boolean offUiThread() {
820            return Looper.getMainLooper().getThread() != Thread.currentThread();
821        }
822
823        public int apply(ArrayList<ConversationOperation> ops) {
824            final HashMap<String, ArrayList<ContentProviderOperation>> batchMap =
825                    new HashMap<String, ArrayList<ContentProviderOperation>>();
826            // Increment sequence count
827            sSequence++;
828            // Execute locally and build CPO's for underlying provider
829            for (ConversationOperation op: ops) {
830                Uri underlyingUri = uriFromCachingUri(op.mUri);
831                String authority = underlyingUri.getAuthority();
832                ArrayList<ContentProviderOperation> authOps = batchMap.get(authority);
833                if (authOps == null) {
834                    authOps = new ArrayList<ContentProviderOperation>();
835                    batchMap.put(authority, authOps);
836                }
837                authOps.add(op.execute(underlyingUri));
838            }
839
840            // Send changes to underlying provider
841            for (String authority: batchMap.keySet()) {
842                try {
843                    if (offUiThread()) {
844                        mResolver.applyBatch(authority, batchMap.get(authority));
845                    } else {
846                        final String auth = authority;
847                        new Thread(new Runnable() {
848                            @Override
849                            public void run() {
850                                try {
851                                    mResolver.applyBatch(auth, batchMap.get(auth));
852                                } catch (RemoteException e) {
853                                } catch (OperationApplicationException e) {
854                                }
855                           }
856                        }).start();
857                    }
858                } catch (RemoteException e) {
859                } catch (OperationApplicationException e) {
860                }
861            }
862            return sSequence;
863        }
864    }
865
866    /**
867     * ConversationOperation is the encapsulation of a ContentProvider operation to be performed
868     * atomically as part of a "batch" operation.
869     */
870    public static class ConversationOperation {
871        public static final int DELETE = 0;
872        public static final int INSERT = 1;
873        public static final int UPDATE = 2;
874        public static final int ARCHIVE = 3;
875        public static final int MUTE = 4;
876        public static final int REPORT_SPAM = 5;
877
878        private final int mType;
879        private final Uri mUri;
880        private final ContentValues mValues;
881        // True if an updated item should be removed locally (from ConversationCursor)
882        // This would be the case for a folder/label change in which the conversation is no longer
883        // in the folder represented by the ConversationCursor
884        private final boolean mLocalDeleteOnUpdate;
885
886        public ConversationOperation(int type, Conversation conv) {
887            this(type, conv, null);
888        }
889
890        public ConversationOperation(int type, Conversation conv, ContentValues values) {
891            mType = type;
892            mUri = conv.uri;
893            mValues = values;
894            mLocalDeleteOnUpdate = conv.localDeleteOnUpdate;
895        }
896
897        private ContentProviderOperation execute(Uri underlyingUri) {
898            Uri uri = underlyingUri.buildUpon()
899                    .appendQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER,
900                            Integer.toString(sSequence))
901                    .build();
902            switch(mType) {
903                case DELETE:
904                    sProvider.deleteLocal(mUri);
905                    return ContentProviderOperation.newDelete(uri).build();
906                case UPDATE:
907                    if (mLocalDeleteOnUpdate) {
908                        sProvider.deleteLocal(mUri);
909                    } else {
910                        sProvider.updateLocal(mUri, mValues);
911                    }
912                    return ContentProviderOperation.newUpdate(uri)
913                            .withValues(mValues)
914                            .build();
915                case INSERT:
916                    sProvider.insertLocal(mUri, mValues);
917                    return ContentProviderOperation.newInsert(uri)
918                            .withValues(mValues).build();
919                case ARCHIVE:
920                    sProvider.deleteLocal(mUri);
921
922                    // Create an update operation that represents archive
923                    return ContentProviderOperation.newUpdate(uri).withValue(
924                            ConversationOperations.OPERATION_KEY, ConversationOperations.ARCHIVE)
925                            .build();
926                case MUTE:
927                    if (mLocalDeleteOnUpdate) {
928                        sProvider.deleteLocal(mUri);
929                    }
930
931                    // Create an update operation that represents mute
932                    return ContentProviderOperation.newUpdate(uri).withValue(
933                            ConversationOperations.OPERATION_KEY, ConversationOperations.MUTE)
934                            .build();
935                case REPORT_SPAM:
936                    sProvider.deleteLocal(mUri);
937
938                    // Create an update operation that represents report spam
939                    return ContentProviderOperation.newUpdate(uri).withValue(
940                            ConversationOperations.OPERATION_KEY,
941                            ConversationOperations.REPORT_SPAM).build();
942                default:
943                    throw new UnsupportedOperationException(
944                            "No such ConversationOperation type: " + mType);
945            }
946        }
947    }
948
949    /**
950     * For now, a single listener can be associated with the cursor, and for now we'll just
951     * notify on deletions
952     */
953    public interface ConversationListener {
954        // Data in the underlying provider has changed; a refresh is required to sync up
955        public void onRefreshRequired();
956        // We've completed a requested refresh of the underlying cursor
957        public void onRefreshReady();
958    }
959
960    @Override
961    public boolean isFirst() {
962        throw new UnsupportedOperationException();
963    }
964
965    @Override
966    public boolean isLast() {
967        throw new UnsupportedOperationException();
968    }
969
970    @Override
971    public boolean isBeforeFirst() {
972        throw new UnsupportedOperationException();
973    }
974
975    @Override
976    public boolean isAfterLast() {
977        throw new UnsupportedOperationException();
978    }
979
980    @Override
981    public int getColumnIndex(String columnName) {
982        return sUnderlyingCursor.getColumnIndex(columnName);
983    }
984
985    @Override
986    public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
987        return sUnderlyingCursor.getColumnIndexOrThrow(columnName);
988    }
989
990    @Override
991    public String getColumnName(int columnIndex) {
992        return sUnderlyingCursor.getColumnName(columnIndex);
993    }
994
995    @Override
996    public String[] getColumnNames() {
997        return sUnderlyingCursor.getColumnNames();
998    }
999
1000    @Override
1001    public int getColumnCount() {
1002        return sUnderlyingCursor.getColumnCount();
1003    }
1004
1005    @Override
1006    public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
1007        throw new UnsupportedOperationException();
1008    }
1009
1010    @Override
1011    public int getType(int columnIndex) {
1012        return sUnderlyingCursor.getType(columnIndex);
1013    }
1014
1015    @Override
1016    public boolean isNull(int columnIndex) {
1017        throw new UnsupportedOperationException();
1018    }
1019
1020    @Override
1021    public void deactivate() {
1022        throw new UnsupportedOperationException();
1023    }
1024
1025    @Override
1026    public boolean isClosed() {
1027        return sUnderlyingCursor.isClosed();
1028    }
1029
1030    @Override
1031    public void registerContentObserver(ContentObserver observer) {
1032        sUnderlyingCursor.registerContentObserver(observer);
1033    }
1034
1035    @Override
1036    public void unregisterContentObserver(ContentObserver observer) {
1037        sUnderlyingCursor.unregisterContentObserver(observer);
1038    }
1039
1040    @Override
1041    public void registerDataSetObserver(DataSetObserver observer) {
1042        sUnderlyingCursor.registerDataSetObserver(observer);
1043    }
1044
1045    @Override
1046    public void unregisterDataSetObserver(DataSetObserver observer) {
1047        sUnderlyingCursor.unregisterDataSetObserver(observer);
1048    }
1049
1050    @Override
1051    public void setNotificationUri(ContentResolver cr, Uri uri) {
1052        throw new UnsupportedOperationException();
1053    }
1054
1055    @Override
1056    public boolean getWantsAllOnMoveCalls() {
1057        throw new UnsupportedOperationException();
1058    }
1059
1060    @Override
1061    public Bundle getExtras() {
1062        throw new UnsupportedOperationException();
1063    }
1064
1065    @Override
1066    public Bundle respond(Bundle extras) {
1067        throw new UnsupportedOperationException();
1068    }
1069
1070    @Override
1071    public boolean requery() {
1072        return true;
1073    }
1074}
1075