MessageList.java revision ded3c915d88a5ee2d143b75cbf5718dae92a2f1c
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.R;
21import com.android.email.Utility;
22import com.android.email.activity.setup.AccountSettings;
23import com.android.email.mail.MessagingException;
24import com.android.email.provider.EmailContent;
25import com.android.email.provider.EmailContent.MessageColumns;
26
27import android.app.ListActivity;
28import android.content.ContentUris;
29import android.content.ContentValues;
30import android.content.Context;
31import android.content.Intent;
32import android.content.res.Resources;
33import android.database.Cursor;
34import android.graphics.drawable.Drawable;
35import android.net.Uri;
36import android.os.AsyncTask;
37import android.os.Bundle;
38import android.os.Handler;
39import android.view.ContextMenu;
40import android.view.LayoutInflater;
41import android.view.Menu;
42import android.view.MenuItem;
43import android.view.View;
44import android.view.ViewGroup;
45import android.view.Window;
46import android.view.ContextMenu.ContextMenuInfo;
47import android.view.View.OnClickListener;
48import android.view.animation.AnimationUtils;
49import android.widget.AdapterView;
50import android.widget.CursorAdapter;
51import android.widget.ImageView;
52import android.widget.ListView;
53import android.widget.TextView;
54import android.widget.Toast;
55import android.widget.AdapterView.OnItemClickListener;
56
57import java.util.Date;
58import java.util.HashSet;
59import java.util.Set;
60
61public class MessageList extends ListActivity implements OnItemClickListener, OnClickListener {
62
63    // Intent extras (internal to this activity)
64    private static final String EXTRA_ACCOUNT_ID = "com.android.email.activity._ACCOUNT_ID";
65    private static final String EXTRA_MAILBOX_TYPE = "com.android.email.activity.MAILBOX_TYPE";
66    private static final String EXTRA_MAILBOX_ID = "com.android.email.activity.MAILBOX_ID";
67    private static final String EXTRA_ACCOUNT_NAME = "com.android.email.activity.ACCOUNT_NAME";
68    private static final String EXTRA_MAILBOX_NAME = "com.android.email.activity.MAILBOX_NAME";
69
70    // UI support
71    private ListView mListView;
72    private View mMultiSelectPanel;
73    private View mReadUnreadButton;
74    private View mFavoriteButton;
75    private View mDeleteButton;
76    private MessageListAdapter mListAdapter;
77    private MessageListHandler mHandler = new MessageListHandler();
78    private ControllerResults mControllerCallback = new ControllerResults();
79
80    // DB access
81    private long mMailboxId;
82    private LoadMessagesTask mLoadMessagesTask;
83
84    /**
85     * Open a specific mailbox.
86     *
87     * TODO This should just shortcut to a more generic version that can accept a list of
88     * accounts/mailboxes (e.g. merged inboxes).
89     *
90     * @param context
91     * @param id mailbox key
92     * @param accountName the account we're viewing
93     * @param mailboxName the mailbox we're viewing
94     */
95    public static void actionHandleAccount(Context context, long id,
96            String accountName, String mailboxName) {
97        Intent intent = new Intent(context, MessageList.class);
98        intent.putExtra(EXTRA_MAILBOX_ID, id);
99        intent.putExtra(EXTRA_ACCOUNT_NAME, accountName);
100        intent.putExtra(EXTRA_MAILBOX_NAME, mailboxName);
101        context.startActivity(intent);
102    }
103
104    /**
105     * Open a specific mailbox by account & type
106     *
107     * @param context The caller's context (for generating an intent)
108     * @param accountId The account to open
109     * @param mailboxType the type of mailbox to open (e.g. @see EmailContent.Mailbox.TYPE_INBOX)
110     */
111    public static void actionHandleAccount(Context context, long accountId, int mailboxType) {
112        Intent intent = new Intent(context, MessageList.class);
113        intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
114        intent.putExtra(EXTRA_MAILBOX_TYPE, mailboxType);
115        context.startActivity(intent);
116    }
117
118    /**
119     * Return an intent to open a specific mailbox by account & type.  It will also clear
120     * notifications.
121     *
122     * @param context The caller's context (for generating an intent)
123     * @param accountId The account to open
124     * @param mailboxType the type of mailbox to open (e.g. @see EmailContent.Mailbox.TYPE_INBOX)
125     */
126    public static Intent actionHandleAccountIntent(Context context, long accountId,
127            int mailboxType) {
128        Intent intent = new Intent(context, MessageList.class);
129        intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
130        intent.putExtra(EXTRA_MAILBOX_TYPE, mailboxType);
131        return intent;
132    }
133
134    @Override
135    public void onCreate(Bundle icicle) {
136        super.onCreate(icicle);
137
138        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
139
140        setContentView(R.layout.message_list);
141        mListView = getListView();
142        mMultiSelectPanel = findViewById(R.id.footer_organize);
143        mReadUnreadButton = findViewById(R.id.btn_read_unread);
144        mFavoriteButton = findViewById(R.id.btn_multi_favorite);
145        mDeleteButton = findViewById(R.id.btn_multi_delete);
146
147        mReadUnreadButton.setOnClickListener(this);
148        mFavoriteButton.setOnClickListener(this);
149        mDeleteButton.setOnClickListener(this);
150
151        mListView.setOnItemClickListener(this);
152        mListView.setItemsCanFocus(false);
153        registerForContextMenu(mListView);
154
155        mListAdapter = new MessageListAdapter(this);
156        setListAdapter(mListAdapter);
157
158        // TODO extend this to properly deal with multiple mailboxes, cursor, etc.
159        mMailboxId = getIntent().getLongExtra(EXTRA_MAILBOX_ID, -1);
160        if (mMailboxId == -1) {
161            // Try account/type mode
162            long accountId = getIntent().getLongExtra(EXTRA_ACCOUNT_ID, -1);
163            int mailboxType = getIntent().getIntExtra(EXTRA_MAILBOX_TYPE, -1);
164            Cursor c = null;
165            try {
166                c = getContentResolver().query(EmailContent.Mailbox.CONTENT_URI,
167                        EmailContent.Mailbox.CONTENT_PROJECTION,
168                        EmailContent.MailboxColumns.ACCOUNT_KEY + "=? AND " +
169                        EmailContent.MailboxColumns.TYPE + "=?",
170                        new String[] { Long.toString(accountId), Integer.toString(mailboxType) },
171                        null);
172                if (c.moveToFirst()) {
173                    mMailboxId = c.getLong(EmailContent.Mailbox.CONTENT_ID_COLUMN);
174                }
175            } finally {
176                if (c != null) c.close();
177            }
178
179        }
180
181        // TODO set title to "account > mailbox (#unread)"
182
183        mLoadMessagesTask = (LoadMessagesTask) new LoadMessagesTask(mMailboxId).execute();
184    }
185
186    @Override
187    public void onPause() {
188        super.onPause();
189        Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback);
190    }
191
192    @Override
193    public void onResume() {
194        super.onResume();
195        Controller.getInstance(getApplication()).addResultCallback(mControllerCallback);
196
197        // TODO: may need to clear notifications here
198    }
199
200    @Override
201    protected void onDestroy() {
202        super.onDestroy();
203
204        if (mLoadMessagesTask != null &&
205                mLoadMessagesTask.getStatus() != LoadMessagesTask.Status.FINISHED) {
206            mLoadMessagesTask.cancel(true);
207            mLoadMessagesTask = null;
208        }
209    }
210
211    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
212        MessageListItem itemView = (MessageListItem) view;
213        onOpenMessage(id, itemView.mMailboxId);
214    }
215
216    public void onClick(View v) {
217        switch (v.getId()) {
218            case R.id.btn_read_unread:
219                onMultiToggleRead(mListAdapter.getSelectedSet());
220                break;
221            case R.id.btn_multi_favorite:
222                onMultiToggleFavorite(mListAdapter.getSelectedSet());
223                break;
224            case R.id.btn_multi_delete:
225                onMultiDelete(mListAdapter.getSelectedSet());
226                break;
227        }
228    }
229
230    @Override
231    public boolean onCreateOptionsMenu(Menu menu) {
232        super.onCreateOptionsMenu(menu);
233        getMenuInflater().inflate(R.menu.message_list_option, menu);
234        return true;
235    }
236
237    @Override
238    public boolean onOptionsItemSelected(MenuItem item) {
239        switch (item.getItemId()) {
240            case R.id.refresh:
241                onRefresh();
242                return true;
243            case R.id.accounts:
244                onAccounts();
245                return true;
246            case R.id.compose:
247                onCompose();
248                return true;
249            case R.id.account_settings:
250                onEditAccount();
251                return true;
252            default:
253                return super.onOptionsItemSelected(item);
254        }
255    }
256
257    @Override
258    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
259        super.onCreateContextMenu(menu, v, menuInfo);
260        AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
261        MessageListItem itemView = (MessageListItem) info.targetView;
262
263        // TODO: There is no context menu for the outbox
264        // TODO: There is probably a special context menu for the trash
265
266        getMenuInflater().inflate(R.menu.message_list_context, menu);
267
268        // The default menu contains "mark as read".  If the message is read, change
269        // the menu text to "mark as unread."
270        if (itemView.mRead) {
271            menu.findItem(R.id.mark_as_read).setTitle(R.string.mark_as_unread_action);
272        }
273    }
274
275    @Override
276    public boolean onContextItemSelected(MenuItem item) {
277        AdapterView.AdapterContextMenuInfo info =
278            (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
279        MessageListItem itemView = (MessageListItem) info.targetView;
280
281        switch (item.getItemId()) {
282            case R.id.open:
283                onOpenMessage(info.id, itemView.mMailboxId);
284                break;
285            case R.id.delete:
286                onDelete(info.id, itemView.mAccountId);
287                break;
288            case R.id.reply:
289                //onReply(holder);
290                break;
291            case R.id.reply_all:
292                //onReplyAll(holder);
293                break;
294            case R.id.forward:
295                //onForward(holder);
296                break;
297            case R.id.mark_as_read:
298                onToggleRead(info.id, itemView.mRead);
299                break;
300        }
301        return super.onContextItemSelected(item);
302    }
303
304    private void onRefresh() {
305        // TODO: This needs to loop through all open mailboxes (there might be more than one)
306        EmailContent.Mailbox mailbox =
307                EmailContent.Mailbox.restoreMailboxWithId(this, mMailboxId);
308        EmailContent.Account account =
309                EmailContent.Account.restoreAccountWithId(this, mailbox.mAccountKey);
310        mHandler.progress(true);
311        Controller.getInstance(getApplication()).updateMailbox(
312                account, mailbox, mControllerCallback);
313    }
314
315    private void onAccounts() {
316        AccountFolderList.actionShowAccounts(this);
317        finish();
318    }
319
320    private void onCompose() {
321        // TODO: Select correct account to send from when there are multiple mailboxes
322        EmailContent.Mailbox mailbox =
323                EmailContent.Mailbox.restoreMailboxWithId(this, mMailboxId);
324        MessageCompose.actionCompose(this, mailbox.mAccountKey);
325    }
326
327    private void onEditAccount() {
328        // TODO: Select correct account to edit when there are multiple mailboxes
329        EmailContent.Mailbox mailbox =
330                EmailContent.Mailbox.restoreMailboxWithId(this, mMailboxId);
331        AccountSettings.actionSettings(this, mailbox.mAccountKey);
332    }
333
334    public void onOpenMessage(long messageId, long mailboxId) {
335        EmailContent.Mailbox mailbox = EmailContent.Mailbox.restoreMailboxWithId(this, mailboxId);
336
337        if (mailbox.mType == EmailContent.Mailbox.TYPE_DRAFTS) {
338            // TODO need id-based API for MessageCompose
339            // MessageCompose.actionEditDraft(this, messageId);
340        } else {
341            MessageView.actionView(this, messageId);
342        }
343    }
344
345    private void onDelete(long messageId, long accountId) {
346        Controller.getInstance(getApplication()).deleteMessage(messageId, accountId);
347        Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show();
348    }
349
350    private void onToggleRead(long messageId, boolean oldRead) {
351        boolean isRead = ! oldRead;
352
353        // TODO this should be a call to the controller, since it may possibly kick off
354        // more than just a DB update.  Also, the DB update shouldn't be in the UI thread
355        // as it is here.  Also, it needs to update the read/unread count in the mailbox?
356        ContentValues cv = new ContentValues();
357        cv.put(EmailContent.MessageColumns.FLAG_READ, isRead);
358        Uri uri = ContentUris.withAppendedId(
359                EmailContent.Message.SYNCED_CONTENT_URI, messageId);
360        getContentResolver().update(uri, cv, null, null);
361    }
362
363    /**
364     * Toggles a set read/unread states.  Note, the default behavior is "mark unread", so the
365     * sense of the helper methods is "true=unread".
366     *
367     * @param selectedSet The current list of selected items
368     */
369    private void onMultiToggleRead(Set<Long> selectedSet) {
370        int numChanged = toggleMultiple(selectedSet, new MultiToggleHelper() {
371
372            public boolean getField(long messageId, Cursor c) {
373                return c.getInt(MessageListAdapter.COLUMN_READ) == 0;
374            }
375
376            public boolean setField(long messageId, Cursor c, boolean newValue) {
377                boolean oldValue = getField(messageId, c);
378                if (oldValue != newValue) {
379                    onToggleRead(messageId, !oldValue);
380                    return true;
381                }
382                return false;
383            }
384        });
385    }
386
387    /**
388     * Toggles a set of favorites (stars)
389     *
390     * @param selectedSet The current list of selected items
391     */
392    private void onMultiToggleFavorite(Set<Long> selectedSet) {
393        int numChanged = toggleMultiple(selectedSet, new MultiToggleHelper() {
394
395            public boolean getField(long messageId, Cursor c) {
396                return c.getInt(MessageListAdapter.COLUMN_FAVORITE) != 0;
397            }
398
399            public boolean setField(long messageId, Cursor c, boolean newValue) {
400                boolean oldValue = getField(messageId, c);
401                if (oldValue != newValue) {
402                    // Update provider
403                    // TODO this should probably be a call to the controller, since it may possibly
404                    // kick off more than just a DB update.
405                    ContentValues cv = new ContentValues();
406                    cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, newValue);
407                    Uri uri = ContentUris.withAppendedId(
408                            EmailContent.Message.SYNCED_CONTENT_URI, messageId);
409                    MessageList.this.getContentResolver().update(uri, cv, null, null);
410
411                    return true;
412                }
413                return false;
414            }
415        });
416    }
417
418    private void onMultiDelete(Set<Long> selectedSet) {
419        // Clone the set, because deleting is going to thrash things
420        HashSet<Long> cloneSet = new HashSet<Long>(selectedSet);
421        for (Long id : cloneSet) {
422            Controller.getInstance(getApplication()).deleteMessage(id, -1);
423        }
424        // TODO: count messages and show "n messages deleted"
425        Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show();
426        selectedSet.clear();
427        showMultiPanel(false);
428    }
429
430    private interface MultiToggleHelper {
431        /**
432         * Return true if the field of interest is "set".  If one or more are false, then our
433         * bulk action will be to "set".  If all are set, our bulk action will be to "clear".
434         * @param messageId the message id of the current message
435         * @param c the cursor, positioned to the item of interest
436         * @return true if the field at this row is "set"
437         */
438        public boolean getField(long messageId, Cursor c);
439
440        /**
441         * Set or clear the field of interest.  Return true if a change was made.
442         * @param messageId the message id of the current message
443         * @param c the cursor, positioned to the item of interest
444         * @param newValue the new value to be set at this row
445         * @return true if a change was actually made
446         */
447        public boolean setField(long messageId, Cursor c, boolean newValue);
448    }
449
450    /**
451     * Toggle multiple fields in a message, using the following logic:  If one or more fields
452     * are "clear", then "set" them.  If all fields are "set", then "clear" them all.
453     *
454     * @param selectedSet the set of messages that are selected
455     * @param helper functions to implement the specific getter & setter
456     * @return the number of messages that were updated
457     */
458    private int toggleMultiple(Set<Long> selectedSet, MultiToggleHelper helper) {
459        Cursor c = mListAdapter.getCursor();
460        boolean anyWereFound = false;
461        boolean allWereSet = true;
462
463        c.moveToPosition(-1);
464        while (c.moveToNext()) {
465            long id = c.getInt(MessageListAdapter.COLUMN_ID);
466            if (selectedSet.contains(Long.valueOf(id))) {
467                anyWereFound = true;
468                if (!helper.getField(id, c)) {
469                    allWereSet = false;
470                    break;
471                }
472            }
473        }
474
475        int numChanged = 0;
476
477        if (anyWereFound) {
478            boolean newValue = !allWereSet;
479            c.moveToPosition(-1);
480            while (c.moveToNext()) {
481                long id = c.getInt(MessageListAdapter.COLUMN_ID);
482                if (selectedSet.contains(Long.valueOf(id))) {
483                    if (helper.setField(id, c, newValue)) {
484                        ++numChanged;
485                    }
486                }
487            }
488        }
489
490        return numChanged;
491    }
492
493    /**
494     * Show or hide the panel of multi-select options
495     */
496    private void showMultiPanel(boolean show) {
497        if (show && mMultiSelectPanel.getVisibility() != View.VISIBLE) {
498            mMultiSelectPanel.setVisibility(View.VISIBLE);
499            mMultiSelectPanel.startAnimation(
500                    AnimationUtils.loadAnimation(this, R.anim.footer_appear));
501
502        } else if (!show && mMultiSelectPanel.getVisibility() != View.GONE) {
503            mMultiSelectPanel.setVisibility(View.GONE);
504            mMultiSelectPanel.startAnimation(
505                        AnimationUtils.loadAnimation(this, R.anim.footer_disappear));
506        }
507    }
508
509    /**
510     * Async task for loading a single folder out of the UI thread
511     *
512     * TODO: Extend API to support compound select (e.g. merged inbox list)
513     */
514    private class LoadMessagesTask extends AsyncTask<Void, Void, Cursor> {
515
516        private long mMailboxKey;
517
518        /**
519         * Special constructor to cache some local info
520         */
521        public LoadMessagesTask(long mailboxKey) {
522            mMailboxKey = mailboxKey;
523        }
524
525        @Override
526        protected Cursor doInBackground(Void... params) {
527            return MessageList.this.managedQuery(
528                    EmailContent.Message.CONTENT_URI,
529                    MessageList.this.mListAdapter.PROJECTION,
530                    EmailContent.MessageColumns.MAILBOX_KEY + "=?",
531                    new String[] {
532                        String.valueOf(mMailboxKey)
533                    },
534                    EmailContent.MessageColumns.TIMESTAMP + " DESC");
535        }
536
537        @Override
538        protected void onPostExecute(Cursor cursor) {
539            MessageList.this.mListAdapter.changeCursor(cursor);
540
541            // TODO: remove this hack and only update at the right time
542            if (cursor != null && cursor.getCount() == 0) {
543                onRefresh();
544            }
545        }
546    }
547
548    /**
549     * Handler for UI-thread operations (when called from callbacks or any other threads)
550     */
551    class MessageListHandler extends Handler {
552        private static final int MSG_PROGRESS = 1;
553
554        @Override
555        public void handleMessage(android.os.Message msg) {
556            switch (msg.what) {
557                case MSG_PROGRESS:
558                    setProgressBarIndeterminateVisibility(msg.arg1 != 0);
559                    break;
560                default:
561                    super.handleMessage(msg);
562            }
563        }
564
565        /**
566         * Call from any thread to start/stop progress indicator(s)
567         * @param progress true to start, false to stop
568         */
569        public void progress(boolean progress) {
570            android.os.Message msg = android.os.Message.obtain();
571            msg.what = MSG_PROGRESS;
572            msg.arg1 = progress ? 1 : 0;
573            sendMessage(msg);
574        }
575    }
576
577    /**
578     * Callback for async Controller results.  This is all a placeholder until we figure out the
579     * final way to do this.
580     */
581    private class ControllerResults implements Controller.Result {
582        public void updateMailboxListCallback(MessagingException result, long accountKey) {
583        }
584
585        public void updateMailboxCallback(MessagingException result, long accountKey,
586                long mailboxKey, int totalMessagesInMailbox, int numNewMessages) {
587            mHandler.progress(false);
588        }
589    }
590
591    /**
592     * This class implements the adapter for displaying messages based on cursors.
593     */
594    /* package */ class MessageListAdapter extends CursorAdapter {
595
596        public static final int COLUMN_ID = 0;
597        public static final int COLUMN_MAILBOX_KEY = 1;
598        public static final int COLUMN_ACCOUNT_KEY = 2;
599        public static final int COLUMN_DISPLAY_NAME = 3;
600        public static final int COLUMN_SUBJECT = 4;
601        public static final int COLUMN_DATE = 5;
602        public static final int COLUMN_READ = 6;
603        public static final int COLUMN_FAVORITE = 7;
604        public static final int COLUMN_ATTACHMENTS = 8;
605
606        public final String[] PROJECTION = new String[] {
607            EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
608            MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP,
609            MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT,
610        };
611
612        Context mContext;
613        private LayoutInflater mInflater;
614        private Drawable mAttachmentIcon;
615        private Drawable mFavoriteIconOn;
616        private Drawable mFavoriteIconOff;
617        private Drawable mSelectedIconOn;
618        private Drawable mSelectedIconOff;
619
620        private java.text.DateFormat mDateFormat;
621        private java.text.DateFormat mDayFormat;
622        private java.text.DateFormat mTimeFormat;
623
624        private HashSet<Long> mChecked = new HashSet<Long>();
625
626        public MessageListAdapter(Context context) {
627            super(context, null);
628            mContext = context;
629            mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
630
631            Resources resources = context.getResources();
632            mAttachmentIcon = resources.getDrawable(R.drawable.ic_mms_attachment_small);
633            mFavoriteIconOn = resources.getDrawable(android.R.drawable.star_on);
634            mFavoriteIconOff = resources.getDrawable(android.R.drawable.star_off);
635            mSelectedIconOn = resources.getDrawable(R.drawable.btn_check_buttonless_on);
636            mSelectedIconOff = resources.getDrawable(R.drawable.btn_check_buttonless_off);
637
638            mDateFormat = android.text.format.DateFormat.getDateFormat(context);    // short date
639            mDayFormat = android.text.format.DateFormat.getDateFormat(context);     // TODO: day
640            mTimeFormat = android.text.format.DateFormat.getTimeFormat(context);    // 12/24 time
641        }
642
643        public Set<Long> getSelectedSet() {
644            return mChecked;
645        }
646
647        @Override
648        public void bindView(View view, Context context, Cursor cursor) {
649            // Reset the view (in case it was recycled) and prepare for binding
650            MessageListItem itemView = (MessageListItem) view;
651            itemView.bindViewInit(this, true);
652
653            // Load the public fields in the view (for later use)
654            itemView.mMessageId = cursor.getLong(COLUMN_ID);
655            itemView.mMailboxId = cursor.getLong(COLUMN_MAILBOX_KEY);
656            itemView.mAccountId = cursor.getLong(COLUMN_ACCOUNT_KEY);
657            itemView.mRead = cursor.getInt(COLUMN_READ) != 0;
658            itemView.mFavorite = cursor.getInt(COLUMN_FAVORITE) != 0;
659            itemView.mSelected = mChecked.contains(Long.valueOf(itemView.mMessageId));
660
661            // Load the UI
662            View chipView = view.findViewById(R.id.chip);
663            chipView.getBackground().setAlpha(itemView.mRead ? 0 : 255);
664
665            TextView fromView = (TextView) view.findViewById(R.id.from);
666            String text = cursor.getString(COLUMN_DISPLAY_NAME);
667            if (text != null) fromView.setText(text);
668
669            boolean hasAttachments = cursor.getInt(COLUMN_ATTACHMENTS) != 0;
670            fromView.setCompoundDrawablesWithIntrinsicBounds(null, null,
671                    hasAttachments ? mAttachmentIcon : null, null);
672
673            TextView subjectView = (TextView) view.findViewById(R.id.subject);
674            text = cursor.getString(COLUMN_SUBJECT);
675            if (text != null) subjectView.setText(text);
676
677            // TODO ui spec suggests "time", "day", "date" - implement "day"
678            TextView dateView = (TextView) view.findViewById(R.id.date);
679            long timestamp = cursor.getLong(COLUMN_DATE);
680            Date date = new Date(timestamp);
681            if (Utility.isDateToday(date)) {
682                text = mTimeFormat.format(date);
683            } else {
684                text = mDateFormat.format(date);
685            }
686            dateView.setText(text);
687
688            ImageView selectedView = (ImageView) view.findViewById(R.id.selected);
689            selectedView.setImageDrawable(itemView.mSelected ? mSelectedIconOn : mSelectedIconOff);
690
691            ImageView favoriteView = (ImageView) view.findViewById(R.id.favorite);
692            favoriteView.setImageDrawable(itemView.mFavorite ? mFavoriteIconOn : mFavoriteIconOff);
693        }
694
695        @Override
696        public View newView(Context context, Cursor cursor, ViewGroup parent) {
697            return mInflater.inflate(R.layout.message_list_item, parent, false);
698        }
699
700        /**
701         * This is used as a callback from the list items, to set the selected state
702         *
703         * @param itemView the item being changed
704         * @param newSelected the new value of the selected flag (checkbox state)
705         */
706        public void updateSelected(MessageListItem itemView, boolean newSelected) {
707            ImageView selectedView = (ImageView) itemView.findViewById(R.id.selected);
708            selectedView.setImageDrawable(newSelected ? mSelectedIconOn : mSelectedIconOff);
709
710            // Set checkbox state in list, and show/hide panel if necessary
711            Long id = Long.valueOf(itemView.mMessageId);
712            if (newSelected) {
713                mChecked.add(id);
714            } else {
715                mChecked.remove(id);
716            }
717
718            MessageList.this.showMultiPanel(mChecked.size() > 0);
719        }
720
721        /**
722         * This is used as a callback from the list items, to set the favorite state
723         *
724         * @param itemView the item being changed
725         * @param newFavorite the new value of the favorite flag (star state)
726         */
727        public void updateFavorite(MessageListItem itemView, boolean newFavorite) {
728            ImageView favoriteView = (ImageView) itemView.findViewById(R.id.favorite);
729            favoriteView.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff);
730
731            // Update provider
732            // TODO this should probably be a call to the controller, since it may possibly kick off
733            // more than just a DB update.
734            ContentValues cv = new ContentValues();
735            cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, newFavorite);
736            Uri uri = ContentUris.withAppendedId(
737                    EmailContent.Message.SYNCED_CONTENT_URI, itemView.mMessageId);
738            mContext.getContentResolver().update(uri, cv, null, null);
739        }
740    }
741}
742