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