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