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