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