blob: fffcf7c3b1a9ed64a99660d5e7151071485237f8 [file] [log] [blame]
/*
* 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.
}
}
}