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