MessageOrderManager.java revision bc47398187c6ffd132435e51d8d61e6ec79a79db
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.activity;
18
19import android.content.ContentResolver;
20import android.content.Context;
21import android.database.ContentObserver;
22import android.database.Cursor;
23import android.os.Handler;
24
25import com.android.email.MessageListContext;
26import com.android.email.activity.MessageOrderManager.Callback;
27import com.android.emailcommon.provider.EmailContent;
28import com.android.emailcommon.provider.EmailContent.Message;
29import com.android.emailcommon.provider.Mailbox;
30import com.android.emailcommon.utility.DelayedOperations;
31import com.android.emailcommon.utility.EmailAsyncTask;
32import com.android.emailcommon.utility.Utility;
33import com.google.common.annotations.VisibleForTesting;
34import com.google.common.base.Preconditions;
35
36/**
37 * Used by {@link MessageView} to determine the message-id of the previous/next messages.
38 *
39 * All public methods must be called on the main thread.
40 *
41 * Call {@link #moveTo} to set the current message id.  As a result,
42 * either {@link Callback#onMessagesChanged} or {@link Callback#onMessageNotFound} is called.
43 *
44 * Use {@link #canMoveToNewer()} and {@link #canMoveToOlder()} to see if there is a newer/older
45 * message, and {@link #moveToNewer()} and {@link #moveToOlder()} to update the current position.
46 *
47 * If the message list changes (e.g. message removed, new message arrived, etc), {@link Callback}
48 * gets called again.
49 *
50 * When an instance is no longer needed, call {@link #close()}, which closes an underlying cursor
51 * and shuts down an async task.
52 *
53 * TODO: Is there better words than "newer"/"older" that works even if we support other sort orders
54 * than timestamp?
55 */
56public class MessageOrderManager {
57    private final Context mContext;
58    private final ContentResolver mContentResolver;
59
60    private final MessageListContext mListContext;
61    private final ContentObserver mObserver;
62    private final Callback mCallback;
63    private final DelayedOperations mDelayedOperations;
64
65    private LoadMessageListTask mLoadMessageListTask;
66    private Cursor mCursor;
67
68    private long mCurrentMessageId = -1;
69
70    private int mTotalMessageCount;
71
72    private int mCurrentPosition;
73
74    private boolean mClosed = false;
75
76    public interface Callback {
77        /**
78         * Called when the message set by {@link MessageOrderManager#moveTo(long)} is found in the
79         * mailbox.  {@link #canMoveToOlder}, {@link #canMoveToNewer}, {@link #moveToOlder} and
80         * {@link #moveToNewer} are ready to be called.
81         */
82        public void onMessagesChanged();
83        /**
84         * Called when the message set by {@link MessageOrderManager#moveTo(long)} is not found.
85         */
86        public void onMessageNotFound();
87    }
88
89    /**
90     * Wrapper for {@link Callback}, which uses {@link DelayedOperations#post(Runnable)} to
91     * kick callbacks rather than calling them directly.  This is used to avoid the "nested fragment
92     * transaction" exception.  e.g. {@link #moveTo} is often called during a fragment transaction,
93     * and if the message no longer exists we call {@link #onMessageNotFound}, which most probably
94     * triggers another fragment transaction.
95     */
96    private class PostingCallback implements Callback {
97        private final Callback mOriginal;
98
99        private PostingCallback(Callback original) {
100            mOriginal = original;
101        }
102
103        private final Runnable mOnMessagesChangedRunnable = new Runnable() {
104            @Override public void run() {
105                mOriginal.onMessagesChanged();
106            }
107        };
108
109        @Override
110        public void onMessagesChanged() {
111            mDelayedOperations.post(mOnMessagesChangedRunnable);
112        }
113
114        private final Runnable mOnMessageNotFoundRunnable = new Runnable() {
115            @Override public void run() {
116                mOriginal.onMessageNotFound();
117            }
118        };
119
120        @Override
121        public void onMessageNotFound() {
122            mDelayedOperations.post(mOnMessageNotFoundRunnable);
123        }
124    }
125
126    public MessageOrderManager(Context context, MessageListContext listContext, Callback callback) {
127        this(context, listContext, callback, new DelayedOperations(Utility.getMainThreadHandler()));
128    }
129
130    @VisibleForTesting
131    MessageOrderManager(Context context, MessageListContext listContext, Callback callback,
132            DelayedOperations delayedOperations) {
133        Preconditions.checkArgument(listContext.getMailboxId() != Mailbox.NO_MAILBOX);
134        mContext = context.getApplicationContext();
135        mContentResolver = mContext.getContentResolver();
136        mDelayedOperations = delayedOperations;
137        mListContext = listContext;
138        mCallback = new PostingCallback(callback);
139        mObserver = new ContentObserver(getHandlerForContentObserver()) {
140                @Override public void onChange(boolean selfChange) {
141                    if (mClosed) {
142                        return;
143                    }
144                    onContentChanged();
145                }
146        };
147        startTask();
148    }
149
150    public MessageListContext getListContext() {
151        return mListContext;
152    }
153
154    public long getMailboxId() {
155        return mListContext.getMailboxId();
156    }
157
158    /**
159     * @return the total number of messages.
160     */
161    public int getTotalMessageCount() {
162        return mTotalMessageCount;
163    }
164
165    /**
166     * @return current cursor position, starting from 0.
167     */
168    public int getCurrentPosition() {
169        return mCurrentPosition;
170    }
171
172    /**
173     * @return a {@link Handler} for {@link ContentObserver}.
174     *
175     * Unit tests override this and return null, so that {@link ContentObserver#onChange} is
176     * called synchronously.
177     */
178    /* package */ Handler getHandlerForContentObserver() {
179        return new Handler();
180    }
181
182    private boolean isTaskRunning() {
183        return mLoadMessageListTask != null;
184    }
185
186    private void startTask() {
187        cancelTask();
188        startQuery();
189    }
190
191    /**
192     * Start {@link LoadMessageListTask} to query DB.
193     * Unit tests override this to make tests synchronous and to inject a mock query.
194     */
195    /* package */ void startQuery() {
196        mLoadMessageListTask = new LoadMessageListTask();
197        mLoadMessageListTask.executeParallel();
198    }
199
200    private void cancelTask() {
201        Utility.cancelTaskInterrupt(mLoadMessageListTask);
202        mLoadMessageListTask = null;
203    }
204
205    private void closeCursor() {
206        if (mCursor != null) {
207            mCursor.close();
208            mCursor = null;
209        }
210    }
211
212    private void setCurrentMessageIdFromCursor() {
213        if (mCursor != null) {
214            mCurrentMessageId = mCursor.getLong(EmailContent.ID_PROJECTION_COLUMN);
215        }
216    }
217
218    private void onContentChanged() {
219        if (!isTaskRunning()) { // Start only if not running already.
220            startTask();
221        }
222    }
223
224    /**
225     * Shutdown itself and release resources.
226     */
227    public void close() {
228        mClosed = true;
229        mDelayedOperations.removeCallbacks();
230        cancelTask();
231        closeCursor();
232    }
233
234    public long getCurrentMessageId() {
235        return mCurrentMessageId;
236    }
237
238    /**
239     * Set the current message id.  As a result, either {@link Callback#onMessagesChanged} or
240     * {@link Callback#onMessageNotFound} is called.
241     */
242    public void moveTo(long messageId) {
243        if (mCurrentMessageId != messageId) {
244            mCurrentMessageId = messageId;
245            adjustCursorPosition();
246        }
247    }
248
249    private void adjustCursorPosition() {
250        mCurrentPosition = 0;
251        if (mCurrentMessageId == -1) {
252            return; // Current ID not specified yet.
253        }
254        if (mCursor == null) {
255            // Task not finished yet.
256            // We call adjustCursorPosition() again when we've opened a cursor.
257            return;
258        }
259        mCursor.moveToPosition(-1);
260        while (mCursor.moveToNext()
261                && mCursor.getLong(EmailContent.ID_PROJECTION_COLUMN) != mCurrentMessageId) {
262            mCurrentPosition++;
263        }
264        if (mCursor.isAfterLast()) {
265            mCurrentPosition = 0;
266            mCallback.onMessageNotFound(); // Message not found... Already deleted?
267        } else {
268            mCallback.onMessagesChanged();
269        }
270    }
271
272    /**
273     * @return true if the message set to {@link #moveTo} has an older message in the mailbox.
274     * false otherwise, or unknown yet.
275     */
276    public boolean canMoveToOlder() {
277        return (mCursor != null) && !mCursor.isLast();
278    }
279
280
281    /**
282     * @return true if the message set to {@link #moveTo} has an newer message in the mailbox.
283     * false otherwise, or unknown yet.
284     */
285    public boolean canMoveToNewer() {
286        return (mCursor != null) && !mCursor.isFirst();
287    }
288
289    /**
290     * Move to the older message.
291     *
292     * @return true iif succeed, and {@link Callback#onMessagesChanged} is called.
293     */
294    public boolean moveToOlder() {
295        if (canMoveToOlder() && mCursor.moveToNext()) {
296            mCurrentPosition++;
297            setCurrentMessageIdFromCursor();
298            mCallback.onMessagesChanged();
299            return true;
300        } else {
301            return false;
302        }
303    }
304
305    /**
306     * Move to the newer message.
307     *
308     * @return true iif succeed, and {@link Callback#onMessagesChanged} is called.
309     */
310    public boolean moveToNewer() {
311        if (canMoveToNewer() && mCursor.moveToPrevious()) {
312            mCurrentPosition--;
313            setCurrentMessageIdFromCursor();
314            mCallback.onMessagesChanged();
315            return true;
316        } else {
317            return false;
318        }
319    }
320
321    /**
322     * Task to open a Cursor on a worker thread.
323     */
324    private class LoadMessageListTask extends EmailAsyncTask<Void, Void, Cursor> {
325        public LoadMessageListTask() {
326            super(null);
327        }
328
329        @Override
330        protected Cursor doInBackground(Void... params) {
331            return openNewCursor();
332        }
333
334        @Override
335        protected void onCancelled(Cursor cursor) {
336            if (cursor != null) {
337                cursor.close();
338            }
339            onCursorOpenDone(null);
340        }
341
342        @Override
343        protected void onSuccess(Cursor cursor) {
344            onCursorOpenDone(cursor);
345        }
346    }
347
348    /**
349     * Open a new cursor for a message list.
350     *
351     * This method is called on a worker thread by LoadMessageListTask.
352     */
353    private Cursor openNewCursor() {
354        final Cursor cursor = mContentResolver.query(EmailContent.Message.CONTENT_URI,
355                EmailContent.ID_PROJECTION,
356                Message.buildMessageListSelection(
357                        mContext, mListContext.mAccountId, mListContext.getMailboxId()),
358                null, EmailContent.MessageColumns.TIMESTAMP + " DESC");
359        return cursor;
360    }
361
362    /**
363     * Called when {@link #openNewCursor()} is finished.
364     *
365     * Unit tests call this directly to inject a mock cursor.
366     */
367    /* package */ void onCursorOpenDone(Cursor cursor) {
368        try {
369            closeCursor();
370            if (cursor == null || cursor.isClosed()) {
371                mTotalMessageCount = 0;
372                mCurrentPosition = 0;
373                return; // Task canceled
374            }
375            mCursor = cursor;
376            mTotalMessageCount = mCursor.getCount();
377            mCursor.registerContentObserver(mObserver);
378            adjustCursorPosition();
379        } finally {
380            mLoadMessageListTask = null; // isTaskRunning() becomes false.
381        }
382    }
383}
384