| /* |
| * 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.Context; |
| import android.content.Loader; |
| import android.database.Cursor; |
| import android.database.CursorWrapper; |
| import android.os.Bundle; |
| import android.util.Log; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.CursorAdapter; |
| |
| import com.android.email.Controller; |
| import com.android.email.Email; |
| import com.android.email.MessageListContext; |
| import com.android.email.ResourceHelper; |
| import com.android.email.data.ThrottlingCursorLoader; |
| import com.android.emailcommon.Logging; |
| import com.android.emailcommon.mail.MessagingException; |
| import com.android.emailcommon.provider.Account; |
| import com.android.emailcommon.provider.EmailContent; |
| import com.android.emailcommon.provider.EmailContent.Message; |
| import com.android.emailcommon.provider.EmailContent.MessageColumns; |
| import com.android.emailcommon.provider.Mailbox; |
| import com.android.emailcommon.utility.TextUtilities; |
| import com.android.emailcommon.utility.Utility; |
| import com.google.common.base.Preconditions; |
| |
| import java.util.HashSet; |
| import java.util.Set; |
| |
| |
| /** |
| * This class implements the adapter for displaying messages based on cursors. |
| */ |
| /* package */ class MessagesAdapter extends CursorAdapter { |
| private static final String STATE_CHECKED_ITEMS = |
| "com.android.email.activity.MessagesAdapter.checkedItems"; |
| |
| /* package */ static final String[] MESSAGE_PROJECTION = new String[] { |
| EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY, |
| MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP, |
| MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT, |
| MessageColumns.FLAGS, MessageColumns.SNIPPET |
| }; |
| |
| public static final int COLUMN_ID = 0; |
| public static final int COLUMN_MAILBOX_KEY = 1; |
| public static final int COLUMN_ACCOUNT_KEY = 2; |
| public static final int COLUMN_DISPLAY_NAME = 3; |
| public static final int COLUMN_SUBJECT = 4; |
| public static final int COLUMN_DATE = 5; |
| public static final int COLUMN_READ = 6; |
| public static final int COLUMN_FAVORITE = 7; |
| public static final int COLUMN_ATTACHMENTS = 8; |
| public static final int COLUMN_FLAGS = 9; |
| public static final int COLUMN_SNIPPET = 10; |
| |
| private final ResourceHelper mResourceHelper; |
| |
| /** If true, show color chips. */ |
| private boolean mShowColorChips; |
| |
| /** If not null, the query represented by this group of messages */ |
| private String mQuery; |
| |
| /** |
| * Set of seleced message IDs. |
| */ |
| private final HashSet<Long> mSelectedSet = new HashSet<Long>(); |
| |
| /** |
| * Callback from MessageListAdapter. All methods are called on the UI thread. |
| */ |
| public interface Callback { |
| /** Called when the use starts/unstars a message */ |
| void onAdapterFavoriteChanged(MessageListItem itemView, boolean newFavorite); |
| /** Called when the user selects/unselects a message */ |
| void onAdapterSelectedChanged(MessageListItem itemView, boolean newSelected, |
| int mSelectedCount); |
| } |
| |
| private final Callback mCallback; |
| |
| private ThreePaneLayout mLayout; |
| |
| private boolean mIsSearchResult = false; |
| |
| /** |
| * The actual return type from the loader. |
| */ |
| public static class MessagesCursor extends CursorWrapper { |
| /** Whether the mailbox is found. */ |
| public final boolean mIsFound; |
| /** {@link Account} that owns the mailbox. Null for combined mailboxes. */ |
| public final Account mAccount; |
| /** {@link Mailbox} for the loaded mailbox. Null for combined mailboxes. */ |
| public final Mailbox mMailbox; |
| /** {@code true} if the account is an EAS account */ |
| public final boolean mIsEasAccount; |
| /** {@code true} if the loaded mailbox can be refreshed. */ |
| public final boolean mIsRefreshable; |
| /** the number of accounts currently configured. */ |
| public final int mCountTotalAccounts; |
| |
| private MessagesCursor(Cursor cursor, |
| boolean found, Account account, Mailbox mailbox, boolean isEasAccount, |
| boolean isRefreshable, int countTotalAccounts) { |
| super(cursor); |
| mIsFound = found; |
| mAccount = account; |
| mMailbox = mailbox; |
| mIsEasAccount = isEasAccount; |
| mIsRefreshable = isRefreshable; |
| mCountTotalAccounts = countTotalAccounts; |
| } |
| } |
| |
| public MessagesAdapter(Context context, Callback callback, boolean isSearchResult) { |
| super(context.getApplicationContext(), null, 0 /* no auto requery */); |
| mResourceHelper = ResourceHelper.getInstance(context); |
| mCallback = callback; |
| mIsSearchResult = isSearchResult; |
| } |
| |
| public void setLayout(ThreePaneLayout layout) { |
| mLayout = layout; |
| } |
| |
| public void onSaveInstanceState(Bundle outState) { |
| outState.putLongArray(STATE_CHECKED_ITEMS, Utility.toPrimitiveLongArray(getSelectedSet())); |
| } |
| |
| public void loadState(Bundle savedInstanceState) { |
| Set<Long> checkedset = getSelectedSet(); |
| checkedset.clear(); |
| for (long l: savedInstanceState.getLongArray(STATE_CHECKED_ITEMS)) { |
| checkedset.add(l); |
| } |
| notifyDataSetChanged(); |
| } |
| |
| /** |
| * Set true for combined mailboxes. |
| */ |
| public void setShowColorChips(boolean show) { |
| mShowColorChips = show; |
| } |
| |
| public void setQuery(String query) { |
| mQuery = query; |
| } |
| |
| public Set<Long> getSelectedSet() { |
| return mSelectedSet; |
| } |
| |
| /** |
| * Clear the selection. It's preferable to calling {@link Set#clear()} on |
| * {@link #getSelectedSet()}, because it also notifies observers. |
| */ |
| public void clearSelection() { |
| Set<Long> checkedset = getSelectedSet(); |
| if (checkedset.size() > 0) { |
| checkedset.clear(); |
| notifyDataSetChanged(); |
| } |
| } |
| |
| public boolean isSelected(MessageListItem itemView) { |
| return getSelectedSet().contains(itemView.mMessageId); |
| } |
| |
| @Override |
| public void bindView(View view, Context context, Cursor cursor) { |
| // Reset the view (in case it was recycled) and prepare for binding |
| MessageListItem itemView = (MessageListItem) view; |
| itemView.bindViewInit(this, mLayout, mIsSearchResult); |
| |
| // TODO: just move thise all to a MessageListItem.bindTo(cursor) so that the fields can |
| // be private, and their inter-dependence when they change can be abstracted away. |
| |
| // Load the public fields in the view (for later use) |
| itemView.mMessageId = cursor.getLong(COLUMN_ID); |
| itemView.mMailboxId = cursor.getLong(COLUMN_MAILBOX_KEY); |
| final long accountId = cursor.getLong(COLUMN_ACCOUNT_KEY); |
| itemView.mAccountId = accountId; |
| |
| boolean isRead = cursor.getInt(COLUMN_READ) != 0; |
| boolean readChanged = isRead != itemView.mRead; |
| itemView.mRead = isRead; |
| itemView.mIsFavorite = cursor.getInt(COLUMN_FAVORITE) != 0; |
| final int flags = cursor.getInt(COLUMN_FLAGS); |
| itemView.mHasInvite = (flags & Message.FLAG_INCOMING_MEETING_INVITE) != 0; |
| itemView.mHasBeenRepliedTo = (flags & Message.FLAG_REPLIED_TO) != 0; |
| itemView.mHasBeenForwarded = (flags & Message.FLAG_FORWARDED) != 0; |
| itemView.mHasAttachment = cursor.getInt(COLUMN_ATTACHMENTS) != 0; |
| itemView.setTimestamp(cursor.getLong(COLUMN_DATE)); |
| itemView.mSender = cursor.getString(COLUMN_DISPLAY_NAME); |
| itemView.setText( |
| cursor.getString(COLUMN_SUBJECT), cursor.getString(COLUMN_SNIPPET), readChanged); |
| itemView.mColorChipPaint = |
| mShowColorChips ? mResourceHelper.getAccountColorPaint(accountId) : null; |
| |
| if (mQuery != null && itemView.mSnippet != null) { |
| itemView.mSnippet = |
| TextUtilities.highlightTermsInText(cursor.getString(COLUMN_SNIPPET), mQuery); |
| } |
| } |
| |
| @Override |
| public View newView(Context context, Cursor cursor, ViewGroup parent) { |
| MessageListItem item = new MessageListItem(context); |
| item.setVisibility(View.VISIBLE); |
| return item; |
| } |
| |
| public void toggleSelected(MessageListItem itemView) { |
| updateSelected(itemView, !isSelected(itemView)); |
| } |
| |
| /** |
| * This is used as a callback from the list items, to set the selected state |
| * |
| * <p>Must be called on the UI thread. |
| * |
| * @param itemView the item being changed |
| * @param newSelected the new value of the selected flag (checkbox state) |
| */ |
| private void updateSelected(MessageListItem itemView, boolean newSelected) { |
| if (newSelected) { |
| mSelectedSet.add(itemView.mMessageId); |
| } else { |
| mSelectedSet.remove(itemView.mMessageId); |
| } |
| if (mCallback != null) { |
| mCallback.onAdapterSelectedChanged(itemView, newSelected, mSelectedSet.size()); |
| } |
| } |
| |
| /** |
| * This is used as a callback from the list items, to set the favorite state |
| * |
| * <p>Must be called on the UI thread. |
| * |
| * @param itemView the item being changed |
| * @param newFavorite the new value of the favorite flag (star state) |
| */ |
| public void updateFavorite(MessageListItem itemView, boolean newFavorite) { |
| changeFavoriteIcon(itemView, newFavorite); |
| if (mCallback != null) { |
| mCallback.onAdapterFavoriteChanged(itemView, newFavorite); |
| } |
| } |
| |
| private void changeFavoriteIcon(MessageListItem view, boolean isFavorite) { |
| view.invalidate(); |
| } |
| |
| /** |
| * Creates the loader for {@link MessageListFragment}. |
| * |
| * @return always of {@link MessagesCursor}. |
| */ |
| public static Loader<Cursor> createLoader(Context context, MessageListContext listContext) { |
| if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { |
| Log.d(Logging.LOG_TAG, "MessagesAdapter createLoader listContext=" + listContext); |
| } |
| return listContext.isSearch() |
| ? new SearchCursorLoader(context, listContext) |
| : new MessagesCursorLoader(context, listContext); |
| } |
| |
| private static class MessagesCursorLoader extends ThrottlingCursorLoader { |
| protected final Context mContext; |
| private final long mAccountId; |
| private final long mMailboxId; |
| |
| public MessagesCursorLoader(Context context, MessageListContext listContext) { |
| // Initialize with no where clause. We'll set it later. |
| super(context, EmailContent.Message.CONTENT_URI, |
| MESSAGE_PROJECTION, null, null, |
| EmailContent.MessageColumns.TIMESTAMP + " DESC"); |
| mContext = context; |
| mAccountId = listContext.mAccountId; |
| mMailboxId = listContext.getMailboxId(); |
| } |
| |
| @Override |
| public Cursor loadInBackground() { |
| // Build the where cause (which can't be done on the UI thread.) |
| setSelection(Message.buildMessageListSelection(mContext, mAccountId, mMailboxId)); |
| // Then do a query to get the cursor |
| return loadExtras(super.loadInBackground()); |
| } |
| |
| private Cursor loadExtras(Cursor baseCursor) { |
| boolean found = false; |
| Account account = null; |
| Mailbox mailbox = null; |
| boolean isEasAccount = false; |
| boolean isRefreshable = false; |
| |
| if (mMailboxId < 0) { |
| // Magic mailbox. |
| found = true; |
| } else { |
| mailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId); |
| if (mailbox != null) { |
| account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey); |
| if (account != null) { |
| found = true; |
| isEasAccount = account.isEasAccount(mContext) ; |
| isRefreshable = Mailbox.isRefreshable(mContext, mMailboxId); |
| } else { // Account removed? |
| mailbox = null; |
| } |
| } |
| } |
| final int countAccounts = EmailContent.count(mContext, Account.CONTENT_URI); |
| return wrapCursor(baseCursor, found, account, mailbox, isEasAccount, |
| isRefreshable, countAccounts); |
| } |
| |
| /** |
| * Wraps a basic cursor containing raw messages with information about the context of |
| * the list that's being loaded, such as the account and the mailbox the messages |
| * are for. |
| * Subclasses may extend this to wrap with additional data. |
| */ |
| protected Cursor wrapCursor(Cursor cursor, |
| boolean found, Account account, Mailbox mailbox, boolean isEasAccount, |
| boolean isRefreshable, int countTotalAccounts) { |
| return new MessagesCursor(cursor, found, account, mailbox, isEasAccount, |
| isRefreshable, countTotalAccounts); |
| } |
| } |
| |
| public static class SearchResultsCursor extends MessagesCursor { |
| private final Mailbox mSearchedMailbox; |
| private final int mResultsCount; |
| private SearchResultsCursor(Cursor cursor, |
| boolean found, Account account, Mailbox mailbox, boolean isEasAccount, |
| boolean isRefreshable, int countTotalAccounts, |
| Mailbox searchedMailbox, int resultsCount) { |
| super(cursor, found, account, mailbox, isEasAccount, |
| isRefreshable, countTotalAccounts); |
| mSearchedMailbox = searchedMailbox; |
| mResultsCount = resultsCount; |
| } |
| |
| /** |
| * @return the total number of results that match the given search query. Note that |
| * there may not be that many items loaded in the cursor yet. |
| */ |
| public int getResultsCount() { |
| return mResultsCount; |
| } |
| |
| public Mailbox getSearchedMailbox() { |
| return mSearchedMailbox; |
| } |
| } |
| |
| /** |
| * A special loader used to perform a search. |
| */ |
| private static class SearchCursorLoader extends MessagesCursorLoader { |
| private final MessageListContext mListContext; |
| private int mResultsCount = -1; |
| private Mailbox mSearchedMailbox = null; |
| |
| public SearchCursorLoader(Context context, MessageListContext listContext) { |
| super(context, listContext); |
| Preconditions.checkArgument(listContext.isSearch()); |
| mListContext = listContext; |
| } |
| |
| @Override |
| public Cursor loadInBackground() { |
| if (mResultsCount >= 0) { |
| // Result count known - the initial search meta data must have completed. |
| return super.loadInBackground(); |
| } |
| |
| if (mSearchedMailbox == null) { |
| mSearchedMailbox = Mailbox.restoreMailboxWithId( |
| mContext, mListContext.getSearchedMailbox()); |
| } |
| |
| // The search results info hasn't even been loaded yet, so the Controller has not yet |
| // initialized the search mailbox properly. Kick off the search first. |
| Controller controller = Controller.getInstance(mContext); |
| try { |
| mResultsCount = controller.searchMessages( |
| mListContext.mAccountId, mListContext.getSearchParams()); |
| } catch (MessagingException e) { |
| } |
| |
| // Return whatever the super would do, now that we know the results are ready. |
| // After this point, it should behave as a normal mailbox load for messages. |
| return super.loadInBackground(); |
| } |
| |
| @Override |
| protected Cursor wrapCursor(Cursor cursor, |
| boolean found, Account account, Mailbox mailbox, boolean isEasAccount, |
| boolean isRefreshable, int countTotalAccounts) { |
| return new SearchResultsCursor(cursor, found, account, mailbox, isEasAccount, |
| isRefreshable, countTotalAccounts, mSearchedMailbox, mResultsCount); |
| } |
| } |
| } |