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