| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.email.activity; |
| |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.database.ContentObserver; |
| import android.database.Cursor; |
| import android.os.Handler; |
| |
| import com.android.email.MessageListContext; |
| import com.android.email.activity.MessageOrderManager.Callback; |
| import com.android.emailcommon.provider.EmailContent; |
| import com.android.emailcommon.provider.EmailContent.Message; |
| import com.android.emailcommon.provider.Mailbox; |
| import com.android.emailcommon.utility.DelayedOperations; |
| import com.android.emailcommon.utility.EmailAsyncTask; |
| import com.android.emailcommon.utility.Utility; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Preconditions; |
| |
| /** |
| * Used by {@link MessageView} to determine the message-id of the previous/next messages. |
| * |
| * All public methods must be called on the main thread. |
| * |
| * Call {@link #moveTo} to set the current message id. As a result, |
| * either {@link Callback#onMessagesChanged} or {@link Callback#onMessageNotFound} is called. |
| * |
| * Use {@link #canMoveToNewer()} and {@link #canMoveToOlder()} to see if there is a newer/older |
| * message, and {@link #moveToNewer()} and {@link #moveToOlder()} to update the current position. |
| * |
| * If the message list changes (e.g. message removed, new message arrived, etc), {@link Callback} |
| * gets called again. |
| * |
| * When an instance is no longer needed, call {@link #close()}, which closes an underlying cursor |
| * and shuts down an async task. |
| * |
| * TODO: Is there better words than "newer"/"older" that works even if we support other sort orders |
| * than timestamp? |
| */ |
| public class MessageOrderManager { |
| private final Context mContext; |
| private final ContentResolver mContentResolver; |
| |
| private final MessageListContext mListContext; |
| private final ContentObserver mObserver; |
| private final Callback mCallback; |
| private final DelayedOperations mDelayedOperations; |
| |
| private LoadMessageListTask mLoadMessageListTask; |
| private Cursor mCursor; |
| |
| private long mCurrentMessageId = -1; |
| |
| private int mTotalMessageCount; |
| |
| private int mCurrentPosition; |
| |
| private boolean mClosed = false; |
| |
| public interface Callback { |
| /** |
| * Called when the message set by {@link MessageOrderManager#moveTo(long)} is found in the |
| * mailbox. {@link #canMoveToOlder}, {@link #canMoveToNewer}, {@link #moveToOlder} and |
| * {@link #moveToNewer} are ready to be called. |
| */ |
| public void onMessagesChanged(); |
| /** |
| * Called when the message set by {@link MessageOrderManager#moveTo(long)} is not found. |
| */ |
| public void onMessageNotFound(); |
| } |
| |
| /** |
| * Wrapper for {@link Callback}, which uses {@link DelayedOperations#post(Runnable)} to |
| * kick callbacks rather than calling them directly. This is used to avoid the "nested fragment |
| * transaction" exception. e.g. {@link #moveTo} is often called during a fragment transaction, |
| * and if the message no longer exists we call {@link #onMessageNotFound}, which most probably |
| * triggers another fragment transaction. |
| */ |
| private class PostingCallback implements Callback { |
| private final Callback mOriginal; |
| |
| private PostingCallback(Callback original) { |
| mOriginal = original; |
| } |
| |
| private final Runnable mOnMessagesChangedRunnable = new Runnable() { |
| @Override public void run() { |
| mOriginal.onMessagesChanged(); |
| } |
| }; |
| |
| @Override |
| public void onMessagesChanged() { |
| mDelayedOperations.post(mOnMessagesChangedRunnable); |
| } |
| |
| private final Runnable mOnMessageNotFoundRunnable = new Runnable() { |
| @Override public void run() { |
| mOriginal.onMessageNotFound(); |
| } |
| }; |
| |
| @Override |
| public void onMessageNotFound() { |
| mDelayedOperations.post(mOnMessageNotFoundRunnable); |
| } |
| } |
| |
| public MessageOrderManager(Context context, MessageListContext listContext, Callback callback) { |
| this(context, listContext, callback, new DelayedOperations(Utility.getMainThreadHandler())); |
| } |
| |
| @VisibleForTesting |
| MessageOrderManager(Context context, MessageListContext listContext, Callback callback, |
| DelayedOperations delayedOperations) { |
| Preconditions.checkArgument(listContext.getMailboxId() != Mailbox.NO_MAILBOX); |
| mContext = context.getApplicationContext(); |
| mContentResolver = mContext.getContentResolver(); |
| mDelayedOperations = delayedOperations; |
| mListContext = listContext; |
| mCallback = new PostingCallback(callback); |
| mObserver = new ContentObserver(getHandlerForContentObserver()) { |
| @Override public void onChange(boolean selfChange) { |
| if (mClosed) { |
| return; |
| } |
| onContentChanged(); |
| } |
| }; |
| startTask(); |
| } |
| |
| public MessageListContext getListContext() { |
| return mListContext; |
| } |
| |
| public long getMailboxId() { |
| return mListContext.getMailboxId(); |
| } |
| |
| /** |
| * @return the total number of messages. |
| */ |
| public int getTotalMessageCount() { |
| return mTotalMessageCount; |
| } |
| |
| /** |
| * @return current cursor position, starting from 0. |
| */ |
| public int getCurrentPosition() { |
| return mCurrentPosition; |
| } |
| |
| /** |
| * @return a {@link Handler} for {@link ContentObserver}. |
| * |
| * Unit tests override this and return null, so that {@link ContentObserver#onChange} is |
| * called synchronously. |
| */ |
| /* package */ Handler getHandlerForContentObserver() { |
| return new Handler(); |
| } |
| |
| private boolean isTaskRunning() { |
| return mLoadMessageListTask != null; |
| } |
| |
| private void startTask() { |
| cancelTask(); |
| startQuery(); |
| } |
| |
| /** |
| * Start {@link LoadMessageListTask} to query DB. |
| * Unit tests override this to make tests synchronous and to inject a mock query. |
| */ |
| /* package */ void startQuery() { |
| mLoadMessageListTask = new LoadMessageListTask(); |
| mLoadMessageListTask.executeParallel(); |
| } |
| |
| private void cancelTask() { |
| Utility.cancelTaskInterrupt(mLoadMessageListTask); |
| mLoadMessageListTask = null; |
| } |
| |
| private void closeCursor() { |
| if (mCursor != null) { |
| mCursor.close(); |
| mCursor = null; |
| } |
| } |
| |
| private void setCurrentMessageIdFromCursor() { |
| if (mCursor != null) { |
| mCurrentMessageId = mCursor.getLong(EmailContent.ID_PROJECTION_COLUMN); |
| } |
| } |
| |
| private void onContentChanged() { |
| if (!isTaskRunning()) { // Start only if not running already. |
| startTask(); |
| } |
| } |
| |
| /** |
| * Shutdown itself and release resources. |
| */ |
| public void close() { |
| mClosed = true; |
| mDelayedOperations.removeCallbacks(); |
| cancelTask(); |
| closeCursor(); |
| } |
| |
| public long getCurrentMessageId() { |
| return mCurrentMessageId; |
| } |
| |
| /** |
| * Set the current message id. As a result, either {@link Callback#onMessagesChanged} or |
| * {@link Callback#onMessageNotFound} is called. |
| */ |
| public void moveTo(long messageId) { |
| if (mCurrentMessageId != messageId) { |
| mCurrentMessageId = messageId; |
| adjustCursorPosition(); |
| } |
| } |
| |
| private void adjustCursorPosition() { |
| mCurrentPosition = 0; |
| if (mCurrentMessageId == -1) { |
| return; // Current ID not specified yet. |
| } |
| if (mCursor == null) { |
| // Task not finished yet. |
| // We call adjustCursorPosition() again when we've opened a cursor. |
| return; |
| } |
| mCursor.moveToPosition(-1); |
| while (mCursor.moveToNext() |
| && mCursor.getLong(EmailContent.ID_PROJECTION_COLUMN) != mCurrentMessageId) { |
| mCurrentPosition++; |
| } |
| if (mCursor.isAfterLast()) { |
| mCurrentPosition = 0; |
| mCallback.onMessageNotFound(); // Message not found... Already deleted? |
| } else { |
| mCallback.onMessagesChanged(); |
| } |
| } |
| |
| /** |
| * @return true if the message set to {@link #moveTo} has an older message in the mailbox. |
| * false otherwise, or unknown yet. |
| */ |
| public boolean canMoveToOlder() { |
| return (mCursor != null) && !mCursor.isLast(); |
| } |
| |
| |
| /** |
| * @return true if the message set to {@link #moveTo} has an newer message in the mailbox. |
| * false otherwise, or unknown yet. |
| */ |
| public boolean canMoveToNewer() { |
| return (mCursor != null) && !mCursor.isFirst(); |
| } |
| |
| /** |
| * Move to the older message. |
| * |
| * @return true iif succeed, and {@link Callback#onMessagesChanged} is called. |
| */ |
| public boolean moveToOlder() { |
| if (canMoveToOlder() && mCursor.moveToNext()) { |
| mCurrentPosition++; |
| setCurrentMessageIdFromCursor(); |
| mCallback.onMessagesChanged(); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| /** |
| * Move to the newer message. |
| * |
| * @return true iif succeed, and {@link Callback#onMessagesChanged} is called. |
| */ |
| public boolean moveToNewer() { |
| if (canMoveToNewer() && mCursor.moveToPrevious()) { |
| mCurrentPosition--; |
| setCurrentMessageIdFromCursor(); |
| mCallback.onMessagesChanged(); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| /** |
| * Task to open a Cursor on a worker thread. |
| */ |
| private class LoadMessageListTask extends EmailAsyncTask<Void, Void, Cursor> { |
| public LoadMessageListTask() { |
| super(null); |
| } |
| |
| @Override |
| protected Cursor doInBackground(Void... params) { |
| return openNewCursor(); |
| } |
| |
| @Override |
| protected void onCancelled(Cursor cursor) { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| onCursorOpenDone(null); |
| } |
| |
| @Override |
| protected void onSuccess(Cursor cursor) { |
| onCursorOpenDone(cursor); |
| } |
| } |
| |
| /** |
| * Open a new cursor for a message list. |
| * |
| * This method is called on a worker thread by LoadMessageListTask. |
| */ |
| private Cursor openNewCursor() { |
| final Cursor cursor = mContentResolver.query(EmailContent.Message.CONTENT_URI, |
| EmailContent.ID_PROJECTION, |
| Message.buildMessageListSelection( |
| mContext, mListContext.mAccountId, mListContext.getMailboxId()), |
| null, EmailContent.MessageColumns.TIMESTAMP + " DESC"); |
| return cursor; |
| } |
| |
| /** |
| * Called when {@link #openNewCursor()} is finished. |
| * |
| * Unit tests call this directly to inject a mock cursor. |
| */ |
| /* package */ void onCursorOpenDone(Cursor cursor) { |
| try { |
| closeCursor(); |
| if (cursor == null || cursor.isClosed()) { |
| mTotalMessageCount = 0; |
| mCurrentPosition = 0; |
| return; // Task canceled |
| } |
| mCursor = cursor; |
| mTotalMessageCount = mCursor.getCount(); |
| mCursor.registerContentObserver(mObserver); |
| adjustCursorPosition(); |
| } finally { |
| mLoadMessageListTask = null; // isTaskRunning() becomes false. |
| } |
| } |
| } |