• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
onAdapterFavoriteChanged(MessageListItem itemView, boolean newFavorite)93         void onAdapterFavoriteChanged(MessageListItem itemView, boolean newFavorite);
94         /** Called when the user selects/unselects a message */
onAdapterSelectedChanged(MessageListItem itemView, boolean newSelected, int mSelectedCount)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 
MessagesCursor(Cursor cursor, boolean found, Account account, Mailbox mailbox, boolean isEasAccount, boolean isRefreshable, int countTotalAccounts)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 
MessagesAdapter(Context context, Callback callback, boolean isSearchResult)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 
setLayout(ThreePaneLayout layout)142     public void setLayout(ThreePaneLayout layout) {
143         mLayout = layout;
144     }
145 
onSaveInstanceState(Bundle outState)146     public void onSaveInstanceState(Bundle outState) {
147         outState.putLongArray(STATE_CHECKED_ITEMS, Utility.toPrimitiveLongArray(getSelectedSet()));
148     }
149 
loadState(Bundle savedInstanceState)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      */
setShowColorChips(boolean show)162     public void setShowColorChips(boolean show) {
163         mShowColorChips = show;
164     }
165 
setQuery(String query)166     public void setQuery(String query) {
167         mQuery = query;
168     }
169 
getSelectedSet()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      */
clearSelection()178     public void clearSelection() {
179         Set<Long> checkedset = getSelectedSet();
180         if (checkedset.size() > 0) {
181             checkedset.clear();
182             notifyDataSetChanged();
183         }
184     }
185 
isSelected(MessageListItem itemView)186     public boolean isSelected(MessageListItem itemView) {
187         return getSelectedSet().contains(itemView.mMessageId);
188     }
189 
190     @Override
bindView(View view, Context context, Cursor cursor)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
newView(Context context, Cursor cursor, ViewGroup parent)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 
toggleSelected(MessageListItem itemView)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      */
updateSelected(MessageListItem itemView, boolean newSelected)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      */
updateFavorite(MessageListItem itemView, boolean newFavorite)265     public void updateFavorite(MessageListItem itemView, boolean newFavorite) {
266         changeFavoriteIcon(itemView, newFavorite);
267         if (mCallback != null) {
268             mCallback.onAdapterFavoriteChanged(itemView, newFavorite);
269         }
270     }
271 
changeFavoriteIcon(MessageListItem view, boolean isFavorite)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      */
createLoader(Context context, MessageListContext listContext)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 
MessagesCursorLoader(Context context, MessageListContext listContext)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
loadInBackground()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 
loadExtras(Cursor baseCursor)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          */
wrapCursor(Cursor cursor, boolean found, Account account, Mailbox mailbox, boolean isEasAccount, boolean isRefreshable, int countTotalAccounts)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;
SearchResultsCursor(Cursor cursor, boolean found, Account account, Mailbox mailbox, boolean isEasAccount, boolean isRefreshable, int countTotalAccounts, Mailbox searchedMailbox, int resultsCount)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          */
getResultsCount()372         public int getResultsCount() {
373             return mResultsCount;
374         }
375 
getSearchedMailbox()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 
SearchCursorLoader(Context context, MessageListContext listContext)389         public SearchCursorLoader(Context context, MessageListContext listContext) {
390             super(context, listContext);
391             Preconditions.checkArgument(listContext.isSearch());
392             mListContext = listContext;
393         }
394 
395         @Override
loadInBackground()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
wrapCursor(Cursor cursor, boolean found, Account account, Mailbox mailbox, boolean isEasAccount, boolean isRefreshable, int countTotalAccounts)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