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.Context;
     20 import android.content.Loader;
     21 import android.database.Cursor;
     22 import android.database.CursorWrapper;
     23 import android.os.Bundle;
     24 import android.util.Log;
     25 import android.view.View;
     26 import android.view.ViewGroup;
     27 import android.widget.CursorAdapter;
     28 
     29 import com.android.email.Controller;
     30 import com.android.email.Email;
     31 import com.android.email.MessageListContext;
     32 import com.android.email.ResourceHelper;
     33 import com.android.email.data.ThrottlingCursorLoader;
     34 import com.android.emailcommon.Logging;
     35 import com.android.emailcommon.mail.MessagingException;
     36 import com.android.emailcommon.provider.Account;
     37 import com.android.emailcommon.provider.EmailContent;
     38 import com.android.emailcommon.provider.EmailContent.Message;
     39 import com.android.emailcommon.provider.EmailContent.MessageColumns;
     40 import com.android.emailcommon.provider.Mailbox;
     41 import com.android.emailcommon.utility.TextUtilities;
     42 import com.android.emailcommon.utility.Utility;
     43 import com.google.common.base.Preconditions;
     44 
     45 import java.util.HashSet;
     46 import java.util.Set;
     47 
     48 
     49 /**
     50  * This class implements the adapter for displaying messages based on cursors.
     51  */
     52 /* package */ class MessagesAdapter extends CursorAdapter {
     53     private static final String STATE_CHECKED_ITEMS =
     54             "com.android.email.activity.MessagesAdapter.checkedItems";
     55 
     56     /* package */ static final String[] MESSAGE_PROJECTION = new String[] {
     57         EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
     58         MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP,
     59         MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT,
     60         MessageColumns.FLAGS, MessageColumns.SNIPPET
     61     };
     62 
     63     public static final int COLUMN_ID = 0;
     64     public static final int COLUMN_MAILBOX_KEY = 1;
     65     public static final int COLUMN_ACCOUNT_KEY = 2;
     66     public static final int COLUMN_DISPLAY_NAME = 3;
     67     public static final int COLUMN_SUBJECT = 4;
     68     public static final int COLUMN_DATE = 5;
     69     public static final int COLUMN_READ = 6;
     70     public static final int COLUMN_FAVORITE = 7;
     71     public static final int COLUMN_ATTACHMENTS = 8;
     72     public static final int COLUMN_FLAGS = 9;
     73     public static final int COLUMN_SNIPPET = 10;
     74 
     75     private final ResourceHelper mResourceHelper;
     76 
     77     /** If true, show color chips. */
     78     private boolean mShowColorChips;
     79 
     80     /** If not null, the query represented by this group of messages */
     81     private String mQuery;
     82 
     83     /**
     84      * Set of seleced message IDs.
     85      */
     86     private final HashSet<Long> mSelectedSet = new HashSet<Long>();
     87 
     88     /**
     89      * Callback from MessageListAdapter.  All methods are called on the UI thread.
     90      */
     91     public interface Callback {
     92         /** Called when the use starts/unstars a message */
     93         void onAdapterFavoriteChanged(MessageListItem itemView, boolean newFavorite);
     94         /** Called when the user selects/unselects a message */
     95         void onAdapterSelectedChanged(MessageListItem itemView, boolean newSelected,
     96                 int mSelectedCount);
     97     }
     98 
     99     private final Callback mCallback;
    100 
    101     private ThreePaneLayout mLayout;
    102 
    103     /**
    104      * The actual return type from the loader.
    105      */
    106     public static class MessagesCursor extends CursorWrapper {
    107         /**  Whether the mailbox is found. */
    108         public final boolean mIsFound;
    109         /** {@link Account} that owns the mailbox.  Null for combined mailboxes. */
    110         public final Account mAccount;
    111         /** {@link Mailbox} for the loaded mailbox. Null for combined mailboxes. */
    112         public final Mailbox mMailbox;
    113         /** {@code true} if the account is an EAS account */
    114         public final boolean mIsEasAccount;
    115         /** {@code true} if the loaded mailbox can be refreshed. */
    116         public final boolean mIsRefreshable;
    117         /** the number of accounts currently configured. */
    118         public final int mCountTotalAccounts;
    119 
    120         private MessagesCursor(Cursor cursor,
    121                 boolean found, Account account, Mailbox mailbox, boolean isEasAccount,
    122                 boolean isRefreshable, int countTotalAccounts) {
    123             super(cursor);
    124             mIsFound = found;
    125             mAccount = account;
    126             mMailbox = mailbox;
    127             mIsEasAccount = isEasAccount;
    128             mIsRefreshable = isRefreshable;
    129             mCountTotalAccounts = countTotalAccounts;
    130         }
    131     }
    132 
    133     public MessagesAdapter(Context context, Callback callback) {
    134         super(context.getApplicationContext(), null, 0 /* no auto requery */);
    135         mResourceHelper = ResourceHelper.getInstance(context);
    136         mCallback = callback;
    137     }
    138 
    139     public void setLayout(ThreePaneLayout layout) {
    140         mLayout = layout;
    141     }
    142 
    143     public void onSaveInstanceState(Bundle outState) {
    144         outState.putLongArray(STATE_CHECKED_ITEMS, Utility.toPrimitiveLongArray(getSelectedSet()));
    145     }
    146 
    147     public void loadState(Bundle savedInstanceState) {
    148         Set<Long> checkedset = getSelectedSet();
    149         checkedset.clear();
    150         for (long l: savedInstanceState.getLongArray(STATE_CHECKED_ITEMS)) {
    151             checkedset.add(l);
    152         }
    153         notifyDataSetChanged();
    154     }
    155 
    156     /**
    157      * Set true for combined mailboxes.
    158      */
    159     public void setShowColorChips(boolean show) {
    160         mShowColorChips = show;
    161     }
    162 
    163     public void setQuery(String query) {
    164         mQuery = query;
    165     }
    166 
    167     public Set<Long> getSelectedSet() {
    168         return mSelectedSet;
    169     }
    170 
    171     /**
    172      * Clear the selection.  It's preferable to calling {@link Set#clear()} on
    173      * {@link #getSelectedSet()}, because it also notifies observers.
    174      */
    175     public void clearSelection() {
    176         Set<Long> checkedset = getSelectedSet();
    177         if (checkedset.size() > 0) {
    178             checkedset.clear();
    179             notifyDataSetChanged();
    180         }
    181     }
    182 
    183     public boolean isSelected(MessageListItem itemView) {
    184         return getSelectedSet().contains(itemView.mMessageId);
    185     }
    186 
    187     @Override
    188     public void bindView(View view, Context context, Cursor cursor) {
    189         // Reset the view (in case it was recycled) and prepare for binding
    190         MessageListItem itemView = (MessageListItem) view;
    191         itemView.bindViewInit(this, mLayout);
    192 
    193         // TODO: just move thise all to a MessageListItem.bindTo(cursor) so that the fields can
    194         // be private, and their inter-dependence when they change can be abstracted away.
    195 
    196         // Load the public fields in the view (for later use)
    197         itemView.mMessageId = cursor.getLong(COLUMN_ID);
    198         itemView.mMailboxId = cursor.getLong(COLUMN_MAILBOX_KEY);
    199         final long accountId = cursor.getLong(COLUMN_ACCOUNT_KEY);
    200         itemView.mAccountId = accountId;
    201 
    202         boolean isRead = cursor.getInt(COLUMN_READ) != 0;
    203         boolean readChanged = isRead != itemView.mRead;
    204         itemView.mRead = isRead;
    205         itemView.mIsFavorite = cursor.getInt(COLUMN_FAVORITE) != 0;
    206         final int flags = cursor.getInt(COLUMN_FLAGS);
    207         itemView.mHasInvite = (flags & Message.FLAG_INCOMING_MEETING_INVITE) != 0;
    208         itemView.mHasBeenRepliedTo = (flags & Message.FLAG_REPLIED_TO) != 0;
    209         itemView.mHasBeenForwarded = (flags & Message.FLAG_FORWARDED) != 0;
    210         itemView.mHasAttachment = cursor.getInt(COLUMN_ATTACHMENTS) != 0;
    211         itemView.setTimestamp(cursor.getLong(COLUMN_DATE));
    212         itemView.mSender = cursor.getString(COLUMN_DISPLAY_NAME);
    213         itemView.setText(
    214                 cursor.getString(COLUMN_SUBJECT), cursor.getString(COLUMN_SNIPPET), readChanged);
    215         itemView.mColorChipPaint =
    216             mShowColorChips ? mResourceHelper.getAccountColorPaint(accountId) : null;
    217 
    218         if (mQuery != null && itemView.mSnippet != null) {
    219             itemView.mSnippet =
    220                 TextUtilities.highlightTermsInText(cursor.getString(COLUMN_SNIPPET), mQuery);
    221         }
    222     }
    223 
    224     @Override
    225     public View newView(Context context, Cursor cursor, ViewGroup parent) {
    226         MessageListItem item = new MessageListItem(context);
    227         item.setVisibility(View.VISIBLE);
    228         return item;
    229     }
    230 
    231     public void toggleSelected(MessageListItem itemView) {
    232         updateSelected(itemView, !isSelected(itemView));
    233     }
    234 
    235     /**
    236      * This is used as a callback from the list items, to set the selected state
    237      *
    238      * <p>Must be called on the UI thread.
    239      *
    240      * @param itemView the item being changed
    241      * @param newSelected the new value of the selected flag (checkbox state)
    242      */
    243     private void updateSelected(MessageListItem itemView, boolean newSelected) {
    244         if (newSelected) {
    245             mSelectedSet.add(itemView.mMessageId);
    246         } else {
    247             mSelectedSet.remove(itemView.mMessageId);
    248         }
    249         if (mCallback != null) {
    250             mCallback.onAdapterSelectedChanged(itemView, newSelected, mSelectedSet.size());
    251         }
    252     }
    253 
    254     /**
    255      * This is used as a callback from the list items, to set the favorite state
    256      *
    257      * <p>Must be called on the UI thread.
    258      *
    259      * @param itemView the item being changed
    260      * @param newFavorite the new value of the favorite flag (star state)
    261      */
    262     public void updateFavorite(MessageListItem itemView, boolean newFavorite) {
    263         changeFavoriteIcon(itemView, newFavorite);
    264         if (mCallback != null) {
    265             mCallback.onAdapterFavoriteChanged(itemView, newFavorite);
    266         }
    267     }
    268 
    269     private void changeFavoriteIcon(MessageListItem view, boolean isFavorite) {
    270         view.invalidate();
    271     }
    272 
    273     /**
    274      * Creates the loader for {@link MessageListFragment}.
    275      *
    276      * @return always of {@link MessagesCursor}.
    277      */
    278     public static Loader<Cursor> createLoader(Context context, MessageListContext listContext) {
    279         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    280             Log.d(Logging.LOG_TAG, "MessagesAdapter createLoader listContext=" + listContext);
    281         }
    282         return listContext.isSearch()
    283                 ? new SearchCursorLoader(context, listContext)
    284                 : new MessagesCursorLoader(context, listContext);
    285     }
    286 
    287     private static class MessagesCursorLoader extends ThrottlingCursorLoader {
    288         protected final Context mContext;
    289         private final long mAccountId;
    290         private final long mMailboxId;
    291 
    292         public MessagesCursorLoader(Context context, MessageListContext listContext) {
    293             // Initialize with no where clause.  We'll set it later.
    294             super(context, EmailContent.Message.CONTENT_URI,
    295                     MESSAGE_PROJECTION, null, null,
    296                     EmailContent.MessageColumns.TIMESTAMP + " DESC");
    297             mContext = context;
    298             mAccountId = listContext.mAccountId;
    299             mMailboxId = listContext.getMailboxId();
    300         }
    301 
    302         @Override
    303         public Cursor loadInBackground() {
    304             // Build the where cause (which can't be done on the UI thread.)
    305             setSelection(Message.buildMessageListSelection(mContext, mAccountId, mMailboxId));
    306             // Then do a query to get the cursor
    307             return loadExtras(super.loadInBackground());
    308         }
    309 
    310         private Cursor loadExtras(Cursor baseCursor) {
    311             boolean found = false;
    312             Account account = null;
    313             Mailbox mailbox = null;
    314             boolean isEasAccount = false;
    315             boolean isRefreshable = false;
    316 
    317             if (mMailboxId < 0) {
    318                 // Magic mailbox.
    319                 found = true;
    320             } else {
    321                 mailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId);
    322                 if (mailbox != null) {
    323                     account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey);
    324                     if (account != null) {
    325                         found = true;
    326                         isEasAccount = account.isEasAccount(mContext) ;
    327                         isRefreshable = Mailbox.isRefreshable(mContext, mMailboxId);
    328                     } else { // Account removed?
    329                         mailbox = null;
    330                     }
    331                 }
    332             }
    333             final int countAccounts = EmailContent.count(mContext, Account.CONTENT_URI);
    334             return wrapCursor(baseCursor, found, account, mailbox, isEasAccount,
    335                     isRefreshable, countAccounts);
    336         }
    337 
    338         /**
    339          * Wraps a basic cursor containing raw messages with information about the context of
    340          * the list that's being loaded, such as the account and the mailbox the messages
    341          * are for.
    342          * Subclasses may extend this to wrap with additional data.
    343          */
    344         protected Cursor wrapCursor(Cursor cursor,
    345                 boolean found, Account account, Mailbox mailbox, boolean isEasAccount,
    346                 boolean isRefreshable, int countTotalAccounts) {
    347             return new MessagesCursor(cursor, found, account, mailbox, isEasAccount,
    348                     isRefreshable, countTotalAccounts);
    349         }
    350     }
    351 
    352     public static class SearchResultsCursor extends MessagesCursor {
    353         private final Mailbox mSearchedMailbox;
    354         private final int mResultsCount;
    355         private SearchResultsCursor(Cursor cursor,
    356                 boolean found, Account account, Mailbox mailbox, boolean isEasAccount,
    357                 boolean isRefreshable, int countTotalAccounts,
    358                 Mailbox searchedMailbox, int resultsCount) {
    359             super(cursor, found, account, mailbox, isEasAccount,
    360                     isRefreshable, countTotalAccounts);
    361             mSearchedMailbox = searchedMailbox;
    362             mResultsCount = resultsCount;
    363         }
    364 
    365         /**
    366          * @return the total number of results that match the given search query. Note that
    367          *     there may not be that many items loaded in the cursor yet.
    368          */
    369         public int getResultsCount() {
    370             return mResultsCount;
    371         }
    372 
    373         public Mailbox getSearchedMailbox() {
    374             return mSearchedMailbox;
    375         }
    376     }
    377 
    378     /**
    379      * A special loader used to perform a search.
    380      */
    381     private static class SearchCursorLoader extends MessagesCursorLoader {
    382         private final MessageListContext mListContext;
    383         private int mResultsCount = -1;
    384         private Mailbox mSearchedMailbox = null;
    385 
    386         public SearchCursorLoader(Context context, MessageListContext listContext) {
    387             super(context, listContext);
    388             Preconditions.checkArgument(listContext.isSearch());
    389             mListContext = listContext;
    390         }
    391 
    392         @Override
    393         public Cursor loadInBackground() {
    394             if (mResultsCount >= 0) {
    395                 // Result count known - the initial search meta data must have completed.
    396                 return super.loadInBackground();
    397             }
    398 
    399             if (mSearchedMailbox == null) {
    400                 mSearchedMailbox = Mailbox.restoreMailboxWithId(
    401                         mContext, mListContext.getSearchedMailbox());
    402             }
    403 
    404             // The search results info hasn't even been loaded yet, so the Controller has not yet
    405             // initialized the search mailbox properly. Kick off the search first.
    406             Controller controller = Controller.getInstance(mContext);
    407             try {
    408                 mResultsCount = controller.searchMessages(
    409                         mListContext.mAccountId, mListContext.getSearchParams());
    410             } catch (MessagingException e) {
    411             }
    412 
    413             // Return whatever the super would do, now that we know the results are ready.
    414             // After this point, it should behave as a normal mailbox load for messages.
    415             return super.loadInBackground();
    416         }
    417 
    418         @Override
    419         protected Cursor wrapCursor(Cursor cursor,
    420                 boolean found, Account account, Mailbox mailbox, boolean isEasAccount,
    421                 boolean isRefreshable, int countTotalAccounts) {
    422             return new SearchResultsCursor(cursor, found, account, mailbox, isEasAccount,
    423                     isRefreshable, countTotalAccounts, mSearchedMailbox, mResultsCount);
    424         }
    425     }
    426 }
    427