MessagesAdapter.java revision 6b256f1fa10b858ae2bea827b65c8608592e4ba3
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.database.MatrixCursor;
24import android.os.Bundle;
25import android.util.Log;
26import android.view.View;
27import android.view.ViewGroup;
28import android.widget.CursorAdapter;
29
30import com.android.email.Controller;
31import com.android.email.Email;
32import com.android.email.MessageListContext;
33import com.android.email.ResourceHelper;
34import com.android.email.data.ThrottlingCursorLoader;
35import com.android.emailcommon.Logging;
36import com.android.emailcommon.mail.MessagingException;
37import com.android.emailcommon.provider.Account;
38import com.android.emailcommon.provider.EmailContent;
39import com.android.emailcommon.provider.EmailContent.Message;
40import com.android.emailcommon.provider.EmailContent.MessageColumns;
41import com.android.emailcommon.provider.Mailbox;
42import com.android.emailcommon.utility.TextUtilities;
43import com.android.emailcommon.utility.Utility;
44import com.google.common.base.Preconditions;
45
46import java.util.HashSet;
47import java.util.Set;
48
49
50/**
51 * This class implements the adapter for displaying messages based on cursors.
52 */
53/* package */ class MessagesAdapter extends CursorAdapter {
54    private static final String STATE_CHECKED_ITEMS =
55            "com.android.email.activity.MessagesAdapter.checkedItems";
56
57    /* package */ static final String[] MESSAGE_PROJECTION = new String[] {
58        EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
59        MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP,
60        MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT,
61        MessageColumns.FLAGS, MessageColumns.SNIPPET
62    };
63
64    public static final int COLUMN_ID = 0;
65    public static final int COLUMN_MAILBOX_KEY = 1;
66    public static final int COLUMN_ACCOUNT_KEY = 2;
67    public static final int COLUMN_DISPLAY_NAME = 3;
68    public static final int COLUMN_SUBJECT = 4;
69    public static final int COLUMN_DATE = 5;
70    public static final int COLUMN_READ = 6;
71    public static final int COLUMN_FAVORITE = 7;
72    public static final int COLUMN_ATTACHMENTS = 8;
73    public static final int COLUMN_FLAGS = 9;
74    public static final int COLUMN_SNIPPET = 10;
75
76    private final ResourceHelper mResourceHelper;
77
78    /** If true, show color chips. */
79    private boolean mShowColorChips;
80
81    /** If not null, the query represented by this group of messages */
82    private String mQuery;
83
84    /**
85     * Set of seleced message IDs.
86     */
87    private final HashSet<Long> mSelectedSet = new HashSet<Long>();
88
89    /**
90     * Callback from MessageListAdapter.  All methods are called on the UI thread.
91     */
92    public interface Callback {
93        /** Called when the use starts/unstars a message */
94        void onAdapterFavoriteChanged(MessageListItem itemView, boolean newFavorite);
95        /** Called when the user selects/unselects a message */
96        void onAdapterSelectedChanged(MessageListItem itemView, boolean newSelected,
97                int mSelectedCount);
98    }
99
100    private final Callback mCallback;
101
102    /**
103     * The actual return type from the loader.
104     */
105    public static class MessagesCursor extends CursorWrapper {
106        /**  Whether the mailbox is found. */
107        public final boolean mIsFound;
108        /** {@link Account} that owns the mailbox.  Null for combined mailboxes. */
109        public final Account mAccount;
110        /** {@link Mailbox} for the loaded mailbox. Null for combined mailboxes. */
111        public final Mailbox mMailbox;
112        /** {@code true} if the account is an EAS account */
113        public final boolean mIsEasAccount;
114        /** {@code true} if the loaded mailbox can be refreshed. */
115        public final boolean mIsRefreshable;
116        /** the number of accounts currently configured. */
117        public final int mCountTotalAccounts;
118
119        private MessagesCursor(Cursor cursor,
120                boolean found, Account account, Mailbox mailbox, boolean isEasAccount,
121                boolean isRefreshable, int countTotalAccounts) {
122            super(cursor);
123            mIsFound = found;
124            mAccount = account;
125            mMailbox = mailbox;
126            mIsEasAccount = isEasAccount;
127            mIsRefreshable = isRefreshable;
128            mCountTotalAccounts = countTotalAccounts;
129        }
130    }
131
132    public MessagesAdapter(Context context, Callback callback) {
133        super(context.getApplicationContext(), null, 0 /* no auto requery */);
134        mResourceHelper = ResourceHelper.getInstance(context);
135        mCallback = callback;
136    }
137
138    public void onSaveInstanceState(Bundle outState) {
139        outState.putLongArray(STATE_CHECKED_ITEMS, Utility.toPrimitiveLongArray(getSelectedSet()));
140    }
141
142    public void loadState(Bundle savedInstanceState) {
143        Set<Long> checkedset = getSelectedSet();
144        checkedset.clear();
145        for (long l: savedInstanceState.getLongArray(STATE_CHECKED_ITEMS)) {
146            checkedset.add(l);
147        }
148        notifyDataSetChanged();
149    }
150
151    /**
152     * Set true for combined mailboxes.
153     */
154    public void setShowColorChips(boolean show) {
155        mShowColorChips = show;
156    }
157
158    public void setQuery(String query) {
159        mQuery = query;
160    }
161
162    public Set<Long> getSelectedSet() {
163        return mSelectedSet;
164    }
165
166    /**
167     * Clear the selection.  It's preferable to calling {@link Set#clear()} on
168     * {@link #getSelectedSet()}, because it also notifies observers.
169     */
170    public void clearSelection() {
171        Set<Long> checkedset = getSelectedSet();
172        if (checkedset.size() > 0) {
173            checkedset.clear();
174            notifyDataSetChanged();
175        }
176    }
177
178    public boolean isSelected(MessageListItem itemView) {
179        return getSelectedSet().contains(itemView.mMessageId);
180    }
181
182    @Override
183    public void bindView(View view, Context context, Cursor cursor) {
184        // Reset the view (in case it was recycled) and prepare for binding
185        MessageListItem itemView = (MessageListItem) view;
186        itemView.bindViewInit(this);
187
188        // Load the public fields in the view (for later use)
189        itemView.mMessageId = cursor.getLong(COLUMN_ID);
190        itemView.mMailboxId = cursor.getLong(COLUMN_MAILBOX_KEY);
191        final long accountId = cursor.getLong(COLUMN_ACCOUNT_KEY);
192        itemView.mAccountId = accountId;
193        itemView.mRead = cursor.getInt(COLUMN_READ) != 0;
194        itemView.mIsFavorite = cursor.getInt(COLUMN_FAVORITE) != 0;
195        itemView.mHasInvite =
196            (cursor.getInt(COLUMN_FLAGS) & Message.FLAG_INCOMING_MEETING_INVITE) != 0;
197        itemView.mHasAttachment = cursor.getInt(COLUMN_ATTACHMENTS) != 0;
198        itemView.mTimestamp = cursor.getLong(COLUMN_DATE);
199        itemView.mSender = cursor.getString(COLUMN_DISPLAY_NAME);
200        itemView.mSnippet = cursor.getString(COLUMN_SNIPPET);
201        itemView.mSubject = cursor.getString(COLUMN_SUBJECT);
202        itemView.mSnippetLineCount = MessageListItem.NEEDS_LAYOUT;
203        itemView.mColorChipPaint =
204            mShowColorChips ? mResourceHelper.getAccountColorPaint(accountId) : null;
205
206        if (mQuery != null && itemView.mSnippet != null) {
207            itemView.mSnippet =
208                TextUtilities.highlightTermsInText(cursor.getString(COLUMN_SNIPPET), mQuery);
209        }
210    }
211
212    @Override
213    public View newView(Context context, Cursor cursor, ViewGroup parent) {
214        MessageListItem item = new MessageListItem(context);
215        item.setVisibility(View.VISIBLE);
216        return item;
217    }
218
219    public void toggleSelected(MessageListItem itemView) {
220        updateSelected(itemView, !isSelected(itemView));
221    }
222
223    /**
224     * This is used as a callback from the list items, to set the selected state
225     *
226     * <p>Must be called on the UI thread.
227     *
228     * @param itemView the item being changed
229     * @param newSelected the new value of the selected flag (checkbox state)
230     */
231    private void updateSelected(MessageListItem itemView, boolean newSelected) {
232        if (newSelected) {
233            mSelectedSet.add(itemView.mMessageId);
234        } else {
235            mSelectedSet.remove(itemView.mMessageId);
236        }
237        if (mCallback != null) {
238            mCallback.onAdapterSelectedChanged(itemView, newSelected, mSelectedSet.size());
239        }
240    }
241
242    /**
243     * This is used as a callback from the list items, to set the favorite state
244     *
245     * <p>Must be called on the UI thread.
246     *
247     * @param itemView the item being changed
248     * @param newFavorite the new value of the favorite flag (star state)
249     */
250    public void updateFavorite(MessageListItem itemView, boolean newFavorite) {
251        changeFavoriteIcon(itemView, newFavorite);
252        if (mCallback != null) {
253            mCallback.onAdapterFavoriteChanged(itemView, newFavorite);
254        }
255    }
256
257    private void changeFavoriteIcon(MessageListItem view, boolean isFavorite) {
258        view.invalidate();
259    }
260
261    /**
262     * Creates the loader for {@link MessageListFragment}.
263     *
264     * @return always of {@link MessagesCursor}.
265     */
266    public static Loader<Cursor> createLoader(Context context, MessageListContext listContext) {
267        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
268            Log.d(Logging.LOG_TAG, "MessagesAdapter createLoader listContext=" + listContext);
269        }
270        return listContext.isSearch()
271                ? new SearchCursorLoader(context, listContext)
272                : new MessagesCursorLoader(context, listContext.getMailboxId());
273    }
274
275    private static class MessagesCursorLoader extends ThrottlingCursorLoader {
276        protected final Context mContext;
277        private final long mMailboxId;
278
279        public MessagesCursorLoader(Context context, long mailboxId) {
280            // Initialize with no where clause.  We'll set it later.
281            super(context, EmailContent.Message.CONTENT_URI,
282                    MESSAGE_PROJECTION, null, null,
283                    EmailContent.MessageColumns.TIMESTAMP + " DESC");
284            mContext = context;
285            mMailboxId = mailboxId;
286        }
287
288        @Override
289        public Cursor loadInBackground() {
290            final Cursor returnCursor;
291
292            // Only perform a load if the selected mailbox can hold messages
293            // box can be null on the combined view where we use negative mailbox ids.
294            final boolean canHaveMessages;
295            if (mMailboxId < 0) {
296                // Combined mailboxes can always have messages.
297                canHaveMessages = true;
298            } else {
299                Mailbox box = Mailbox.restoreMailboxWithId(mContext, mMailboxId);
300                canHaveMessages = (box != null) && (box.mFlags & Mailbox.FLAG_HOLDS_MAIL) != 0;
301            }
302            if (canHaveMessages) {
303                // Build the where cause (which can't be done on the UI thread.)
304                setSelection(Message.buildMessageListSelection(mContext, mMailboxId));
305                // Then do a query to get the cursor
306                returnCursor = super.loadInBackground();
307            } else {
308                // return an empty cursor
309                returnCursor = new MatrixCursor(getProjection());
310            }
311            return loadExtras(returnCursor);
312        }
313
314        private Cursor loadExtras(Cursor baseCursor) {
315            boolean found = false;
316            Account account = null;
317            Mailbox mailbox = null;
318            boolean isEasAccount = false;
319            boolean isRefreshable = false;
320
321            if (mMailboxId < 0) {
322                // Magic mailbox.
323                found = true;
324            } else {
325                mailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId);
326                if (mailbox != null) {
327                    account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey);
328                    if (account != null) {
329                        found = true;
330                        isEasAccount = account.isEasAccount(mContext) ;
331                        isRefreshable = Mailbox.isRefreshable(mContext, mMailboxId);
332                    } else { // Account removed?
333                        mailbox = null;
334                    }
335                }
336            }
337            final int countAccounts = EmailContent.count(mContext, Account.CONTENT_URI);
338            return wrapCursor(baseCursor, found, account, mailbox, isEasAccount,
339                    isRefreshable, countAccounts);
340        }
341
342        /**
343         * Wraps a basic cursor containing raw messages with information about the context of
344         * the list that's being loaded, such as the account and the mailbox the messages
345         * are for.
346         * Subclasses may extend this to wrap with additional data.
347         */
348        protected Cursor wrapCursor(Cursor cursor,
349                boolean found, Account account, Mailbox mailbox, boolean isEasAccount,
350                boolean isRefreshable, int countTotalAccounts) {
351            return new MessagesCursor(cursor, found, account, mailbox, isEasAccount,
352                    isRefreshable, countTotalAccounts);
353        }
354    }
355
356    public static class SearchResultsCursor extends MessagesCursor {
357        private final int mResultsCount;
358        private SearchResultsCursor(Cursor cursor,
359                boolean found, Account account, Mailbox mailbox, boolean isEasAccount,
360                boolean isRefreshable, int countTotalAccounts, int resultsCount) {
361            super(cursor, found, account, mailbox, isEasAccount,
362                    isRefreshable, countTotalAccounts);
363            mResultsCount = resultsCount;
364        }
365
366        public int getResultsCount() {
367            return mResultsCount;
368        }
369
370        public boolean hasResults() {
371            return getResultsCount() == 0;
372        }
373    }
374
375    /**
376     * A special loader used to perform a search.
377     */
378    private static class SearchCursorLoader extends MessagesCursorLoader {
379        private final MessageListContext mListContext;
380        private int mResultsCount = -1;
381
382        public SearchCursorLoader(Context context, MessageListContext listContext) {
383            super(context, listContext.getMailboxId());
384            Preconditions.checkArgument(listContext.isSearch());
385            mListContext = listContext;
386        }
387
388        @Override
389        public Cursor loadInBackground() {
390            if (mResultsCount >= 0) {
391                // Result count known - the initial search meta data must have completed.
392                return super.loadInBackground();
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, mResultsCount);
415        }
416    }
417}
418