• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.email.activity;
18 
19 import android.content.ContentResolver;
20 import android.content.Context;
21 import android.database.ContentObserver;
22 import android.database.Cursor;
23 import android.os.Handler;
24 
25 import com.android.email.MessageListContext;
26 import com.android.email.activity.MessageOrderManager.Callback;
27 import com.android.emailcommon.provider.EmailContent;
28 import com.android.emailcommon.provider.EmailContent.Message;
29 import com.android.emailcommon.provider.Mailbox;
30 import com.android.emailcommon.utility.DelayedOperations;
31 import com.android.emailcommon.utility.EmailAsyncTask;
32 import com.android.emailcommon.utility.Utility;
33 import com.google.common.annotations.VisibleForTesting;
34 import 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  */
56 public 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          */
onMessagesChanged()82         public void onMessagesChanged();
83         /**
84          * Called when the message set by {@link MessageOrderManager#moveTo(long)} is not found.
85          */
onMessageNotFound()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 
PostingCallback(Callback original)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
onMessagesChanged()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
onMessageNotFound()121         public void onMessageNotFound() {
122             mDelayedOperations.post(mOnMessageNotFoundRunnable);
123         }
124     }
125 
MessageOrderManager(Context context, MessageListContext listContext, Callback callback)126     public MessageOrderManager(Context context, MessageListContext listContext, Callback callback) {
127         this(context, listContext, callback, new DelayedOperations(Utility.getMainThreadHandler()));
128     }
129 
130     @VisibleForTesting
MessageOrderManager(Context context, MessageListContext listContext, Callback callback, DelayedOperations delayedOperations)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 
getListContext()150     public MessageListContext getListContext() {
151         return mListContext;
152     }
153 
getMailboxId()154     public long getMailboxId() {
155         return mListContext.getMailboxId();
156     }
157 
158     /**
159      * @return the total number of messages.
160      */
getTotalMessageCount()161     public int getTotalMessageCount() {
162         return mTotalMessageCount;
163     }
164 
165     /**
166      * @return current cursor position, starting from 0.
167      */
getCurrentPosition()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      */
getHandlerForContentObserver()178     /* package */ Handler getHandlerForContentObserver() {
179         return new Handler();
180     }
181 
isTaskRunning()182     private boolean isTaskRunning() {
183         return mLoadMessageListTask != null;
184     }
185 
startTask()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      */
startQuery()195     /* package */ void startQuery() {
196         mLoadMessageListTask = new LoadMessageListTask();
197         mLoadMessageListTask.executeParallel();
198     }
199 
cancelTask()200     private void cancelTask() {
201         Utility.cancelTaskInterrupt(mLoadMessageListTask);
202         mLoadMessageListTask = null;
203     }
204 
closeCursor()205     private void closeCursor() {
206         if (mCursor != null) {
207             mCursor.close();
208             mCursor = null;
209         }
210     }
211 
setCurrentMessageIdFromCursor()212     private void setCurrentMessageIdFromCursor() {
213         if (mCursor != null) {
214             mCurrentMessageId = mCursor.getLong(EmailContent.ID_PROJECTION_COLUMN);
215         }
216     }
217 
onContentChanged()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      */
close()227     public void close() {
228         mClosed = true;
229         mDelayedOperations.removeCallbacks();
230         cancelTask();
231         closeCursor();
232     }
233 
getCurrentMessageId()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      */
moveTo(long messageId)242     public void moveTo(long messageId) {
243         if (mCurrentMessageId != messageId) {
244             mCurrentMessageId = messageId;
245             adjustCursorPosition();
246         }
247     }
248 
adjustCursorPosition()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      */
canMoveToOlder()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      */
canMoveToNewer()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      */
moveToOlder()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      */
moveToNewer()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> {
LoadMessageListTask()325         public LoadMessageListTask() {
326             super(null);
327         }
328 
329         @Override
doInBackground(Void... params)330         protected Cursor doInBackground(Void... params) {
331             return openNewCursor();
332         }
333 
334         @Override
onCancelled(Cursor cursor)335         protected void onCancelled(Cursor cursor) {
336             if (cursor != null) {
337                 cursor.close();
338             }
339             onCursorOpenDone(null);
340         }
341 
342         @Override
onSuccess(Cursor cursor)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      */
openNewCursor()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      */
onCursorOpenDone(Cursor cursor)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