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