MessageList.java revision d2174733b146eeccd5f3b3b95f98e1e1aaafb257
1/*
2 * Copyright (C) 2009 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.Controller;
20import com.android.email.Email;
21import com.android.email.R;
22import com.android.email.Utility;
23import com.android.email.activity.setup.AccountSettings;
24import com.android.email.mail.MessagingException;
25import com.android.email.provider.EmailContent;
26import com.android.email.provider.EmailContent.Account;
27import com.android.email.provider.EmailContent.AccountColumns;
28import com.android.email.provider.EmailContent.Mailbox;
29import com.android.email.provider.EmailContent.MailboxColumns;
30import com.android.email.provider.EmailContent.MessageColumns;
31import com.android.email.service.MailService;
32
33import android.app.ListActivity;
34import android.app.NotificationManager;
35import android.content.ContentResolver;
36import android.content.ContentUris;
37import android.content.Context;
38import android.content.Intent;
39import android.content.res.Resources;
40import android.database.Cursor;
41import android.graphics.Typeface;
42import android.graphics.drawable.Drawable;
43import android.net.Uri;
44import android.os.AsyncTask;
45import android.os.Bundle;
46import android.os.Handler;
47import android.util.Log;
48import android.view.ContextMenu;
49import android.view.LayoutInflater;
50import android.view.Menu;
51import android.view.MenuItem;
52import android.view.View;
53import android.view.ViewGroup;
54import android.view.Window;
55import android.view.ContextMenu.ContextMenuInfo;
56import android.view.View.OnClickListener;
57import android.view.animation.AnimationUtils;
58import android.widget.AdapterView;
59import android.widget.Button;
60import android.widget.CursorAdapter;
61import android.widget.ImageView;
62import android.widget.ListView;
63import android.widget.ProgressBar;
64import android.widget.TextView;
65import android.widget.Toast;
66import android.widget.AdapterView.OnItemClickListener;
67
68import java.util.Date;
69import java.util.HashSet;
70import java.util.Set;
71
72public class MessageList extends ListActivity implements OnItemClickListener, OnClickListener {
73    // Intent extras (internal to this activity)
74    private static final String EXTRA_ACCOUNT_ID = "com.android.email.activity._ACCOUNT_ID";
75    private static final String EXTRA_MAILBOX_TYPE = "com.android.email.activity.MAILBOX_TYPE";
76    private static final String EXTRA_MAILBOX_ID = "com.android.email.activity.MAILBOX_ID";
77
78    // UI support
79    private ListView mListView;
80    private View mMultiSelectPanel;
81    private Button mReadUnreadButton;
82    private Button mFavoriteButton;
83    private Button mDeleteButton;
84    private View mListFooterView;
85    private TextView mListFooterText;
86    private View mListFooterProgress;
87
88    private static final int LIST_FOOTER_MODE_NONE = 0;
89    private static final int LIST_FOOTER_MODE_REFRESH = 1;
90    private static final int LIST_FOOTER_MODE_MORE = 2;
91    private static final int LIST_FOOTER_MODE_SEND = 3;
92    private int mListFooterMode;
93
94    private MessageListAdapter mListAdapter;
95    private MessageListHandler mHandler = new MessageListHandler();
96    private Controller mController = Controller.getInstance(getApplication());
97    private ControllerResults mControllerCallback = new ControllerResults();
98    private TextView mLeftTitle;
99    private TextView mRightTitle;
100    private ProgressBar mProgressIcon;
101
102    private static final int[] mColorChipResIds = new int[] {
103        R.drawable.appointment_indicator_leftside_1,
104        R.drawable.appointment_indicator_leftside_2,
105        R.drawable.appointment_indicator_leftside_3,
106        R.drawable.appointment_indicator_leftside_4,
107        R.drawable.appointment_indicator_leftside_5,
108        R.drawable.appointment_indicator_leftside_6,
109        R.drawable.appointment_indicator_leftside_7,
110        R.drawable.appointment_indicator_leftside_8,
111        R.drawable.appointment_indicator_leftside_9,
112        R.drawable.appointment_indicator_leftside_10,
113        R.drawable.appointment_indicator_leftside_11,
114        R.drawable.appointment_indicator_leftside_12,
115        R.drawable.appointment_indicator_leftside_13,
116        R.drawable.appointment_indicator_leftside_14,
117        R.drawable.appointment_indicator_leftside_15,
118        R.drawable.appointment_indicator_leftside_16,
119        R.drawable.appointment_indicator_leftside_17,
120        R.drawable.appointment_indicator_leftside_18,
121        R.drawable.appointment_indicator_leftside_19,
122        R.drawable.appointment_indicator_leftside_20,
123        R.drawable.appointment_indicator_leftside_21,
124    };
125
126    // DB access
127    private ContentResolver mResolver;
128    private long mMailboxId;
129    private LoadMessagesTask mLoadMessagesTask;
130    private FindMailboxTask mFindMailboxTask;
131    private SetTitleTask mSetTitleTask;
132    private SetFooterTask mSetFooterTask;
133
134    public final static String[] MAILBOX_FIND_INBOX_PROJECTION = new String[] {
135        EmailContent.RECORD_ID, MailboxColumns.TYPE, MailboxColumns.FLAG_VISIBLE
136    };
137
138    private static final int MAILBOX_NAME_COLUMN_ID = 0;
139    private static final int MAILBOX_NAME_COLUMN_ACCOUNT_KEY = 1;
140    private static final int MAILBOX_NAME_COLUMN_TYPE = 2;
141    private static final String[] MAILBOX_NAME_PROJECTION = new String[] {
142            MailboxColumns.DISPLAY_NAME, MailboxColumns.ACCOUNT_KEY,
143            MailboxColumns.TYPE};
144
145    private static final int ACCOUNT_DISPLAY_NAME_COLUMN_ID = 0;
146    private static final String[] ACCOUNT_NAME_PROJECTION = new String[] {
147            AccountColumns.DISPLAY_NAME };
148
149    private static final String ID_SELECTION = EmailContent.RECORD_ID + "=?";
150
151    private Boolean mPushModeMailbox = null;
152
153    /**
154     * Open a specific mailbox.
155     *
156     * TODO This should just shortcut to a more generic version that can accept a list of
157     * accounts/mailboxes (e.g. merged inboxes).
158     *
159     * @param context
160     * @param id mailbox key
161     */
162    public static void actionHandleMailbox(Context context, long id) {
163        Intent intent = new Intent(context, MessageList.class);
164        intent.putExtra(EXTRA_MAILBOX_ID, id);
165        context.startActivity(intent);
166    }
167
168    /**
169     * Open a specific mailbox by account & type
170     *
171     * @param context The caller's context (for generating an intent)
172     * @param accountId The account to open
173     * @param mailboxType the type of mailbox to open (e.g. @see EmailContent.Mailbox.TYPE_INBOX)
174     */
175    public static void actionHandleAccount(Context context, long accountId, int mailboxType) {
176        Intent intent = new Intent(context, MessageList.class);
177        intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
178        intent.putExtra(EXTRA_MAILBOX_TYPE, mailboxType);
179        context.startActivity(intent);
180    }
181
182    /**
183     * Return an intent to open a specific mailbox by account & type.  It will also clear
184     * notifications.
185     *
186     * @param context The caller's context (for generating an intent)
187     * @param accountId The account to open, or -1
188     * @param mailboxId the ID of the mailbox to open, or -1
189     * @param mailboxType the type of mailbox to open (e.g. @see Mailbox.TYPE_INBOX) or -1
190     */
191    public static Intent actionHandleAccountIntent(Context context, long accountId,
192            long mailboxId, int mailboxType) {
193        Intent intent = new Intent(context, MessageList.class);
194        intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
195        intent.putExtra(EXTRA_MAILBOX_ID, mailboxId);
196        intent.putExtra(EXTRA_MAILBOX_TYPE, mailboxType);
197        return intent;
198    }
199
200    /**
201     * Used for generating lightweight (Uri-only) intents.
202     *
203     * @param context Calling context for building the intent
204     * @param accountId The account of interest
205     * @param mailboxType The folder name to open (typically Mailbox.TYPE_INBOX)
206     * @return an Intent which can be used to view that account
207     */
208    public static Intent actionHandleAccountUriIntent(Context context, long accountId,
209            int mailboxType) {
210        Intent i = actionHandleAccountIntent(context, accountId, -1, mailboxType);
211        i.removeExtra(EXTRA_ACCOUNT_ID);
212        Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
213        i.setData(uri);
214        return i;
215    }
216
217    @Override
218    public void onCreate(Bundle icicle) {
219        super.onCreate(icicle);
220
221        requestWindowFeature(Window.FEATURE_CUSTOM_TITLE);
222        setContentView(R.layout.message_list);
223        getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE,
224                R.layout.list_title);
225
226        mListView = getListView();
227        mMultiSelectPanel = findViewById(R.id.footer_organize);
228        mReadUnreadButton = (Button) findViewById(R.id.btn_read_unread);
229        mFavoriteButton = (Button) findViewById(R.id.btn_multi_favorite);
230        mDeleteButton = (Button) findViewById(R.id.btn_multi_delete);
231
232        mLeftTitle = (TextView) findViewById(R.id.title_left_text);
233        mRightTitle = (TextView) findViewById(R.id.title_right_text);
234        mProgressIcon = (ProgressBar) findViewById(R.id.title_progress_icon);
235
236        mReadUnreadButton.setOnClickListener(this);
237        mFavoriteButton.setOnClickListener(this);
238        mDeleteButton.setOnClickListener(this);
239
240        mListView.setOnItemClickListener(this);
241        mListView.setItemsCanFocus(false);
242        registerForContextMenu(mListView);
243
244        mListAdapter = new MessageListAdapter(this);
245        setListAdapter(mListAdapter);
246
247        mResolver = getContentResolver();
248
249        // TODO extend this to properly deal with multiple mailboxes, cursor, etc.
250
251        // Select 'by id' or 'by type' or 'by uri' mode and launch appropriate queries
252
253        mMailboxId = getIntent().getLongExtra(EXTRA_MAILBOX_ID, -1);
254        if (mMailboxId != -1) {
255            // Specific mailbox ID was provided - go directly to it
256            mSetTitleTask = new SetTitleTask(mMailboxId);
257            mSetTitleTask.execute();
258            mLoadMessagesTask = new LoadMessagesTask(mMailboxId, -1);
259            mLoadMessagesTask.execute();
260            addFooterView(mMailboxId, -1, -1);
261        } else {
262            long accountId = -1;
263            int mailboxType = getIntent().getIntExtra(EXTRA_MAILBOX_TYPE, Mailbox.TYPE_INBOX);
264            Uri uri = getIntent().getData();
265            if (uri != null
266                    && "content".equals(uri.getScheme())
267                    && EmailContent.AUTHORITY.equals(uri.getAuthority())) {
268                // A content URI was provided - try to look up the account
269                String accountIdString = uri.getPathSegments().get(1);
270                if (accountIdString != null) {
271                    accountId = Long.parseLong(accountIdString);
272                }
273                mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false);
274                mFindMailboxTask.execute();
275            } else {
276                // Go by account id + type
277                accountId = getIntent().getLongExtra(EXTRA_ACCOUNT_ID, -1);
278                mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, true);
279                mFindMailboxTask.execute();
280            }
281            addFooterView(-1, accountId, mailboxType);
282        }
283
284        // TODO set title to "account > mailbox (#unread)"
285    }
286
287    @Override
288    public void onPause() {
289        super.onPause();
290        mController.removeResultCallback(mControllerCallback);
291    }
292
293    @Override
294    public void onResume() {
295        super.onResume();
296        mController.addResultCallback(mControllerCallback);
297
298        // clear notifications here
299        NotificationManager notificationManager = (NotificationManager)
300                getSystemService(Context.NOTIFICATION_SERVICE);
301        notificationManager.cancel(MailService.NEW_MESSAGE_NOTIFICATION_ID);
302        autoRefreshStaleMailbox();
303    }
304
305    @Override
306    protected void onDestroy() {
307        super.onDestroy();
308
309        if (mLoadMessagesTask != null &&
310                mLoadMessagesTask.getStatus() != LoadMessagesTask.Status.FINISHED) {
311            mLoadMessagesTask.cancel(true);
312            mLoadMessagesTask = null;
313        }
314        if (mFindMailboxTask != null &&
315                mFindMailboxTask.getStatus() != FindMailboxTask.Status.FINISHED) {
316            mFindMailboxTask.cancel(true);
317            mFindMailboxTask = null;
318        }
319        if (mSetTitleTask != null &&
320                mSetTitleTask.getStatus() != SetTitleTask.Status.FINISHED) {
321            mSetTitleTask.cancel(true);
322            mSetTitleTask = null;
323        }
324        if (mSetFooterTask != null &&
325                mSetFooterTask.getStatus() != SetTitleTask.Status.FINISHED) {
326            mSetFooterTask.cancel(true);
327            mSetFooterTask = null;
328        }
329    }
330
331    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
332        if (view != mListFooterView) {
333            MessageListItem itemView = (MessageListItem) view;
334            onOpenMessage(id, itemView.mMailboxId);
335        } else {
336            doFooterClick();
337        }
338    }
339
340    public void onClick(View v) {
341        switch (v.getId()) {
342            case R.id.btn_read_unread:
343                onMultiToggleRead(mListAdapter.getSelectedSet());
344                break;
345            case R.id.btn_multi_favorite:
346                onMultiToggleFavorite(mListAdapter.getSelectedSet());
347                break;
348            case R.id.btn_multi_delete:
349                onMultiDelete(mListAdapter.getSelectedSet());
350                break;
351        }
352    }
353
354    @Override
355    public boolean onCreateOptionsMenu(Menu menu) {
356        super.onCreateOptionsMenu(menu);
357        if (mMailboxId < 0) {
358            getMenuInflater().inflate(R.menu.message_list_option_smart_folder, menu);
359        } else {
360            getMenuInflater().inflate(R.menu.message_list_option, menu);
361        }
362        return true;
363    }
364
365    @Override
366    public boolean onOptionsItemSelected(MenuItem item) {
367        switch (item.getItemId()) {
368            case R.id.refresh:
369                onRefresh();
370                return true;
371            case R.id.folders:
372                onFolders();
373                return true;
374            case R.id.accounts:
375                onAccounts();
376                return true;
377            case R.id.compose:
378                onCompose();
379                return true;
380            case R.id.account_settings:
381                onEditAccount();
382                return true;
383            default:
384                return super.onOptionsItemSelected(item);
385        }
386    }
387
388    @Override
389    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
390        super.onCreateContextMenu(menu, v, menuInfo);
391
392        AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
393        // There is no context menu for the list footer
394        if (info.targetView == mListFooterView) {
395            return;
396        }
397        MessageListItem itemView = (MessageListItem) info.targetView;
398
399        Cursor c = (Cursor) mListView.getItemAtPosition(info.position);
400        String messageName = c.getString(MessageListAdapter.COLUMN_SUBJECT);
401
402        menu.setHeaderTitle(messageName);
403
404        // TODO: There is probably a special context menu for the trash
405        Mailbox mailbox = Mailbox.restoreMailboxWithId(this, itemView.mMailboxId);
406
407        switch (mailbox.mType) {
408            case EmailContent.Mailbox.TYPE_DRAFTS:
409                getMenuInflater().inflate(R.menu.message_list_context_drafts, menu);
410                break;
411            case EmailContent.Mailbox.TYPE_OUTBOX:
412                getMenuInflater().inflate(R.menu.message_list_context_outbox, menu);
413                break;
414            case EmailContent.Mailbox.TYPE_TRASH:
415                getMenuInflater().inflate(R.menu.message_list_context_trash, menu);
416                break;
417            default:
418                getMenuInflater().inflate(R.menu.message_list_context, menu);
419                // The default menu contains "mark as read".  If the message is read, change
420                // the menu text to "mark as unread."
421                if (itemView.mRead) {
422                    menu.findItem(R.id.mark_as_read).setTitle(R.string.mark_as_unread_action);
423                }
424                break;
425        }
426    }
427
428    @Override
429    public boolean onContextItemSelected(MenuItem item) {
430        AdapterView.AdapterContextMenuInfo info =
431            (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
432        MessageListItem itemView = (MessageListItem) info.targetView;
433
434        switch (item.getItemId()) {
435            case R.id.open:
436                onOpenMessage(info.id, itemView.mMailboxId);
437                break;
438            case R.id.delete:
439                onDelete(info.id, itemView.mAccountId);
440                break;
441            case R.id.reply:
442                onReply(itemView.mMessageId);
443                break;
444            case R.id.reply_all:
445                onReplyAll(itemView.mMessageId);
446                break;
447            case R.id.forward:
448                onForward(itemView.mMessageId);
449                break;
450            case R.id.mark_as_read:
451                onSetMessageRead(info.id, !itemView.mRead);
452                break;
453        }
454        return super.onContextItemSelected(item);
455    }
456
457    private void onRefresh() {
458        // TODO: Should not be reading from DB in UI thread - need a cleaner way to get accountId
459        if (mMailboxId >= 0) {
460            Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mMailboxId);
461            mController.updateMailbox(mailbox.mAccountKey, mMailboxId, mControllerCallback);
462        }
463    }
464
465    private void onFolders() {
466        if (mMailboxId >= 0) {
467            // TODO smaller projection
468            Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mMailboxId);
469            MailboxList.actionHandleAccount(this, mailbox.mAccountKey);
470            finish();
471        }
472    }
473
474    private void onAccounts() {
475        AccountFolderList.actionShowAccounts(this);
476        finish();
477    }
478
479    private long lookupAccountIdFromMailboxId(long mailboxId) {
480        // TODO: Select correct account to send from when there are multiple mailboxes
481        // TODO: Should not be reading from DB in UI thread
482        if (mailboxId < 0) {
483            return -1; // no info, default account
484        }
485        EmailContent.Mailbox mailbox =
486            EmailContent.Mailbox.restoreMailboxWithId(this, mailboxId);
487        return mailbox.mAccountKey;
488    }
489
490    private void onCompose() {
491        MessageCompose.actionCompose(this, lookupAccountIdFromMailboxId(mMailboxId));
492    }
493
494    private void onEditAccount() {
495        AccountSettings.actionSettings(this, lookupAccountIdFromMailboxId(mMailboxId));
496    }
497
498    private void onOpenMessage(long messageId, long mailboxId) {
499        // TODO: Should not be reading from DB in UI thread
500        EmailContent.Mailbox mailbox = EmailContent.Mailbox.restoreMailboxWithId(this, mailboxId);
501
502        if (mailbox.mType == EmailContent.Mailbox.TYPE_DRAFTS) {
503            MessageCompose.actionEditDraft(this, messageId);
504        } else {
505            MessageView.actionView(this, messageId, mailboxId);
506        }
507    }
508
509    private void onReply(long messageId) {
510        MessageCompose.actionReply(this, messageId, false);
511    }
512
513    private void onReplyAll(long messageId) {
514        MessageCompose.actionReply(this, messageId, true);
515    }
516
517    private void onForward(long messageId) {
518        MessageCompose.actionForward(this, messageId);
519    }
520
521    private void onLoadMoreMessages() {
522        if (mMailboxId >= 0) {
523            mController.loadMoreMessages(mMailboxId, mControllerCallback);
524        }
525    }
526
527    private void onSendPendingMessages() {
528        long accountId = lookupAccountIdFromMailboxId(mMailboxId);
529        mController.sendPendingMessages(accountId, mControllerCallback);
530    }
531
532    private void onDelete(long messageId, long accountId) {
533        mController.deleteMessage(messageId, accountId);
534        Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show();
535    }
536
537    private void onSetMessageRead(long messageId, boolean newRead) {
538        mController.setMessageRead(messageId, newRead);
539    }
540
541    private void onSetMessageFavorite(long messageId, boolean newFavorite) {
542        mController.setMessageFavorite(messageId, newFavorite);
543    }
544
545    /**
546     * Toggles a set read/unread states.  Note, the default behavior is "mark unread", so the
547     * sense of the helper methods is "true=unread".
548     *
549     * @param selectedSet The current list of selected items
550     */
551    private void onMultiToggleRead(Set<Long> selectedSet) {
552        toggleMultiple(selectedSet, new MultiToggleHelper() {
553
554            public boolean getField(long messageId, Cursor c) {
555                return c.getInt(MessageListAdapter.COLUMN_READ) == 0;
556            }
557
558            public boolean setField(long messageId, Cursor c, boolean newValue) {
559                boolean oldValue = getField(messageId, c);
560                if (oldValue != newValue) {
561                    onSetMessageRead(messageId, !newValue);
562                    return true;
563                }
564                return false;
565            }
566        });
567    }
568
569    /**
570     * Toggles a set of favorites (stars)
571     *
572     * @param selectedSet The current list of selected items
573     */
574    private void onMultiToggleFavorite(Set<Long> selectedSet) {
575        toggleMultiple(selectedSet, new MultiToggleHelper() {
576
577            public boolean getField(long messageId, Cursor c) {
578                return c.getInt(MessageListAdapter.COLUMN_FAVORITE) != 0;
579            }
580
581            public boolean setField(long messageId, Cursor c, boolean newValue) {
582                boolean oldValue = getField(messageId, c);
583                if (oldValue != newValue) {
584                    onSetMessageFavorite(messageId, newValue);
585                    return true;
586                }
587                return false;
588            }
589        });
590    }
591
592    private void onMultiDelete(Set<Long> selectedSet) {
593        // Clone the set, because deleting is going to thrash things
594        HashSet<Long> cloneSet = new HashSet<Long>(selectedSet);
595        for (Long id : cloneSet) {
596            mController.deleteMessage(id, -1);
597        }
598        // TODO: count messages and show "n messages deleted"
599        Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show();
600        selectedSet.clear();
601        showMultiPanel(false);
602    }
603
604    private interface MultiToggleHelper {
605        /**
606         * Return true if the field of interest is "set".  If one or more are false, then our
607         * bulk action will be to "set".  If all are set, our bulk action will be to "clear".
608         * @param messageId the message id of the current message
609         * @param c the cursor, positioned to the item of interest
610         * @return true if the field at this row is "set"
611         */
612        public boolean getField(long messageId, Cursor c);
613
614        /**
615         * Set or clear the field of interest.  Return true if a change was made.
616         * @param messageId the message id of the current message
617         * @param c the cursor, positioned to the item of interest
618         * @param newValue the new value to be set at this row
619         * @return true if a change was actually made
620         */
621        public boolean setField(long messageId, Cursor c, boolean newValue);
622    }
623
624    /**
625     * Toggle multiple fields in a message, using the following logic:  If one or more fields
626     * are "clear", then "set" them.  If all fields are "set", then "clear" them all.
627     *
628     * @param selectedSet the set of messages that are selected
629     * @param helper functions to implement the specific getter & setter
630     * @return the number of messages that were updated
631     */
632    private int toggleMultiple(Set<Long> selectedSet, MultiToggleHelper helper) {
633        Cursor c = mListAdapter.getCursor();
634        boolean anyWereFound = false;
635        boolean allWereSet = true;
636
637        c.moveToPosition(-1);
638        while (c.moveToNext()) {
639            long id = c.getInt(MessageListAdapter.COLUMN_ID);
640            if (selectedSet.contains(Long.valueOf(id))) {
641                anyWereFound = true;
642                if (!helper.getField(id, c)) {
643                    allWereSet = false;
644                    break;
645                }
646            }
647        }
648
649        int numChanged = 0;
650
651        if (anyWereFound) {
652            boolean newValue = !allWereSet;
653            c.moveToPosition(-1);
654            while (c.moveToNext()) {
655                long id = c.getInt(MessageListAdapter.COLUMN_ID);
656                if (selectedSet.contains(Long.valueOf(id))) {
657                    if (helper.setField(id, c, newValue)) {
658                        ++numChanged;
659                    }
660                }
661            }
662        }
663
664        return numChanged;
665    }
666
667    /**
668     * Test selected messages for showing appropriate labels
669     * @param selectedSet
670     * @param column_id
671     * @param defaultflag
672     * @return true when the specified flagged message is selected
673     */
674    private boolean testMultiple(Set<Long> selectedSet, int column_id, boolean defaultflag) {
675        Cursor c = mListAdapter.getCursor();
676        c.moveToPosition(-1);
677        while (c.moveToNext()) {
678            long id = c.getInt(MessageListAdapter.COLUMN_ID);
679            if (selectedSet.contains(Long.valueOf(id))) {
680                if (c.getInt(column_id) == (defaultflag? 1 : 0)) {
681                    return true;
682                }
683            }
684        }
685        return false;
686    }
687
688    private void autoRefreshStaleMailbox() {
689        if ((mListAdapter.getCursor() == null) // Check if messages info is loaded
690                || (mPushModeMailbox != null && mPushModeMailbox) // Check the push mode
691                || (mMailboxId < 0) // Check if this mailbox is synthetic/combined
692                || !Email.mailboxRequiresRefresh(mMailboxId)) {
693            return;
694        }
695        onRefresh();
696    }
697
698    private void updateFooterButtonNames () {
699        // Show "unread_action" when one or more read messages are selected.
700        if (testMultiple(mListAdapter.getSelectedSet(), MessageListAdapter.COLUMN_READ, true)) {
701            mReadUnreadButton.setText(R.string.unread_action);
702        } else {
703            mReadUnreadButton.setText(R.string.read_action);
704        }
705        // Show "set_star_action" when one or more un-starred messages are selected.
706        if (testMultiple(mListAdapter.getSelectedSet(),
707                MessageListAdapter.COLUMN_FAVORITE, false)) {
708            mFavoriteButton.setText(R.string.set_star_action);
709        } else {
710            mFavoriteButton.setText(R.string.remove_star_action);
711        }
712    }
713
714    /**
715     * Show or hide the panel of multi-select options
716     */
717    private void showMultiPanel(boolean show) {
718        if (show && mMultiSelectPanel.getVisibility() != View.VISIBLE) {
719            mMultiSelectPanel.setVisibility(View.VISIBLE);
720            mMultiSelectPanel.startAnimation(
721                    AnimationUtils.loadAnimation(this, R.anim.footer_appear));
722        } else if (!show && mMultiSelectPanel.getVisibility() != View.GONE) {
723            mMultiSelectPanel.setVisibility(View.GONE);
724            mMultiSelectPanel.startAnimation(
725                        AnimationUtils.loadAnimation(this, R.anim.footer_disappear));
726        }
727        if (show) {
728            updateFooterButtonNames();
729        }
730    }
731
732    /**
733     * Add the fixed footer view if appropriate (not always - not all accounts & mailboxes).
734     *
735     * Here are some rules (finish this list):
736     *
737     * Any merged box (except send):  refresh
738     * Any push-mode account:  refresh
739     * Any non-push-mode account:  load more
740     * Any outbox (send again):
741     *
742     * @param mailboxId the ID of the mailbox
743     */
744    private void addFooterView(long mailboxId, long accountId, int mailboxType) {
745        // first, look for shortcuts that don't need us to spin up a DB access task
746        if (mailboxId == Mailbox.QUERY_ALL_INBOXES
747                || mailboxId == Mailbox.QUERY_ALL_UNREAD
748                || mailboxId == Mailbox.QUERY_ALL_FAVORITES
749                || mailboxId == Mailbox.QUERY_ALL_DRAFTS) {
750            finishFooterView(LIST_FOOTER_MODE_REFRESH);
751            return;
752        }
753        if (mailboxId == Mailbox.QUERY_ALL_OUTBOX || mailboxType == Mailbox.TYPE_OUTBOX) {
754            finishFooterView(LIST_FOOTER_MODE_SEND);
755            return;
756        }
757
758        // We don't know enough to select the footer command type (yet), so we'll
759        // launch an async task to do the remaining lookups and decide what to do
760        mSetFooterTask = new SetFooterTask();
761        mSetFooterTask.execute(mailboxId, accountId);
762    }
763
764    private final static String[] MAILBOX_ACCOUNT_AND_TYPE_PROJECTION =
765        new String[] { MailboxColumns.ACCOUNT_KEY, MailboxColumns.TYPE };
766
767    private class SetFooterTask extends AsyncTask<Long, Void, Integer> {
768        /**
769         * There are two operational modes here, requiring different lookup.
770         * mailboxIs != -1:  A specific mailbox - check its type, then look up its account
771         * accountId != -1:  A specific account - look up the account
772         */
773        @Override
774        protected Integer doInBackground(Long... params) {
775            long mailboxId = params[0];
776            long accountId = params[1];
777            int mailboxType = -1;
778            if (mailboxId != -1) {
779                try {
780                    Uri uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId);
781                    Cursor c = mResolver.query(uri, MAILBOX_ACCOUNT_AND_TYPE_PROJECTION,
782                            null, null, null);
783                    if (c.moveToFirst()) {
784                        try {
785                            accountId = c.getLong(0);
786                            mailboxType = c.getInt(1);
787                        } finally {
788                            c.close();
789                        }
790                    }
791                } catch (IllegalArgumentException iae) {
792                    // can't do any more here
793                    return LIST_FOOTER_MODE_NONE;
794                }
795            }
796            if (mailboxType == Mailbox.TYPE_OUTBOX) {
797                return LIST_FOOTER_MODE_SEND;
798            }
799            if (accountId != -1) {
800                // This is inefficient but the best fix is not here but in isMessagingController
801                Account account = Account.restoreAccountWithId(MessageList.this, accountId);
802                if (account != null) {
803                    mPushModeMailbox = account.mSyncInterval == Account.CHECK_INTERVAL_PUSH;
804                    if (MessageList.this.mController.isMessagingController(account)) {
805                        return LIST_FOOTER_MODE_MORE;       // IMAP or POP
806                    } else {
807                        return LIST_FOOTER_MODE_REFRESH;    // EAS
808                    }
809                }
810            }
811            return LIST_FOOTER_MODE_NONE;
812        }
813
814        @Override
815        protected void onPostExecute(Integer listFooterMode) {
816            finishFooterView(listFooterMode);
817        }
818    }
819
820    /**
821     * Add the fixed footer view as specified, and set up the test as well.
822     *
823     * @param listFooterMode the footer mode we've determined should be used for this list
824     */
825    private void finishFooterView(int listFooterMode) {
826        mListFooterMode = listFooterMode;
827        if (mListFooterMode != LIST_FOOTER_MODE_NONE) {
828            mListFooterView = ((LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE))
829                    .inflate(R.layout.message_list_item_footer, mListView, false);
830            mList.addFooterView(mListFooterView);
831            setListAdapter(mListAdapter);
832
833            mListFooterProgress = mListFooterView.findViewById(R.id.progress);
834            mListFooterText = (TextView) mListFooterView.findViewById(R.id.main_text);
835            setListFooterText(false);
836        }
837    }
838
839    /**
840     * Set the list footer text based on mode and "active" status
841     */
842    private void setListFooterText(boolean active) {
843        if (mListFooterMode != LIST_FOOTER_MODE_NONE) {
844            int footerTextId = 0;
845            switch (mListFooterMode) {
846                case LIST_FOOTER_MODE_REFRESH:
847                    footerTextId = active ? R.string.status_loading_more
848                                          : R.string.refresh_action;
849                    break;
850                case LIST_FOOTER_MODE_MORE:
851                    footerTextId = active ? R.string.status_loading_more
852                                          : R.string.message_list_load_more_messages_action;
853                    break;
854                case LIST_FOOTER_MODE_SEND:
855                    footerTextId = active ? R.string.status_sending_messages
856                                          : R.string.message_list_send_pending_messages_action;
857                    break;
858            }
859            mListFooterText.setText(footerTextId);
860        }
861    }
862
863    /**
864     * Handle a click in the list footer, which changes meaning depending on what we're looking at.
865     */
866    private void doFooterClick() {
867        switch (mListFooterMode) {
868            case LIST_FOOTER_MODE_NONE:         // should never happen
869                break;
870            case LIST_FOOTER_MODE_REFRESH:
871                onRefresh();
872                break;
873            case LIST_FOOTER_MODE_MORE:
874                onLoadMoreMessages();
875                break;
876            case LIST_FOOTER_MODE_SEND:
877                onSendPendingMessages();
878                break;
879        }
880    }
881
882    /**
883     * Async task for finding a single mailbox by type (possibly even going to the network).
884     *
885     * This is much too complex, as implemented.  It uses this AsyncTask to check for a mailbox,
886     * then (if not found) a Controller call to refresh mailboxes from the server, and a handler
887     * to relaunch this task (a 2nd time) to read the results of the network refresh.  The core
888     * problem is that we have two different non-UI-thread jobs (reading DB and reading network)
889     * and two different paradigms for dealing with them.  Some unification would be needed here
890     * to make this cleaner.
891     *
892     * TODO: If this problem spreads to other operations, find a cleaner way to handle it.
893     */
894    private class FindMailboxTask extends AsyncTask<Void, Void, Long> {
895
896        private long mAccountId;
897        private int mMailboxType;
898        private boolean mOkToRecurse;
899
900        /**
901         * Special constructor to cache some local info
902         */
903        public FindMailboxTask(long accountId, int mailboxType, boolean okToRecurse) {
904            mAccountId = accountId;
905            mMailboxType = mailboxType;
906            mOkToRecurse = okToRecurse;
907        }
908
909        @Override
910        protected Long doInBackground(Void... params) {
911            // See if we can find the requested mailbox in the DB.
912            long mailboxId = Mailbox.findMailboxOfType(MessageList.this, mAccountId, mMailboxType);
913            if (mailboxId == -1 && mOkToRecurse) {
914                // Not found - launch network lookup
915                mControllerCallback.mWaitForMailboxType = mMailboxType;
916                mController.updateMailboxList(mAccountId, mControllerCallback);
917            }
918            return mailboxId;
919        }
920
921        @Override
922        protected void onPostExecute(Long mailboxId) {
923            if (mailboxId != -1) {
924                mMailboxId = mailboxId;
925                mSetTitleTask = new SetTitleTask(mMailboxId);
926                mSetTitleTask.execute();
927                mLoadMessagesTask = new LoadMessagesTask(mMailboxId, mAccountId);
928                mLoadMessagesTask.execute();
929            }
930        }
931    }
932
933    /**
934     * Async task for loading a single folder out of the UI thread
935     *
936     * The code here (for merged boxes) is a placeholder/hack and should be replaced.  Some
937     * specific notes:
938     * TODO:  Move the double query into a specialized URI that returns all inbox messages
939     * and do the dirty work in raw SQL in the provider.
940     * TODO:  Generalize the query generation so we can reuse it in MessageView (for next/prev)
941     */
942    private class LoadMessagesTask extends AsyncTask<Void, Void, Cursor> {
943
944        private long mMailboxKey;
945        private long mAccountKey;
946
947        /**
948         * Special constructor to cache some local info
949         */
950        public LoadMessagesTask(long mailboxKey, long accountKey) {
951            mMailboxKey = mailboxKey;
952            mAccountKey = accountKey;
953        }
954
955        @Override
956        protected Cursor doInBackground(Void... params) {
957            String selection =
958                Utility.buildMailboxIdSelection(MessageList.this.mResolver, mMailboxKey);
959            Cursor c = MessageList.this.managedQuery(
960                    EmailContent.Message.CONTENT_URI,
961                    MessageList.this.mListAdapter.PROJECTION,
962                    selection, null,
963                    EmailContent.MessageColumns.TIMESTAMP + " DESC");
964            return c;
965        }
966
967        @Override
968        protected void onPostExecute(Cursor cursor) {
969            if (cursor.isClosed()) {
970                return;
971            }
972            MessageList.this.mListAdapter.changeCursor(cursor);
973            autoRefreshStaleMailbox();
974            // Reset the "new messages" count in the service, since we're seeing them now
975            if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) {
976                MailService.resetNewMessageCount(MessageList.this, -1);
977            } else if (mMailboxKey >= 0 && mAccountKey != -1) {
978                MailService.resetNewMessageCount(MessageList.this, mAccountKey);
979            }
980        }
981    }
982
983    private class SetTitleTask extends AsyncTask<Void, Void, String[]> {
984
985        private long mMailboxKey;
986
987        public SetTitleTask(long mailboxKey) {
988            mMailboxKey = mailboxKey;
989        }
990
991        @Override
992        protected String[] doInBackground(Void... params) {
993            // Check special Mailboxes
994            if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) {
995                return new String[] {null,
996                        getString(R.string.account_folder_list_summary_inbox)};
997            } else if (mMailboxKey == Mailbox.QUERY_ALL_FAVORITES) {
998                return new String[] {null,
999                        getString(R.string.account_folder_list_summary_favorite)};
1000            } else if (mMailboxKey == Mailbox.QUERY_ALL_DRAFTS) {
1001                return new String[] {null,
1002                        getString(R.string.account_folder_list_summary_drafts)};
1003            } else if (mMailboxKey == Mailbox.QUERY_ALL_OUTBOX) {
1004                return new String[] {null,
1005                        getString(R.string.account_folder_list_summary_outbox)};
1006            }
1007            String accountName = null;
1008            String mailboxName = null;
1009            String accountKey = null;
1010            Cursor c = MessageList.this.mResolver.query(Mailbox.CONTENT_URI,
1011                    MAILBOX_NAME_PROJECTION, ID_SELECTION,
1012                    new String[] { Long.toString(mMailboxKey) }, null);
1013            try {
1014                if (c.moveToFirst()) {
1015                    mailboxName = Utility.FolderProperties.getInstance(MessageList.this)
1016                            .getDisplayName(c.getInt(MAILBOX_NAME_COLUMN_TYPE));
1017                    if (mailboxName == null) {
1018                        mailboxName = c.getString(MAILBOX_NAME_COLUMN_ID);
1019                    }
1020                    accountKey = c.getString(MAILBOX_NAME_COLUMN_ACCOUNT_KEY);
1021                }
1022            } finally {
1023                c.close();
1024            }
1025            if (accountKey != null) {
1026                c = MessageList.this.mResolver.query(Account.CONTENT_URI,
1027                        ACCOUNT_NAME_PROJECTION, ID_SELECTION, new String[] { accountKey },
1028                        null);
1029                try {
1030                    if (c.moveToFirst()) {
1031                        accountName = c.getString(ACCOUNT_DISPLAY_NAME_COLUMN_ID);
1032                    }
1033                } finally {
1034                    c.close();
1035                }
1036            }
1037            return new String[] {accountName, mailboxName};
1038        }
1039
1040        @Override
1041        protected void onPostExecute(String[] names) {
1042            Log.d("MessageList", "ACCOUNT:" + names[0] + "MAILBOX" + names[1]);
1043            if (names[0] != null) {
1044                mRightTitle.setText(names[0]);
1045            }
1046            if (names[1] != null) {
1047                mLeftTitle.setText(names[1]);
1048            }
1049        }
1050    }
1051
1052    /**
1053     * Handler for UI-thread operations (when called from callbacks or any other threads)
1054     */
1055    class MessageListHandler extends Handler {
1056        private static final int MSG_PROGRESS = 1;
1057        private static final int MSG_LOOKUP_MAILBOX_TYPE = 2;
1058
1059        @Override
1060        public void handleMessage(android.os.Message msg) {
1061            switch (msg.what) {
1062                case MSG_PROGRESS:
1063                    boolean visible = (msg.arg1 != 0);
1064                    if (visible) {
1065                        mProgressIcon.setVisibility(View.VISIBLE);
1066                    } else {
1067                        mProgressIcon.setVisibility(View.GONE);
1068                    }
1069                    if (mListFooterProgress != null) {
1070                        mListFooterProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
1071                    }
1072                    setListFooterText(visible);
1073                    break;
1074                case MSG_LOOKUP_MAILBOX_TYPE:
1075                    // kill running async task, if any
1076                    if (mFindMailboxTask != null &&
1077                            mFindMailboxTask.getStatus() != FindMailboxTask.Status.FINISHED) {
1078                        mFindMailboxTask.cancel(true);
1079                        mFindMailboxTask = null;
1080                    }
1081                    // start new one.  do not recurse back to controller.
1082                    long accountId = ((Long)msg.obj).longValue();
1083                    int mailboxType = msg.arg1;
1084                    mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false);
1085                    mFindMailboxTask.execute();
1086                    break;
1087                default:
1088                    super.handleMessage(msg);
1089            }
1090        }
1091
1092        /**
1093         * Call from any thread to start/stop progress indicator(s)
1094         * @param progress true to start, false to stop
1095         */
1096        public void progress(boolean progress) {
1097            android.os.Message msg = android.os.Message.obtain();
1098            msg.what = MSG_PROGRESS;
1099            msg.arg1 = progress ? 1 : 0;
1100            sendMessage(msg);
1101        }
1102
1103        /**
1104         * Called from any thread to look for a mailbox of a specific type.  This is designed
1105         * to be called from the Controller's MailboxList callback;  It instructs the async task
1106         * not to recurse, in case the mailbox is not found after this.
1107         *
1108         * See FindMailboxTask for more notes on this handler.
1109         */
1110        public void lookupMailboxType(long accountId, int mailboxType) {
1111            android.os.Message msg = android.os.Message.obtain();
1112            msg.what = MSG_LOOKUP_MAILBOX_TYPE;
1113            msg.arg1 = mailboxType;
1114            msg.obj = Long.valueOf(accountId);
1115            sendMessage(msg);
1116        }
1117    }
1118
1119    /**
1120     * Callback for async Controller results.
1121     */
1122    private class ControllerResults implements Controller.Result {
1123
1124        // These are preset for use by updateMailboxListCallback
1125        int mWaitForMailboxType = -1;
1126
1127        // TODO report errors into UI
1128        // TODO check accountKey and only react to relevant notifications
1129        public void updateMailboxListCallback(MessagingException result, long accountKey,
1130                int progress) {
1131            updateProgress(result, progress);
1132            if (progress == 100) {
1133                mHandler.lookupMailboxType(accountKey, mWaitForMailboxType);
1134            }
1135        }
1136
1137        // TODO report errors into UI
1138        // TODO check accountKey and only react to relevant notifications
1139        public void updateMailboxCallback(MessagingException result, long accountKey,
1140                long mailboxKey, int progress, int numNewMessages) {
1141            if (result != null || progress == 100) {
1142                Email.updateMailboxRefreshTime(mMailboxId);
1143            }
1144            updateProgress(result, progress);
1145        }
1146
1147        public void loadMessageForViewCallback(MessagingException result, long messageId,
1148                int progress) {
1149        }
1150
1151        public void loadAttachmentCallback(MessagingException result, long messageId,
1152                long attachmentId, int progress) {
1153        }
1154
1155        public void serviceCheckMailCallback(MessagingException result, long accountId,
1156                long mailboxId, int progress, long tag) {
1157        }
1158
1159        // TODO report errors into UI
1160        public void sendMailCallback(MessagingException result, long accountId, long messageId,
1161                int progress) {
1162            if (mListFooterMode == LIST_FOOTER_MODE_SEND) {
1163                updateProgress(result, progress);
1164            }
1165        }
1166
1167        private void updateProgress(MessagingException result, int progress) {
1168            if (result != null || progress == 100) {
1169                mHandler.progress(false);
1170            } else if (progress == 0) {
1171                mHandler.progress(true);
1172            }
1173        }
1174    }
1175
1176    /**
1177     * This class implements the adapter for displaying messages based on cursors.
1178     */
1179    /* package */ class MessageListAdapter extends CursorAdapter {
1180
1181        public static final int COLUMN_ID = 0;
1182        public static final int COLUMN_MAILBOX_KEY = 1;
1183        public static final int COLUMN_ACCOUNT_KEY = 2;
1184        public static final int COLUMN_DISPLAY_NAME = 3;
1185        public static final int COLUMN_SUBJECT = 4;
1186        public static final int COLUMN_DATE = 5;
1187        public static final int COLUMN_READ = 6;
1188        public static final int COLUMN_FAVORITE = 7;
1189        public static final int COLUMN_ATTACHMENTS = 8;
1190
1191        public final String[] PROJECTION = new String[] {
1192            EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
1193            MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP,
1194            MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT,
1195        };
1196
1197        Context mContext;
1198        private LayoutInflater mInflater;
1199        private Drawable mAttachmentIcon;
1200        private Drawable mFavoriteIconOn;
1201        private Drawable mFavoriteIconOff;
1202        private Drawable mSelectedIconOn;
1203        private Drawable mSelectedIconOff;
1204
1205        private java.text.DateFormat mDateFormat;
1206        private java.text.DateFormat mDayFormat;
1207        private java.text.DateFormat mTimeFormat;
1208
1209        private HashSet<Long> mChecked = new HashSet<Long>();
1210
1211        public MessageListAdapter(Context context) {
1212            super(context, null);
1213            mContext = context;
1214            mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
1215
1216            Resources resources = context.getResources();
1217            mAttachmentIcon = resources.getDrawable(R.drawable.ic_mms_attachment_small);
1218            mFavoriteIconOn = resources.getDrawable(android.R.drawable.star_on);
1219            mFavoriteIconOff = resources.getDrawable(android.R.drawable.star_off);
1220            mSelectedIconOn = resources.getDrawable(R.drawable.btn_check_buttonless_on);
1221            mSelectedIconOff = resources.getDrawable(R.drawable.btn_check_buttonless_off);
1222
1223            mDateFormat = android.text.format.DateFormat.getDateFormat(context);    // short date
1224            mDayFormat = android.text.format.DateFormat.getDateFormat(context);     // TODO: day
1225            mTimeFormat = android.text.format.DateFormat.getTimeFormat(context);    // 12/24 time
1226        }
1227
1228        public Set<Long> getSelectedSet() {
1229            return mChecked;
1230        }
1231
1232        @Override
1233        public void bindView(View view, Context context, Cursor cursor) {
1234            // Reset the view (in case it was recycled) and prepare for binding
1235            MessageListItem itemView = (MessageListItem) view;
1236            itemView.bindViewInit(this, true);
1237
1238            // Load the public fields in the view (for later use)
1239            itemView.mMessageId = cursor.getLong(COLUMN_ID);
1240            itemView.mMailboxId = cursor.getLong(COLUMN_MAILBOX_KEY);
1241            itemView.mAccountId = cursor.getLong(COLUMN_ACCOUNT_KEY);
1242            itemView.mRead = cursor.getInt(COLUMN_READ) != 0;
1243            itemView.mFavorite = cursor.getInt(COLUMN_FAVORITE) != 0;
1244            itemView.mSelected = mChecked.contains(Long.valueOf(itemView.mMessageId));
1245
1246            // Load the UI
1247            View chipView = view.findViewById(R.id.chip);
1248            int chipResId = mColorChipResIds[(int)itemView.mAccountId % mColorChipResIds.length];
1249            chipView.setBackgroundResource(chipResId);
1250
1251            TextView fromView = (TextView) view.findViewById(R.id.from);
1252            String text = cursor.getString(COLUMN_DISPLAY_NAME);
1253            fromView.setText(text);
1254
1255            TextView subjectView = (TextView) view.findViewById(R.id.subject);
1256            text = cursor.getString(COLUMN_SUBJECT);
1257            subjectView.setText(text);
1258
1259            boolean hasAttachments = cursor.getInt(COLUMN_ATTACHMENTS) != 0;
1260            subjectView.setCompoundDrawablesWithIntrinsicBounds(null, null,
1261                    hasAttachments ? mAttachmentIcon : null, null);
1262
1263            // TODO ui spec suggests "time", "day", "date" - implement "day"
1264            TextView dateView = (TextView) view.findViewById(R.id.date);
1265            long timestamp = cursor.getLong(COLUMN_DATE);
1266            Date date = new Date(timestamp);
1267            if (Utility.isDateToday(date)) {
1268                text = mTimeFormat.format(date);
1269            } else {
1270                text = mDateFormat.format(date);
1271            }
1272            dateView.setText(text);
1273
1274            if (itemView.mRead) {
1275                subjectView.setTypeface(Typeface.DEFAULT);
1276                fromView.setTypeface(Typeface.DEFAULT);
1277                view.setBackgroundDrawable(context.getResources().getDrawable(
1278                        R.color.message_list_item_background_read));
1279            } else {
1280                subjectView.setTypeface(Typeface.DEFAULT_BOLD);
1281                fromView.setTypeface(Typeface.DEFAULT_BOLD);
1282                view.setBackgroundDrawable(context.getResources().getDrawable(
1283                        R.color.message_list_item_background_unread));
1284            }
1285
1286            ImageView selectedView = (ImageView) view.findViewById(R.id.selected);
1287            selectedView.setImageDrawable(itemView.mSelected ? mSelectedIconOn : mSelectedIconOff);
1288
1289            ImageView favoriteView = (ImageView) view.findViewById(R.id.favorite);
1290            favoriteView.setImageDrawable(itemView.mFavorite ? mFavoriteIconOn : mFavoriteIconOff);
1291        }
1292
1293        @Override
1294        public View newView(Context context, Cursor cursor, ViewGroup parent) {
1295            return mInflater.inflate(R.layout.message_list_item, parent, false);
1296        }
1297
1298        /**
1299         * This is used as a callback from the list items, to set the selected state
1300         *
1301         * @param itemView the item being changed
1302         * @param newSelected the new value of the selected flag (checkbox state)
1303         */
1304        public void updateSelected(MessageListItem itemView, boolean newSelected) {
1305            ImageView selectedView = (ImageView) itemView.findViewById(R.id.selected);
1306            selectedView.setImageDrawable(newSelected ? mSelectedIconOn : mSelectedIconOff);
1307
1308            // Set checkbox state in list, and show/hide panel if necessary
1309            Long id = Long.valueOf(itemView.mMessageId);
1310            if (newSelected) {
1311                mChecked.add(id);
1312            } else {
1313                mChecked.remove(id);
1314            }
1315
1316            MessageList.this.showMultiPanel(mChecked.size() > 0);
1317        }
1318
1319        /**
1320         * This is used as a callback from the list items, to set the favorite state
1321         *
1322         * @param itemView the item being changed
1323         * @param newFavorite the new value of the favorite flag (star state)
1324         */
1325        public void updateFavorite(MessageListItem itemView, boolean newFavorite) {
1326            ImageView favoriteView = (ImageView) itemView.findViewById(R.id.favorite);
1327            favoriteView.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff);
1328            onSetMessageFavorite(itemView.mMessageId, newFavorite);
1329        }
1330    }
1331}
1332