MessageOrderManager.java revision 22d1a794cd9636634bb31689f53603c0ae64c522
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 com.android.emailcommon.provider.EmailContent;
20import com.android.emailcommon.provider.EmailContent.Message;
21import com.android.emailcommon.provider.Mailbox;
22import com.android.emailcommon.utility.EmailAsyncTask;
23import com.android.emailcommon.utility.Utility;
24import com.google.common.base.Preconditions;
25
26import android.content.ContentResolver;
27import android.content.Context;
28import android.database.ContentObserver;
29import android.database.Cursor;
30import android.os.Handler;
31
32/**
33 * Used by {@link MessageView} to determine the message-id of the previous/next messages.
34 *
35 * All public methods must be called on the main thread.
36 *
37 * Call {@link #moveTo} to set the current message id.  As a result,
38 * either {@link Callback#onMessagesChanged} or {@link Callback#onMessageNotFound} is called.
39 *
40 * Use {@link #canMoveToNewer()} and {@link #canMoveToOlder()} to see if there is a newer/older
41 * message, and {@link #moveToNewer()} and {@link #moveToOlder()} to update the current position.
42 *
43 * If the message list changes (e.g. message removed, new message arrived, etc), {@link Callback}
44 * gets called again.
45 *
46 * When an instance is no longer needed, call {@link #close()}, which closes an underlying cursor
47 * and shuts down an async task.
48 *
49 * TODO: Is there better words than "newer"/"older" that works even if we support other sort orders
50 * than timestamp?
51 */
52public class MessageOrderManager {
53    private final Context mContext;
54    private final ContentResolver mContentResolver;
55
56    private final long mMailboxId;
57    private final ContentObserver mObserver;
58    private final Callback mCallback;
59
60    private LoadMessageListTask mLoadMessageListTask;
61    private Cursor mCursor;
62
63    private long mCurrentMessageId = -1;
64
65    private int mTotalMessageCount;
66
67    private int mCurrentPosition;
68
69    private boolean mClosed = false;
70
71    public interface Callback {
72        /**
73         * Called when the message set by {@link MessageOrderManager#moveTo(long)} is found in the
74         * mailbox.  {@link #canMoveToOlder}, {@link #canMoveToNewer}, {@link #moveToOlder} and
75         * {@link #moveToNewer} are ready to be called.
76         */
77        public void onMessagesChanged();
78        /**
79         * Called when the message set by {@link MessageOrderManager#moveTo(long)} is not found.
80         */
81        public void onMessageNotFound();
82    }
83
84    public MessageOrderManager(Context context, long mailboxId, Callback callback) {
85        Preconditions.checkArgument(mailboxId != Mailbox.NO_MAILBOX);
86        mContext = context.getApplicationContext();
87        mContentResolver = mContext.getContentResolver();
88        mMailboxId = mailboxId;
89        mCallback = callback;
90        mObserver = new ContentObserver(getHandlerForContentObserver()) {
91                @Override public void onChange(boolean selfChange) {
92                    if (mClosed) {
93                        return;
94                    }
95                    onContentChanged();
96                }
97        };
98        startTask();
99    }
100
101    public long getMailboxId() {
102        return mMailboxId;
103    }
104
105    /**
106     * @return the total number of messages.
107     */
108    public int getTotalMessageCount() {
109        return mTotalMessageCount;
110    }
111
112    /**
113     * @return current cursor position, starting from 0.
114     */
115    public int getCurrentPosition() {
116        return mCurrentPosition;
117    }
118
119    /**
120     * @return a {@link Handler} for {@link ContentObserver}.
121     *
122     * Unit tests override this and return null, so that {@link ContentObserver#onChange} is
123     * called synchronously.
124     */
125    /* package */ Handler getHandlerForContentObserver() {
126        return new Handler();
127    }
128
129    private boolean isTaskRunning() {
130        return mLoadMessageListTask != null;
131    }
132
133    private void startTask() {
134        cancelTask();
135        startQuery();
136    }
137
138    /**
139     * Start {@link LoadMessageListTask} to query DB.
140     * Unit tests override this to make tests synchronous and to inject a mock query.
141     */
142    /* package */ void startQuery() {
143        mLoadMessageListTask = new LoadMessageListTask();
144        mLoadMessageListTask.executeParallel();
145    }
146
147    private void cancelTask() {
148        Utility.cancelTaskInterrupt(mLoadMessageListTask);
149        mLoadMessageListTask = null;
150    }
151
152    private void closeCursor() {
153        if (mCursor != null) {
154            mCursor.close();
155            mCursor = null;
156        }
157    }
158
159    private void setCurrentMessageIdFromCursor() {
160        if (mCursor != null) {
161            mCurrentMessageId = mCursor.getLong(EmailContent.ID_PROJECTION_COLUMN);
162        }
163    }
164
165    private void onContentChanged() {
166        if (!isTaskRunning()) { // Start only if not running already.
167            startTask();
168        }
169    }
170
171    /**
172     * Shutdown itself and release resources.
173     */
174    public void close() {
175        mClosed = true;
176        cancelTask();
177        closeCursor();
178    }
179
180    public long getCurrentMessageId() {
181        return mCurrentMessageId;
182    }
183
184    /**
185     * Set the current message id.  As a result, either {@link Callback#onMessagesChanged} or
186     * {@link Callback#onMessageNotFound} is called.
187     */
188    public void moveTo(long messageId) {
189        if (mCurrentMessageId != messageId) {
190            mCurrentMessageId = messageId;
191            adjustCursorPosition();
192        }
193    }
194
195    private void adjustCursorPosition() {
196        mCurrentPosition = 0;
197        if (mCurrentMessageId == -1) {
198            return; // Current ID not specified yet.
199        }
200        if (mCursor == null) {
201            // Task not finished yet.
202            // We call adjustCursorPosition() again when we've opened a cursor.
203            return;
204        }
205        mCursor.moveToPosition(-1);
206        while (mCursor.moveToNext()
207                && mCursor.getLong(EmailContent.ID_PROJECTION_COLUMN) != mCurrentMessageId) {
208            mCurrentPosition++;
209        }
210        if (mCursor.isAfterLast()) {
211            mCurrentPosition = 0;
212            mCallback.onMessageNotFound(); // Message not found... Already deleted?
213        } else {
214            mCallback.onMessagesChanged();
215        }
216    }
217
218    /**
219     * @return true if the message set to {@link #moveTo} has an older message in the mailbox.
220     * false otherwise, or unknown yet.
221     */
222    public boolean canMoveToOlder() {
223        return (mCursor != null) && !mCursor.isLast();
224    }
225
226
227    /**
228     * @return true if the message set to {@link #moveTo} has an newer message in the mailbox.
229     * false otherwise, or unknown yet.
230     */
231    public boolean canMoveToNewer() {
232        return (mCursor != null) && !mCursor.isFirst();
233    }
234
235    /**
236     * Move to the older message.
237     *
238     * @return true iif succeed, and {@link Callback#onMessagesChanged} is called.
239     */
240    public boolean moveToOlder() {
241        if (canMoveToOlder() && mCursor.moveToNext()) {
242            mCurrentPosition++;
243            setCurrentMessageIdFromCursor();
244            mCallback.onMessagesChanged();
245            return true;
246        } else {
247            return false;
248        }
249    }
250
251    /**
252     * Move to the newer message.
253     *
254     * @return true iif succeed, and {@link Callback#onMessagesChanged} is called.
255     */
256    public boolean moveToNewer() {
257        if (canMoveToNewer() && mCursor.moveToPrevious()) {
258            mCurrentPosition--;
259            setCurrentMessageIdFromCursor();
260            mCallback.onMessagesChanged();
261            return true;
262        } else {
263            return false;
264        }
265    }
266
267    /**
268     * Task to open a Cursor on a worker thread.
269     */
270    private class LoadMessageListTask extends EmailAsyncTask<Void, Void, Cursor> {
271        public LoadMessageListTask() {
272            super(null);
273        }
274
275        @Override
276        protected Cursor doInBackground(Void... params) {
277            return openNewCursor();
278        }
279
280        @Override
281        protected void onCancelled(Cursor cursor) {
282            if (cursor != null) {
283                cursor.close();
284            }
285            onCursorOpenDone(null);
286        }
287
288        @Override
289        protected void onSuccess(Cursor cursor) {
290            onCursorOpenDone(cursor);
291        }
292    }
293
294    /**
295     * Open a new cursor for a message list.
296     *
297     * This method is called on a worker thread by LoadMessageListTask.
298     */
299    private Cursor openNewCursor() {
300        final Cursor cursor = mContentResolver.query(EmailContent.Message.CONTENT_URI,
301                EmailContent.ID_PROJECTION, Message.buildMessageListSelection(mContext, mMailboxId),
302                null, EmailContent.MessageColumns.TIMESTAMP + " DESC");
303        return cursor;
304    }
305
306    /**
307     * Called when {@link #openNewCursor()} is finished.
308     *
309     * Unit tests call this directly to inject a mock cursor.
310     */
311    /* package */ void onCursorOpenDone(Cursor cursor) {
312        try {
313            closeCursor();
314            if (cursor == null || cursor.isClosed()) {
315                mTotalMessageCount = 0;
316                mCurrentPosition = 0;
317                return; // Task canceled
318            }
319            mCursor = cursor;
320            mTotalMessageCount = mCursor.getCount();
321            mCursor.registerContentObserver(mObserver);
322            adjustCursorPosition();
323        } finally {
324            mLoadMessageListTask = null; // isTaskRunning() becomes false.
325        }
326    }
327}
328