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