ConversationCursor.java revision cf164d64bcb1da92b427bda99b97f7ec310ef704
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            sRefreshRequired = true;
595            if (!sPaused) {
596                notifyRefreshRequired();
597            }
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        notifyDataChanged();
667    }
668
669    public boolean isRefreshRequired() {
670        return sRefreshRequired;
671    }
672
673    public boolean isRefreshReady() {
674        return sRefreshReady;
675    }
676
677    /**
678     * Cancel a refresh in progress
679     */
680    public static void cancelRefresh() {
681        if (DEBUG) {
682            LogUtils.i(TAG, "[cancelRefresh() called]");
683        }
684        synchronized(sCacheMapLock) {
685            if (sRefreshTask != null) {
686                sRefreshTask.cancel(true);
687                sRefreshTask = null;
688            }
689            sRefreshReady = false;
690            // If we have the cursor, close it; otherwise, it will get closed when the query
691            // finishes (it checks sRefreshInProgress)
692            if (sRequeryCursor != null) {
693                sRequeryCursor.close();
694                sRequeryCursor = null;
695            }
696        }
697    }
698
699    /**
700     * Get a list of deletions from ConversationCursor to the refreshed cursor that hasn't yet
701     * been swapped into place; this allows the UI to animate these away if desired
702     * @return a list of positions deleted in ConversationCursor
703     */
704    public ArrayList<Integer> getRefreshDeletions () {
705        return EMPTY_DELETION_LIST;
706    }
707
708    /**
709     * When we get a requery from the UI, we'll do it, but also clear the cache. The listener is
710     * notified when the requery is complete
711     * NOTE: This will have to change, of course, when we start using loaders...
712     */
713    public boolean refresh() {
714        if (DEBUG) {
715            LogUtils.i(TAG, "[refresh() called]");
716        }
717        synchronized(sCacheMapLock) {
718            if (sRefreshTask != null) {
719                if (DEBUG) {
720                    LogUtils.i(TAG, "[refresh() returning; already running %d]",
721                            sRefreshTask.hashCode());
722                }
723                return false;
724            }
725            sRefreshTask = new RefreshTask(qUri, qProjection);
726            sRefreshTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
727        }
728        return true;
729    }
730
731    @Override
732    public void close() {
733        if (!sUnderlyingCursor.isClosed()) {
734            // Unregister our observer on the underlying cursor and close as usual
735            if (mCursorObserverRegistered) {
736                try {
737                    sUnderlyingCursor.unregisterContentObserver(mCursorObserver);
738                } catch (IllegalStateException e) {
739                    // Maybe the cursor got GC'd?
740                }
741                mCursorObserverRegistered = false;
742            }
743            sUnderlyingCursor.close();
744        }
745    }
746
747    /**
748     * Move to the next not-deleted item in the conversation
749     */
750    @Override
751    public boolean moveToNext() {
752        while (true) {
753            boolean ret = sUnderlyingCursor.moveToNext();
754            if (!ret) return false;
755            if (getCachedValue(DELETED_COLUMN_INDEX) instanceof Integer) continue;
756            mPosition++;
757            return true;
758        }
759    }
760
761    /**
762     * Move to the previous not-deleted item in the conversation
763     */
764    @Override
765    public boolean moveToPrevious() {
766        while (true) {
767            boolean ret = sUnderlyingCursor.moveToPrevious();
768            if (!ret) return false;
769            if (getCachedValue(DELETED_COLUMN_INDEX) instanceof Integer) continue;
770            mPosition--;
771            return true;
772        }
773    }
774
775    @Override
776    public int getPosition() {
777        return mPosition;
778    }
779
780    /**
781     * The actual cursor's count must be decremented by the number we've deleted from the UI
782     */
783    @Override
784    public int getCount() {
785        return sUnderlyingCursor.getCount() - sDeletedCount;
786    }
787
788    @Override
789    public boolean moveToFirst() {
790        sUnderlyingCursor.moveToPosition(-1);
791        mPosition = -1;
792        return moveToNext();
793    }
794
795    @Override
796    public boolean moveToPosition(int pos) {
797        if (pos == mPosition) return true;
798        if (pos > mPosition) {
799            while (pos > mPosition) {
800                if (!moveToNext()) {
801                    return false;
802                }
803            }
804            return true;
805        } else if (pos == 0) {
806            return moveToFirst();
807        } else {
808            while (pos < mPosition) {
809                if (!moveToPrevious()) {
810                    return false;
811                }
812            }
813            return true;
814        }
815    }
816
817    /**
818     * Make sure mPosition is correct after locally deleting/undeleting items
819     */
820    private void recalibratePosition() {
821        int pos = mPosition;
822        moveToFirst();
823        moveToPosition(pos);
824    }
825
826    @Override
827    public boolean moveToLast() {
828        throw new UnsupportedOperationException("moveToLast unsupported!");
829    }
830
831    @Override
832    public boolean move(int offset) {
833        throw new UnsupportedOperationException("move unsupported!");
834    }
835
836    /**
837     * We need to override all of the getters to make sure they look at cached values before using
838     * the values in the underlying cursor
839     */
840    @Override
841    public double getDouble(int columnIndex) {
842        Object obj = getCachedValue(columnIndex);
843        if (obj != null) return (Double)obj;
844        return sUnderlyingCursor.getDouble(columnIndex);
845    }
846
847    @Override
848    public float getFloat(int columnIndex) {
849        Object obj = getCachedValue(columnIndex);
850        if (obj != null) return (Float)obj;
851        return sUnderlyingCursor.getFloat(columnIndex);
852    }
853
854    @Override
855    public int getInt(int columnIndex) {
856        Object obj = getCachedValue(columnIndex);
857        if (obj != null) return (Integer)obj;
858        return sUnderlyingCursor.getInt(columnIndex);
859    }
860
861    @Override
862    public long getLong(int columnIndex) {
863        Object obj = getCachedValue(columnIndex);
864        if (obj != null) return (Long)obj;
865        return sUnderlyingCursor.getLong(columnIndex);
866    }
867
868    @Override
869    public short getShort(int columnIndex) {
870        Object obj = getCachedValue(columnIndex);
871        if (obj != null) return (Short)obj;
872        return sUnderlyingCursor.getShort(columnIndex);
873    }
874
875    @Override
876    public String getString(int columnIndex) {
877        // If we're asking for the Uri for the conversation list, we return a forwarding URI
878        // so that we can intercept update/delete and handle it ourselves
879        if (columnIndex == sUriColumnIndex) {
880            Uri uri = Uri.parse(sUnderlyingCursor.getString(columnIndex));
881            return uriToCachingUriString(uri);
882        }
883        Object obj = getCachedValue(columnIndex);
884        if (obj != null) return (String)obj;
885        return sUnderlyingCursor.getString(columnIndex);
886    }
887
888    @Override
889    public byte[] getBlob(int columnIndex) {
890        Object obj = getCachedValue(columnIndex);
891        if (obj != null) return (byte[])obj;
892        return sUnderlyingCursor.getBlob(columnIndex);
893    }
894
895    /**
896     * Observer of changes to underlying data
897     */
898    private class CursorObserver extends ContentObserver {
899        public CursorObserver() {
900            super(null);
901        }
902
903        @Override
904        public void onChange(boolean selfChange) {
905            // If we're here, then something outside of the UI has changed the data, and we
906            // must query the underlying provider for that data
907            ConversationCursor.this.underlyingChanged();
908        }
909    }
910
911    /**
912     * ConversationProvider is the ContentProvider for our forwarding Uri's; it passes queries
913     * and inserts directly, and caches updates/deletes before passing them through.  The caching
914     * will cause a redraw of the list with updated values.
915     */
916    public abstract static class ConversationProvider extends ContentProvider {
917        public static String AUTHORITY;
918
919        /**
920         * Allows the implementing provider to specify the authority that should be used.
921         */
922        protected abstract String getAuthority();
923
924        @Override
925        public boolean onCreate() {
926            sProvider = this;
927            AUTHORITY = getAuthority();
928            return true;
929        }
930
931        @Override
932        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
933                String sortOrder) {
934            return mResolver.query(
935                    uriFromCachingUri(uri), projection, selection, selectionArgs, sortOrder);
936        }
937
938        @Override
939        public Uri insert(Uri uri, ContentValues values) {
940            insertLocal(uri, values);
941            return ProviderExecute.opInsert(uri, values);
942        }
943
944        @Override
945        public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
946            updateLocal(uri, values);
947            return ProviderExecute.opUpdate(uri, values);
948        }
949
950        @Override
951        public int delete(Uri uri, String selection, String[] selectionArgs) {
952            deleteLocal(uri);
953            return ProviderExecute.opDelete(uri);
954        }
955
956        @Override
957        public String getType(Uri uri) {
958            return null;
959        }
960
961        /**
962         * Quick and dirty class that executes underlying provider CRUD operations on a background
963         * thread.
964         */
965        static class ProviderExecute implements Runnable {
966            static final int DELETE = 0;
967            static final int INSERT = 1;
968            static final int UPDATE = 2;
969
970            final int mCode;
971            final Uri mUri;
972            final ContentValues mValues; //HEHEH
973
974            ProviderExecute(int code, Uri uri, ContentValues values) {
975                mCode = code;
976                mUri = uriFromCachingUri(uri);
977                mValues = values;
978            }
979
980            ProviderExecute(int code, Uri uri) {
981                this(code, uri, null);
982            }
983
984            static Uri opInsert(Uri uri, ContentValues values) {
985                ProviderExecute e = new ProviderExecute(INSERT, uri, values);
986                if (offUiThread()) return (Uri)e.go();
987                new Thread(e).start();
988                return null;
989            }
990
991            static int opDelete(Uri uri) {
992                ProviderExecute e = new ProviderExecute(DELETE, uri);
993                if (offUiThread()) return (Integer)e.go();
994                new Thread(new ProviderExecute(DELETE, uri)).start();
995                return 0;
996            }
997
998            static int opUpdate(Uri uri, ContentValues values) {
999                ProviderExecute e = new ProviderExecute(UPDATE, uri, values);
1000                if (offUiThread()) return (Integer)e.go();
1001                new Thread(e).start();
1002                return 0;
1003            }
1004
1005            @Override
1006            public void run() {
1007                go();
1008            }
1009
1010            public Object go() {
1011                switch(mCode) {
1012                    case DELETE:
1013                        return mResolver.delete(mUri, null, null);
1014                    case INSERT:
1015                        return mResolver.insert(mUri, mValues);
1016                    case UPDATE:
1017                        return mResolver.update(mUri,  mValues, null, null);
1018                    default:
1019                        return null;
1020                }
1021            }
1022        }
1023
1024        private void insertLocal(Uri uri, ContentValues values) {
1025            // Placeholder for now; there's no local insert
1026        }
1027
1028        private int mUndoSequence = 0;
1029        private ArrayList<Uri> mUndoDeleteUris = new ArrayList<Uri>();
1030
1031        void addToUndoSequence(Uri uri) {
1032            if (sSequence != mUndoSequence) {
1033                mUndoSequence = sSequence;
1034                mUndoDeleteUris.clear();
1035            }
1036            mUndoDeleteUris.add(uri);
1037        }
1038
1039        @VisibleForTesting
1040        void deleteLocal(Uri uri) {
1041            String uriString = uriStringFromCachingUri(uri);
1042            cacheValue(uriString, DELETED_COLUMN, true);
1043            addToUndoSequence(uri);
1044        }
1045
1046        @VisibleForTesting
1047        void undeleteLocal(Uri uri) {
1048            String uriString = uriStringFromCachingUri(uri);
1049            cacheValue(uriString, DELETED_COLUMN, false);
1050        }
1051
1052        void setMostlyDead(Conversation conv) {
1053            Uri uri = conv.uri;
1054            String uriString = uriStringFromCachingUri(uri);
1055            cacheValue(uriString,
1056                    UIProvider.ConversationColumns.FLAGS, Conversation.FLAG_MOSTLY_DEAD);
1057            conv.convFlags |= Conversation.FLAG_MOSTLY_DEAD;
1058            sMostlyDead.add(conv);
1059            LogUtils.i(TAG, "[Mostly dead, deferring: %s", uri);
1060            addToUndoSequence(uri);
1061            sDeferSync = true;
1062        }
1063
1064        void commitMostlyDead(Conversation conv) {
1065            conv.convFlags &= ~Conversation.FLAG_MOSTLY_DEAD;
1066            sMostlyDead.remove(conv);
1067            LogUtils.i(TAG, "[All dead: %s]", conv.uri);
1068            if (sMostlyDead.isEmpty()) {
1069                sDeferSync = false;
1070                checkNotifyUI();
1071            }
1072        }
1073
1074        boolean clearMostlyDead(Uri uri) {
1075            String uriString =  uriStringFromCachingUri(uri);
1076            Object val = sConversationCursor.getCachedValue(uriString,
1077                    UIProvider.CONVERSATION_FLAGS_COLUMN);
1078            if (val != null) {
1079                int flags = ((Integer)val).intValue();
1080                if ((flags & Conversation.FLAG_MOSTLY_DEAD) != 0) {
1081                    cacheValue(uriString, UIProvider.ConversationColumns.FLAGS,
1082                            flags &= ~Conversation.FLAG_MOSTLY_DEAD);
1083                    return true;
1084                }
1085            }
1086            return false;
1087        }
1088
1089        public void undo() {
1090            if (sSequence == mUndoSequence) {
1091                for (Uri uri: mUndoDeleteUris) {
1092                    if (!clearMostlyDead(uri)) {
1093                        undeleteLocal(uri);
1094                    }
1095                }
1096                mUndoSequence = 0;
1097                sConversationCursor.recalibratePosition();
1098            }
1099        }
1100
1101        @VisibleForTesting
1102        void updateLocal(Uri uri, ContentValues values) {
1103            if (values == null) {
1104                return;
1105            }
1106            String uriString = uriStringFromCachingUri(uri);
1107            for (String columnName: values.keySet()) {
1108                cacheValue(uriString, columnName, values.get(columnName));
1109            }
1110        }
1111
1112        public int apply(ArrayList<ConversationOperation> ops) {
1113            final HashMap<String, ArrayList<ContentProviderOperation>> batchMap =
1114                    new HashMap<String, ArrayList<ContentProviderOperation>>();
1115            // Increment sequence count
1116            sSequence++;
1117
1118            // Execute locally and build CPO's for underlying provider
1119            boolean recalibrateRequired = false;
1120            for (ConversationOperation op: ops) {
1121                Uri underlyingUri = uriFromCachingUri(op.mUri);
1122                String authority = underlyingUri.getAuthority();
1123                ArrayList<ContentProviderOperation> authOps = batchMap.get(authority);
1124                if (authOps == null) {
1125                    authOps = new ArrayList<ContentProviderOperation>();
1126                    batchMap.put(authority, authOps);
1127                }
1128                ContentProviderOperation cpo = op.execute(underlyingUri);
1129                if (cpo != null) {
1130                    authOps.add(cpo);
1131                }
1132                // Keep track of whether our operations require recalibrating the cursor position
1133                if (op.mRecalibrateRequired) {
1134                    recalibrateRequired = true;
1135                }
1136            }
1137
1138            // Recalibrate cursor position if required
1139            if (recalibrateRequired) {
1140                sConversationCursor.recalibratePosition();
1141            }
1142            // Notify listeners that data has changed
1143            sConversationCursor.notifyDataChanged();
1144
1145            // Send changes to underlying provider
1146            for (String authority: batchMap.keySet()) {
1147                try {
1148                    if (offUiThread()) {
1149                        mResolver.applyBatch(authority, batchMap.get(authority));
1150                    } else {
1151                        final String auth = authority;
1152                        new Thread(new Runnable() {
1153                            @Override
1154                            public void run() {
1155                                try {
1156                                    mResolver.applyBatch(auth, batchMap.get(auth));
1157                                } catch (RemoteException e) {
1158                                } catch (OperationApplicationException e) {
1159                                }
1160                           }
1161                        }).start();
1162                    }
1163                } catch (RemoteException e) {
1164                } catch (OperationApplicationException e) {
1165                }
1166            }
1167            return sSequence;
1168        }
1169    }
1170
1171    /**
1172     * ConversationOperation is the encapsulation of a ContentProvider operation to be performed
1173     * atomically as part of a "batch" operation.
1174     */
1175    public static class ConversationOperation {
1176        private static final int MOSTLY = 0x80;
1177        public static final int DELETE = 0;
1178        public static final int INSERT = 1;
1179        public static final int UPDATE = 2;
1180        public static final int ARCHIVE = 3;
1181        public static final int MUTE = 4;
1182        public static final int REPORT_SPAM = 5;
1183        public static final int MOSTLY_ARCHIVE = MOSTLY | ARCHIVE;
1184        public static final int MOSTLY_DELETE = MOSTLY | DELETE;
1185
1186        private final int mType;
1187        private final Uri mUri;
1188        private final Conversation mConversation;
1189        private final ContentValues mValues;
1190        // True if an updated item should be removed locally (from ConversationCursor)
1191        // This would be the case for a folder change in which the conversation is no longer
1192        // in the folder represented by the ConversationCursor
1193        private final boolean mLocalDeleteOnUpdate;
1194        // After execution, this indicates whether or not the operation requires recalibration of
1195        // the current cursor position (i.e. it removed or added items locally)
1196        private boolean mRecalibrateRequired = true;
1197        // Whether this item is already mostly dead
1198        private final boolean mMostlyDead;
1199
1200        /**
1201         * Set to true to immediately notify any {@link DataSetObserver}s watching the global
1202         * {@link ConversationCursor} upon applying the change to the data cache. You would not
1203         * want to do this if a change you make is being handled specially, like an animated delete.
1204         *
1205         * TODO: move this to the application Controller, or whoever has a canonical reference
1206         * to a {@link ConversationCursor} to notify on.
1207         */
1208        private final boolean mAutoNotify;
1209
1210        public ConversationOperation(int type, Conversation conv) {
1211            this(type, conv, null, false /* autoNotify */);
1212        }
1213
1214        public ConversationOperation(int type, Conversation conv, ContentValues values,
1215                boolean autoNotify) {
1216            mType = type;
1217            mUri = conv.uri;
1218            mConversation = conv;
1219            mValues = values;
1220            mLocalDeleteOnUpdate = conv.localDeleteOnUpdate;
1221            mAutoNotify = autoNotify;
1222            mMostlyDead = conv.isMostlyDead();
1223        }
1224
1225        private ContentProviderOperation execute(Uri underlyingUri) {
1226            Uri uri = underlyingUri.buildUpon()
1227                    .appendQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER,
1228                            Integer.toString(sSequence))
1229                    .build();
1230            ContentProviderOperation op = null;
1231            switch(mType) {
1232                case UPDATE:
1233                    if (mLocalDeleteOnUpdate) {
1234                        sProvider.deleteLocal(mUri);
1235                    } else {
1236                        sProvider.updateLocal(mUri, mValues);
1237                        mRecalibrateRequired = false;
1238                    }
1239                    op = ContentProviderOperation.newUpdate(uri)
1240                            .withValues(mValues)
1241                            .build();
1242                    break;
1243                case INSERT:
1244                    sProvider.insertLocal(mUri, mValues);
1245                    op = ContentProviderOperation.newInsert(uri)
1246                            .withValues(mValues).build();
1247                    break;
1248                // Destructive actions below!
1249                // "Mostly" operations are reflected globally, but not locally, except to set
1250                // FLAG_MOSTLY_DEAD in the conversation itself
1251                case DELETE:
1252                    sProvider.deleteLocal(mUri);
1253                    if (!mMostlyDead) {
1254                        op = ContentProviderOperation.newDelete(uri).build();
1255                    } else {
1256                        sProvider.commitMostlyDead(mConversation);
1257                    }
1258                    break;
1259                case MOSTLY_DELETE:
1260                    sProvider.setMostlyDead(mConversation);
1261                    op = ContentProviderOperation.newDelete(uri).build();
1262                    break;
1263                case ARCHIVE:
1264                    sProvider.deleteLocal(mUri);
1265                    if (!mMostlyDead) {
1266                        // Create an update operation that represents archive
1267                        op = ContentProviderOperation.newUpdate(uri).withValue(
1268                                ConversationOperations.OPERATION_KEY,
1269                                ConversationOperations.ARCHIVE)
1270                                .build();
1271                    } else {
1272                        sProvider.commitMostlyDead(mConversation);
1273                    }
1274                    break;
1275                case MOSTLY_ARCHIVE:
1276                    sProvider.setMostlyDead(mConversation);
1277                    // Create an update operation that represents archive
1278                    op = ContentProviderOperation.newUpdate(uri).withValue(
1279                            ConversationOperations.OPERATION_KEY, ConversationOperations.ARCHIVE)
1280                            .build();
1281                    break;
1282                case MUTE:
1283                    if (mLocalDeleteOnUpdate) {
1284                        sProvider.deleteLocal(mUri);
1285                    }
1286
1287                    // Create an update operation that represents mute
1288                    op = ContentProviderOperation.newUpdate(uri).withValue(
1289                            ConversationOperations.OPERATION_KEY, ConversationOperations.MUTE)
1290                            .build();
1291                    break;
1292                case REPORT_SPAM:
1293                    sProvider.deleteLocal(mUri);
1294
1295                    // Create an update operation that represents report spam
1296                    op = ContentProviderOperation.newUpdate(uri).withValue(
1297                            ConversationOperations.OPERATION_KEY,
1298                            ConversationOperations.REPORT_SPAM).build();
1299                    break;
1300                default:
1301                    throw new UnsupportedOperationException(
1302                            "No such ConversationOperation type: " + mType);
1303            }
1304
1305            // FIXME: this is a hack to notify conversation list of changes from conversation view.
1306            // The proper way to do this is to have the Controller handle the 'mark read' action.
1307            // It has a reference to this ConversationCursor so it can notify without using global
1308            // magic.
1309            if (mAutoNotify) {
1310                if (sConversationCursor != null) {
1311                    sConversationCursor.notifyDataSetChanged();
1312                } else {
1313                    LogUtils.i(TAG, "Unable to auto-notify because there is no existing" +
1314                            " conversation cursor");
1315                }
1316            }
1317
1318            return op;
1319        }
1320    }
1321
1322    /**
1323     * For now, a single listener can be associated with the cursor, and for now we'll just
1324     * notify on deletions
1325     */
1326    public interface ConversationListener {
1327        /**
1328         * Data in the underlying provider has changed; a refresh is required to sync up
1329         */
1330        public void onRefreshRequired();
1331        /**
1332         * We've completed a requested refresh of the underlying cursor
1333         */
1334        public void onRefreshReady();
1335        /**
1336         * The data underlying the cursor has changed; the UI should redraw the list
1337         */
1338        public void onDataSetChanged();
1339    }
1340
1341    @Override
1342    public boolean isFirst() {
1343        throw new UnsupportedOperationException();
1344    }
1345
1346    @Override
1347    public boolean isLast() {
1348        throw new UnsupportedOperationException();
1349    }
1350
1351    @Override
1352    public boolean isBeforeFirst() {
1353        throw new UnsupportedOperationException();
1354    }
1355
1356    @Override
1357    public boolean isAfterLast() {
1358        throw new UnsupportedOperationException();
1359    }
1360
1361    @Override
1362    public int getColumnIndex(String columnName) {
1363        return sUnderlyingCursor.getColumnIndex(columnName);
1364    }
1365
1366    @Override
1367    public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
1368        return sUnderlyingCursor.getColumnIndexOrThrow(columnName);
1369    }
1370
1371    @Override
1372    public String getColumnName(int columnIndex) {
1373        return sUnderlyingCursor.getColumnName(columnIndex);
1374    }
1375
1376    @Override
1377    public String[] getColumnNames() {
1378        return sUnderlyingCursor.getColumnNames();
1379    }
1380
1381    @Override
1382    public int getColumnCount() {
1383        return sUnderlyingCursor.getColumnCount();
1384    }
1385
1386    @Override
1387    public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
1388        throw new UnsupportedOperationException();
1389    }
1390
1391    @Override
1392    public int getType(int columnIndex) {
1393        return sUnderlyingCursor.getType(columnIndex);
1394    }
1395
1396    @Override
1397    public boolean isNull(int columnIndex) {
1398        throw new UnsupportedOperationException();
1399    }
1400
1401    @Override
1402    public void deactivate() {
1403        throw new UnsupportedOperationException();
1404    }
1405
1406    @Override
1407    public boolean isClosed() {
1408        return sUnderlyingCursor.isClosed();
1409    }
1410
1411    @Override
1412    public void registerContentObserver(ContentObserver observer) {
1413        // Nope. We never notify of underlying changes on this channel, since the cursor watches
1414        // internally and offers onRefreshRequired/onRefreshReady to accomplish the same thing.
1415    }
1416
1417    @Override
1418    public void unregisterContentObserver(ContentObserver observer) {
1419        // See above.
1420    }
1421
1422    @Override
1423    public void registerDataSetObserver(DataSetObserver observer) {
1424        mDataSetObservable.registerObserver(observer);
1425    }
1426
1427    @Override
1428    public void unregisterDataSetObserver(DataSetObserver observer) {
1429        mDataSetObservable.unregisterObserver(observer);
1430    }
1431
1432    public void notifyDataSetChanged() {
1433        mDataSetObservable.notifyChanged();
1434    }
1435
1436    @Override
1437    public void setNotificationUri(ContentResolver cr, Uri uri) {
1438        throw new UnsupportedOperationException();
1439    }
1440
1441    @Override
1442    public boolean getWantsAllOnMoveCalls() {
1443        throw new UnsupportedOperationException();
1444    }
1445
1446    @Override
1447    public Bundle getExtras() {
1448        throw new UnsupportedOperationException();
1449    }
1450
1451    @Override
1452    public Bundle respond(Bundle extras) {
1453        throw new UnsupportedOperationException();
1454    }
1455
1456    @Override
1457    public boolean requery() {
1458        return true;
1459    }
1460}