Home | History | Annotate | Download | only in activity
      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          */
     82         public void onMessagesChanged();
     83         /**
     84          * Called when the message set by {@link MessageOrderManager#moveTo(long)} is not found.
     85          */
     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 
     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
    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
    121         public void onMessageNotFound() {
    122             mDelayedOperations.post(mOnMessageNotFoundRunnable);
    123         }
    124     }
    125 
    126     public MessageOrderManager(Context context, MessageListContext listContext, Callback callback) {
    127         this(context, listContext, callback, new DelayedOperations(Utility.getMainThreadHandler()));
    128     }
    129 
    130     @VisibleForTesting
    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 
    150     public MessageListContext getListContext() {
    151         return mListContext;
    152     }
    153 
    154     public long getMailboxId() {
    155         return mListContext.getMailboxId();
    156     }
    157 
    158     /**
    159      * @return the total number of messages.
    160      */
    161     public int getTotalMessageCount() {
    162         return mTotalMessageCount;
    163     }
    164 
    165     /**
    166      * @return current cursor position, starting from 0.
    167      */
    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      */
    178     /* package */ Handler getHandlerForContentObserver() {
    179         return new Handler();
    180     }
    181 
    182     private boolean isTaskRunning() {
    183         return mLoadMessageListTask != null;
    184     }
    185 
    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      */
    195     /* package */ void startQuery() {
    196         mLoadMessageListTask = new LoadMessageListTask();
    197         mLoadMessageListTask.executeParallel();
    198     }
    199 
    200     private void cancelTask() {
    201         Utility.cancelTaskInterrupt(mLoadMessageListTask);
    202         mLoadMessageListTask = null;
    203     }
    204 
    205     private void closeCursor() {
    206         if (mCursor != null) {
    207             mCursor.close();
    208             mCursor = null;
    209         }
    210     }
    211 
    212     private void setCurrentMessageIdFromCursor() {
    213         if (mCursor != null) {
    214             mCurrentMessageId = mCursor.getLong(EmailContent.ID_PROJECTION_COLUMN);
    215         }
    216     }
    217 
    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      */
    227     public void close() {
    228         mClosed = true;
    229         mDelayedOperations.removeCallbacks();
    230         cancelTask();
    231         closeCursor();
    232     }
    233 
    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      */
    242     public void moveTo(long messageId) {
    243         if (mCurrentMessageId != messageId) {
    244             mCurrentMessageId = messageId;
    245             adjustCursorPosition();
    246         }
    247     }
    248 
    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      */
    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      */
    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      */
    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      */
    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> {
    325         public LoadMessageListTask() {
    326             super(null);
    327         }
    328 
    329         @Override
    330         protected Cursor doInBackground(Void... params) {
    331             return openNewCursor();
    332         }
    333 
    334         @Override
    335         protected void onCancelled(Cursor cursor) {
    336             if (cursor != null) {
    337                 cursor.close();
    338             }
    339             onCursorOpenDone(null);
    340         }
    341 
    342         @Override
    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      */
    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      */
    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