MessagesAdapter.java revision 8e779e627a2185320ab168fdce09477a73bf5f22
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
17package com.android.email.activity;
18
19import android.content.Context;
20import android.content.Loader;
21import android.database.Cursor;
22import android.database.CursorWrapper;
23import android.os.Bundle;
24import android.util.Log;
25import android.view.View;
26import android.view.ViewGroup;
27import android.widget.CursorAdapter;
28
29import com.android.email.Controller;
30import com.android.email.Email;
31import com.android.email.MessageListContext;
32import com.android.email.ResourceHelper;
33import com.android.email.data.ThrottlingCursorLoader;
34import com.android.emailcommon.Logging;
35import com.android.emailcommon.mail.MessagingException;
36import com.android.emailcommon.provider.Account;
37import com.android.emailcommon.provider.EmailContent;
38import com.android.emailcommon.provider.EmailContent.Message;
39import com.android.emailcommon.provider.EmailContent.MessageColumns;
40import com.android.emailcommon.provider.Mailbox;
41import com.android.emailcommon.utility.TextUtilities;
42import com.android.emailcommon.utility.Utility;
43import com.google.common.base.Preconditions;
44
45import java.util.HashSet;
46import 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        // Load the public fields in the view (for later use)
188        itemView.mMessageId = cursor.getLong(COLUMN_ID);
189        itemView.mMailboxId = cursor.getLong(COLUMN_MAILBOX_KEY);
190        final long accountId = cursor.getLong(COLUMN_ACCOUNT_KEY);
191        itemView.mAccountId = accountId;
192        itemView.mRead = cursor.getInt(COLUMN_READ) != 0;
193        itemView.mIsFavorite = cursor.getInt(COLUMN_FAVORITE) != 0;
194        final int flags = cursor.getInt(COLUMN_FLAGS);
195        itemView.mHasInvite = (flags & Message.FLAG_INCOMING_MEETING_INVITE) != 0;
196        itemView.mHasBeenRepliedTo = (flags & Message.FLAG_REPLIED_TO) != 0;
197        itemView.mHasBeenForwarded = (flags & Message.FLAG_FORWARDED) != 0;
198        itemView.mHasAttachment = cursor.getInt(COLUMN_ATTACHMENTS) != 0;
199        itemView.mTimestamp = cursor.getLong(COLUMN_DATE);
200        itemView.mSender = cursor.getString(COLUMN_DISPLAY_NAME);
201        itemView.mSnippet = cursor.getString(COLUMN_SNIPPET);
202        itemView.setSubject(cursor.getString(COLUMN_SUBJECT));
203        itemView.mSnippetLineCount = MessageListItem.NEEDS_LAYOUT;
204        itemView.mColorChipPaint =
205            mShowColorChips ? mResourceHelper.getAccountColorPaint(accountId) : null;
206
207        if (mQuery != null && itemView.mSnippet != null) {
208            itemView.mSnippet =
209                TextUtilities.highlightTermsInText(cursor.getString(COLUMN_SNIPPET), mQuery);
210        }
211    }
212
213    @Override
214    public View newView(Context context, Cursor cursor, ViewGroup parent) {
215        MessageListItem item = new MessageListItem(context);
216        item.setVisibility(View.VISIBLE);
217        return item;
218    }
219
220    public void toggleSelected(MessageListItem itemView) {
221        updateSelected(itemView, !isSelected(itemView));
222    }
223
224    /**
225     * This is used as a callback from the list items, to set the selected state
226     *
227     * <p>Must be called on the UI thread.
228     *
229     * @param itemView the item being changed
230     * @param newSelected the new value of the selected flag (checkbox state)
231     */
232    private void updateSelected(MessageListItem itemView, boolean newSelected) {
233        if (newSelected) {
234            mSelectedSet.add(itemView.mMessageId);
235        } else {
236            mSelectedSet.remove(itemView.mMessageId);
237        }
238        if (mCallback != null) {
239            mCallback.onAdapterSelectedChanged(itemView, newSelected, mSelectedSet.size());
240        }
241    }
242
243    /**
244     * This is used as a callback from the list items, to set the favorite state
245     *
246     * <p>Must be called on the UI thread.
247     *
248     * @param itemView the item being changed
249     * @param newFavorite the new value of the favorite flag (star state)
250     */
251    public void updateFavorite(MessageListItem itemView, boolean newFavorite) {
252        changeFavoriteIcon(itemView, newFavorite);
253        if (mCallback != null) {
254            mCallback.onAdapterFavoriteChanged(itemView, newFavorite);
255        }
256    }
257
258    private void changeFavoriteIcon(MessageListItem view, boolean isFavorite) {
259        view.invalidate();
260    }
261
262    /**
263     * Creates the loader for {@link MessageListFragment}.
264     *
265     * @return always of {@link MessagesCursor}.
266     */
267    public static Loader<Cursor> createLoader(Context context, MessageListContext listContext) {
268        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
269            Log.d(Logging.LOG_TAG, "MessagesAdapter createLoader listContext=" + listContext);
270        }
271        return listContext.isSearch()
272                ? new SearchCursorLoader(context, listContext)
273                : new MessagesCursorLoader(context, listContext.getMailboxId());
274    }
275
276    private static class MessagesCursorLoader extends ThrottlingCursorLoader {
277        protected final Context mContext;
278        private final long mMailboxId;
279
280        public MessagesCursorLoader(Context context, long mailboxId) {
281            // Initialize with no where clause.  We'll set it later.
282            super(context, EmailContent.Message.CONTENT_URI,
283                    MESSAGE_PROJECTION, null, null,
284                    EmailContent.MessageColumns.TIMESTAMP + " DESC");
285            mContext = context;
286            mMailboxId = mailboxId;
287        }
288
289        @Override
290        public Cursor loadInBackground() {
291            // Build the where cause (which can't be done on the UI thread.)
292            setSelection(Message.buildMessageListSelection(mContext, mMailboxId));
293            // Then do a query to get the cursor
294            return loadExtras(super.loadInBackground());
295        }
296
297        private Cursor loadExtras(Cursor baseCursor) {
298            boolean found = false;
299            Account account = null;
300            Mailbox mailbox = null;
301            boolean isEasAccount = false;
302            boolean isRefreshable = false;
303
304            if (mMailboxId < 0) {
305                // Magic mailbox.
306                found = true;
307            } else {
308                mailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId);
309                if (mailbox != null) {
310                    account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey);
311                    if (account != null) {
312                        found = true;
313                        isEasAccount = account.isEasAccount(mContext) ;
314                        isRefreshable = Mailbox.isRefreshable(mContext, mMailboxId);
315                    } else { // Account removed?
316                        mailbox = null;
317                    }
318                }
319            }
320            final int countAccounts = EmailContent.count(mContext, Account.CONTENT_URI);
321            return wrapCursor(baseCursor, found, account, mailbox, isEasAccount,
322                    isRefreshable, countAccounts);
323        }
324
325        /**
326         * Wraps a basic cursor containing raw messages with information about the context of
327         * the list that's being loaded, such as the account and the mailbox the messages
328         * are for.
329         * Subclasses may extend this to wrap with additional data.
330         */
331        protected Cursor wrapCursor(Cursor cursor,
332                boolean found, Account account, Mailbox mailbox, boolean isEasAccount,
333                boolean isRefreshable, int countTotalAccounts) {
334            return new MessagesCursor(cursor, found, account, mailbox, isEasAccount,
335                    isRefreshable, countTotalAccounts);
336        }
337    }
338
339    public static class SearchResultsCursor extends MessagesCursor {
340        private final Mailbox mSearchedMailbox;
341        private final int mResultsCount;
342        private SearchResultsCursor(Cursor cursor,
343                boolean found, Account account, Mailbox mailbox, boolean isEasAccount,
344                boolean isRefreshable, int countTotalAccounts,
345                Mailbox searchedMailbox, int resultsCount) {
346            super(cursor, found, account, mailbox, isEasAccount,
347                    isRefreshable, countTotalAccounts);
348            mSearchedMailbox = searchedMailbox;
349            mResultsCount = resultsCount;
350        }
351
352        /**
353         * @return the total number of results that match the given search query. Note that
354         *     there may not be that many items loaded in the cursor yet.
355         */
356        public int getResultsCount() {
357            return mResultsCount;
358        }
359
360        public Mailbox getSearchedMailbox() {
361            return mSearchedMailbox;
362        }
363
364        public boolean hasResults() {
365            return getResultsCount() == 0;
366        }
367    }
368
369    /**
370     * A special loader used to perform a search.
371     */
372    private static class SearchCursorLoader extends MessagesCursorLoader {
373        private final MessageListContext mListContext;
374        private int mResultsCount = -1;
375        private Mailbox mSearchedMailbox = null;
376
377        public SearchCursorLoader(Context context, MessageListContext listContext) {
378            super(context, listContext.getMailboxId());
379            Preconditions.checkArgument(listContext.isSearch());
380            mListContext = listContext;
381        }
382
383        @Override
384        public Cursor loadInBackground() {
385            if (mResultsCount >= 0) {
386                // Result count known - the initial search meta data must have completed.
387                return super.loadInBackground();
388            }
389
390            if (mSearchedMailbox == null) {
391                mSearchedMailbox = Mailbox.restoreMailboxWithId(
392                        mContext, mListContext.getSearchedMailbox());
393            }
394
395            // The search results info hasn't even been loaded yet, so the Controller has not yet
396            // initialized the search mailbox properly. Kick off the search first.
397            Controller controller = Controller.getInstance(mContext);
398            try {
399                mResultsCount = controller.searchMessages(
400                        mListContext.mAccountId, mListContext.getSearchParams());
401            } catch (MessagingException e) {
402            }
403
404            // Return whatever the super would do, now that we know the results are ready.
405            // After this point, it should behave as a normal mailbox load for messages.
406            return super.loadInBackground();
407        }
408
409        @Override
410        protected Cursor wrapCursor(Cursor cursor,
411                boolean found, Account account, Mailbox mailbox, boolean isEasAccount,
412                boolean isRefreshable, int countTotalAccounts) {
413            return new SearchResultsCursor(cursor, found, account, mailbox, isEasAccount,
414                    isRefreshable, countTotalAccounts, mSearchedMailbox, mResultsCount);
415        }
416    }
417}
418