MessageList.java revision ea69fc40d75f5fcf38872f836cdea9fb3aa9c991
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.Account;
26import com.android.email.provider.EmailContent.Mailbox;
27import com.android.email.provider.EmailContent.MailboxColumns;
28import com.android.email.provider.EmailContent.Message;
29import com.android.email.provider.EmailContent.MessageColumns;
30
31import android.app.ListActivity;
32import android.content.ContentUris;
33import android.content.Context;
34import android.content.Intent;
35import android.content.res.Resources;
36import android.database.Cursor;
37import android.graphics.drawable.Drawable;
38import android.net.Uri;
39import android.os.AsyncTask;
40import android.os.Bundle;
41import android.os.Handler;
42import android.view.ContextMenu;
43import android.view.LayoutInflater;
44import android.view.Menu;
45import android.view.MenuItem;
46import android.view.View;
47import android.view.ViewGroup;
48import android.view.Window;
49import android.view.ContextMenu.ContextMenuInfo;
50import android.view.View.OnClickListener;
51import android.view.animation.AnimationUtils;
52import android.widget.AdapterView;
53import android.widget.CursorAdapter;
54import android.widget.ImageView;
55import android.widget.ListView;
56import android.widget.TextView;
57import android.widget.Toast;
58import android.widget.AdapterView.OnItemClickListener;
59
60import java.util.Date;
61import java.util.HashSet;
62import java.util.Set;
63
64public class MessageList extends ListActivity implements OnItemClickListener, OnClickListener {
65
66    // Magic mailbox ID's
67    // NOTE:  This is a quick solution for merged mailboxes.  I would rather implement this
68    // with a more generic way of packaging and sharing queries between activities
69    public static final long QUERY_ALL_INBOXES = -2;
70    public static final long QUERY_ALL_UNREAD = -3;
71    public static final long QUERY_ALL_FAVORITES = -4;
72    public static final long QUERY_ALL_DRAFTS = -5;
73    public static final long QUERY_ALL_OUTBOX = -6;
74
75    // Intent extras (internal to this activity)
76    private static final String EXTRA_ACCOUNT_ID = "com.android.email.activity._ACCOUNT_ID";
77    private static final String EXTRA_MAILBOX_TYPE = "com.android.email.activity.MAILBOX_TYPE";
78    private static final String EXTRA_MAILBOX_ID = "com.android.email.activity.MAILBOX_ID";
79    private static final String EXTRA_ACCOUNT_NAME = "com.android.email.activity.ACCOUNT_NAME";
80    private static final String EXTRA_MAILBOX_NAME = "com.android.email.activity.MAILBOX_NAME";
81
82    // UI support
83    private ListView mListView;
84    private View mMultiSelectPanel;
85    private View mReadUnreadButton;
86    private View mFavoriteButton;
87    private View mDeleteButton;
88    private MessageListAdapter mListAdapter;
89    private MessageListHandler mHandler = new MessageListHandler();
90    private ControllerResults mControllerCallback = new ControllerResults();
91
92    private static final int[] mColorChipResIds = new int[] {
93        R.drawable.appointment_indicator_leftside_1,
94        R.drawable.appointment_indicator_leftside_2,
95        R.drawable.appointment_indicator_leftside_3,
96        R.drawable.appointment_indicator_leftside_4,
97        R.drawable.appointment_indicator_leftside_5,
98        R.drawable.appointment_indicator_leftside_6,
99        R.drawable.appointment_indicator_leftside_7,
100        R.drawable.appointment_indicator_leftside_8,
101        R.drawable.appointment_indicator_leftside_9,
102        R.drawable.appointment_indicator_leftside_10,
103        R.drawable.appointment_indicator_leftside_11,
104        R.drawable.appointment_indicator_leftside_12,
105        R.drawable.appointment_indicator_leftside_13,
106        R.drawable.appointment_indicator_leftside_14,
107        R.drawable.appointment_indicator_leftside_15,
108        R.drawable.appointment_indicator_leftside_16,
109        R.drawable.appointment_indicator_leftside_17,
110        R.drawable.appointment_indicator_leftside_18,
111        R.drawable.appointment_indicator_leftside_19,
112        R.drawable.appointment_indicator_leftside_20,
113        R.drawable.appointment_indicator_leftside_21,
114    };
115
116    // DB access
117    private long mMailboxId;
118    private LoadMessagesTask mLoadMessagesTask;
119    private FindMailboxTask mFindMailboxTask;
120
121    /**
122     * Reduced mailbox projection used to hunt for inboxes
123     * TODO: remove this and implement a custom URI
124     */
125    public final static int MAILBOX_FIND_INBOX_COLUMN_ID = 0;
126
127    public final static String[] MAILBOX_FIND_INBOX_PROJECTION = new String[] {
128        EmailContent.RECORD_ID, MailboxColumns.TYPE, MailboxColumns.FLAG_VISIBLE
129    };
130
131    /**
132     * Open a specific mailbox.
133     *
134     * TODO This should just shortcut to a more generic version that can accept a list of
135     * accounts/mailboxes (e.g. merged inboxes).
136     *
137     * @param context
138     * @param id mailbox key
139     * @param accountName the account we're viewing (for title formatting - not for lookup)
140     * @param mailboxName the mailbox we're viewing (for title formatting - not for lookup)
141     */
142    public static void actionHandleAccount(Context context, long id,
143            String accountName, String mailboxName) {
144        Intent intent = new Intent(context, MessageList.class);
145        intent.putExtra(EXTRA_MAILBOX_ID, id);
146        intent.putExtra(EXTRA_ACCOUNT_NAME, accountName);
147        intent.putExtra(EXTRA_MAILBOX_NAME, mailboxName);
148        context.startActivity(intent);
149    }
150
151    /**
152     * Open a specific mailbox by account & type
153     *
154     * @param context The caller's context (for generating an intent)
155     * @param accountId The account to open
156     * @param mailboxType the type of mailbox to open (e.g. @see EmailContent.Mailbox.TYPE_INBOX)
157     */
158    public static void actionHandleAccount(Context context, long accountId, int mailboxType) {
159        Intent intent = new Intent(context, MessageList.class);
160        intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
161        intent.putExtra(EXTRA_MAILBOX_TYPE, mailboxType);
162        context.startActivity(intent);
163    }
164
165    /**
166     * Return an intent to open a specific mailbox by account & type.  It will also clear
167     * notifications.
168     *
169     * @param context The caller's context (for generating an intent)
170     * @param accountId The account to open
171     * @param mailboxType the type of mailbox to open (e.g. @see EmailContent.Mailbox.TYPE_INBOX)
172     */
173    public static Intent actionHandleAccountIntent(Context context, long accountId,
174            int mailboxType) {
175        Intent intent = new Intent(context, MessageList.class);
176        intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
177        intent.putExtra(EXTRA_MAILBOX_TYPE, mailboxType);
178        return intent;
179    }
180
181    /**
182     * Used for generating lightweight (Uri-only) intents.
183     *
184     * @param context Calling context for building the intent
185     * @param accountId The account of interest
186     * @param mailboxType The folder name to open (typically Mailbox.TYPE_INBOX)
187     * @return an Intent which can be used to view that account
188     */
189    public static Intent actionHandleAccountUriIntent(Context context, long accountId,
190            int mailboxType) {
191        Intent i = actionHandleAccountIntent(context, accountId, mailboxType);
192        i.removeExtra(EXTRA_ACCOUNT_ID);
193        Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
194        i.setData(uri);
195        return i;
196    }
197
198    @Override
199    public void onCreate(Bundle icicle) {
200        super.onCreate(icicle);
201
202        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
203
204        setContentView(R.layout.message_list);
205        mListView = getListView();
206        mMultiSelectPanel = findViewById(R.id.footer_organize);
207        mReadUnreadButton = findViewById(R.id.btn_read_unread);
208        mFavoriteButton = findViewById(R.id.btn_multi_favorite);
209        mDeleteButton = findViewById(R.id.btn_multi_delete);
210
211        mReadUnreadButton.setOnClickListener(this);
212        mFavoriteButton.setOnClickListener(this);
213        mDeleteButton.setOnClickListener(this);
214
215        mListView.setOnItemClickListener(this);
216        mListView.setItemsCanFocus(false);
217        registerForContextMenu(mListView);
218
219        mListAdapter = new MessageListAdapter(this);
220        setListAdapter(mListAdapter);
221
222        // TODO extend this to properly deal with multiple mailboxes, cursor, etc.
223
224        // Select 'by id' or 'by type' or 'by uri' mode and launch appropriate queries
225
226        mMailboxId = getIntent().getLongExtra(EXTRA_MAILBOX_ID, -1);
227        if (mMailboxId != -1) {
228            // Specific mailbox ID was provided - go directly to it
229            mLoadMessagesTask = new LoadMessagesTask(mMailboxId);
230            mLoadMessagesTask.execute();
231        } else {
232            long accountId = -1;
233            int mailboxType = getIntent().getIntExtra(EXTRA_MAILBOX_TYPE, Mailbox.TYPE_INBOX);
234            Uri uri = getIntent().getData();
235            if (uri != null
236                    && "content".equals(uri.getScheme())
237                    && EmailContent.AUTHORITY.equals(uri.getAuthority())) {
238                // A content URI was provided - try to look up the account
239                String accountIdString = uri.getPathSegments().get(1);
240                if (accountIdString != null) {
241                    accountId = Long.parseLong(accountIdString);
242                }
243                mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false);
244                mFindMailboxTask.execute();
245            } else {
246                // Go by account id + type
247                accountId = getIntent().getLongExtra(EXTRA_ACCOUNT_ID, -1);
248                mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, true);
249                mFindMailboxTask.execute();
250            }
251        }
252
253        // TODO set title to "account > mailbox (#unread)"
254    }
255
256    @Override
257    public void onPause() {
258        super.onPause();
259        Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback);
260    }
261
262    @Override
263    public void onResume() {
264        super.onResume();
265        Controller.getInstance(getApplication()).addResultCallback(mControllerCallback);
266
267        // TODO: may need to clear notifications here
268    }
269
270    @Override
271    protected void onDestroy() {
272        super.onDestroy();
273
274        if (mLoadMessagesTask != null &&
275                mLoadMessagesTask.getStatus() != LoadMessagesTask.Status.FINISHED) {
276            mLoadMessagesTask.cancel(true);
277            mLoadMessagesTask = null;
278        }
279        if (mFindMailboxTask != null &&
280                mFindMailboxTask.getStatus() != FindMailboxTask.Status.FINISHED) {
281            mFindMailboxTask.cancel(true);
282            mFindMailboxTask = null;
283        }
284    }
285
286    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
287        MessageListItem itemView = (MessageListItem) view;
288        onOpenMessage(id, itemView.mMailboxId);
289    }
290
291    public void onClick(View v) {
292        switch (v.getId()) {
293            case R.id.btn_read_unread:
294                onMultiToggleRead(mListAdapter.getSelectedSet());
295                break;
296            case R.id.btn_multi_favorite:
297                onMultiToggleFavorite(mListAdapter.getSelectedSet());
298                break;
299            case R.id.btn_multi_delete:
300                onMultiDelete(mListAdapter.getSelectedSet());
301                break;
302        }
303    }
304
305    @Override
306    public boolean onCreateOptionsMenu(Menu menu) {
307        super.onCreateOptionsMenu(menu);
308        getMenuInflater().inflate(R.menu.message_list_option, menu);
309        return true;
310    }
311
312    @Override
313    public boolean onOptionsItemSelected(MenuItem item) {
314        switch (item.getItemId()) {
315            case R.id.refresh:
316                onRefresh();
317                return true;
318            case R.id.accounts:
319                onAccounts();
320                return true;
321            case R.id.compose:
322                onCompose();
323                return true;
324            case R.id.account_settings:
325                onEditAccount();
326                return true;
327            default:
328                return super.onOptionsItemSelected(item);
329        }
330    }
331
332    @Override
333    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
334        super.onCreateContextMenu(menu, v, menuInfo);
335        AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
336        MessageListItem itemView = (MessageListItem) info.targetView;
337
338        // TODO: There is no context menu for the outbox
339        // TODO: There is probably a special context menu for the trash
340
341        getMenuInflater().inflate(R.menu.message_list_context, menu);
342
343        // The default menu contains "mark as read".  If the message is read, change
344        // the menu text to "mark as unread."
345        if (itemView.mRead) {
346            menu.findItem(R.id.mark_as_read).setTitle(R.string.mark_as_unread_action);
347        }
348    }
349
350    @Override
351    public boolean onContextItemSelected(MenuItem item) {
352        AdapterView.AdapterContextMenuInfo info =
353            (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
354        MessageListItem itemView = (MessageListItem) info.targetView;
355
356        switch (item.getItemId()) {
357            case R.id.open:
358                onOpenMessage(info.id, itemView.mMailboxId);
359                break;
360            case R.id.delete:
361                onDelete(info.id, itemView.mAccountId);
362                break;
363            case R.id.reply:
364                //onReply(holder);
365                break;
366            case R.id.reply_all:
367                //onReplyAll(holder);
368                break;
369            case R.id.forward:
370                //onForward(holder);
371                break;
372            case R.id.mark_as_read:
373                onSetMessageRead(info.id, !itemView.mRead);
374                break;
375        }
376        return super.onContextItemSelected(item);
377    }
378
379    private void onRefresh() {
380        // TODO: This needs to loop through all open mailboxes (there might be more than one)
381        // TODO: Should not be reading from DB in UI thread
382        if (mMailboxId >= 0) {
383            EmailContent.Mailbox mailbox =
384                    EmailContent.Mailbox.restoreMailboxWithId(this, mMailboxId);
385            EmailContent.Account account =
386                    EmailContent.Account.restoreAccountWithId(this, mailbox.mAccountKey);
387            mHandler.progress(true);
388            Controller.getInstance(getApplication()).updateMailbox(
389                    account, mailbox, mControllerCallback);
390        }
391    }
392
393    private void onAccounts() {
394        AccountFolderList.actionShowAccounts(this);
395        finish();
396    }
397
398    private void onCompose() {
399        // TODO: Select correct account to send from when there are multiple mailboxes
400        // TODO: Should not be reading from DB in UI thread
401        if (mMailboxId >= 0) {
402            EmailContent.Mailbox mailbox =
403                    EmailContent.Mailbox.restoreMailboxWithId(this, mMailboxId);
404            MessageCompose.actionCompose(this, mailbox.mAccountKey);
405        }
406    }
407
408    private void onEditAccount() {
409        // TODO: Select correct account to edit when there are multiple mailboxes
410        // TODO: Should not be reading from DB in UI thread
411        if (mMailboxId >= 0) {
412            EmailContent.Mailbox mailbox =
413                    EmailContent.Mailbox.restoreMailboxWithId(this, mMailboxId);
414            AccountSettings.actionSettings(this, mailbox.mAccountKey);
415        }
416    }
417
418    public void onOpenMessage(long messageId, long mailboxId) {
419        // TODO: Should not be reading from DB in UI thread
420        EmailContent.Mailbox mailbox = EmailContent.Mailbox.restoreMailboxWithId(this, mailboxId);
421
422        if (mailbox.mType == EmailContent.Mailbox.TYPE_DRAFTS) {
423            MessageCompose.actionEditDraft(this, messageId);
424        } else {
425            MessageView.actionView(this, messageId);
426        }
427    }
428
429    private void onDelete(long messageId, long accountId) {
430        Controller.getInstance(getApplication()).deleteMessage(messageId, accountId);
431        Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show();
432    }
433
434    private void onSetMessageRead(long messageId, boolean newRead) {
435        Controller.getInstance(getApplication()).setMessageRead(messageId, newRead);
436    }
437
438    private void onSetMessageFavorite(long messageId, boolean newFavorite) {
439        Controller.getInstance(getApplication()).setMessageFavorite(messageId, newFavorite);
440    }
441
442    /**
443     * Toggles a set read/unread states.  Note, the default behavior is "mark unread", so the
444     * sense of the helper methods is "true=unread".
445     *
446     * @param selectedSet The current list of selected items
447     */
448    private void onMultiToggleRead(Set<Long> selectedSet) {
449        int numChanged = toggleMultiple(selectedSet, new MultiToggleHelper() {
450
451            public boolean getField(long messageId, Cursor c) {
452                return c.getInt(MessageListAdapter.COLUMN_READ) == 0;
453            }
454
455            public boolean setField(long messageId, Cursor c, boolean newValue) {
456                boolean oldValue = getField(messageId, c);
457                if (oldValue != newValue) {
458                    onSetMessageRead(messageId, !newValue);
459                    return true;
460                }
461                return false;
462            }
463        });
464    }
465
466    /**
467     * Toggles a set of favorites (stars)
468     *
469     * @param selectedSet The current list of selected items
470     */
471    private void onMultiToggleFavorite(Set<Long> selectedSet) {
472        int numChanged = toggleMultiple(selectedSet, new MultiToggleHelper() {
473
474            public boolean getField(long messageId, Cursor c) {
475                return c.getInt(MessageListAdapter.COLUMN_FAVORITE) != 0;
476            }
477
478            public boolean setField(long messageId, Cursor c, boolean newValue) {
479                boolean oldValue = getField(messageId, c);
480                if (oldValue != newValue) {
481                    onSetMessageFavorite(messageId, newValue);
482                    return true;
483                }
484                return false;
485            }
486        });
487    }
488
489    private void onMultiDelete(Set<Long> selectedSet) {
490        // Clone the set, because deleting is going to thrash things
491        HashSet<Long> cloneSet = new HashSet<Long>(selectedSet);
492        for (Long id : cloneSet) {
493            Controller.getInstance(getApplication()).deleteMessage(id, -1);
494        }
495        // TODO: count messages and show "n messages deleted"
496        Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show();
497        selectedSet.clear();
498        showMultiPanel(false);
499    }
500
501    private interface MultiToggleHelper {
502        /**
503         * Return true if the field of interest is "set".  If one or more are false, then our
504         * bulk action will be to "set".  If all are set, our bulk action will be to "clear".
505         * @param messageId the message id of the current message
506         * @param c the cursor, positioned to the item of interest
507         * @return true if the field at this row is "set"
508         */
509        public boolean getField(long messageId, Cursor c);
510
511        /**
512         * Set or clear the field of interest.  Return true if a change was made.
513         * @param messageId the message id of the current message
514         * @param c the cursor, positioned to the item of interest
515         * @param newValue the new value to be set at this row
516         * @return true if a change was actually made
517         */
518        public boolean setField(long messageId, Cursor c, boolean newValue);
519    }
520
521    /**
522     * Toggle multiple fields in a message, using the following logic:  If one or more fields
523     * are "clear", then "set" them.  If all fields are "set", then "clear" them all.
524     *
525     * @param selectedSet the set of messages that are selected
526     * @param helper functions to implement the specific getter & setter
527     * @return the number of messages that were updated
528     */
529    private int toggleMultiple(Set<Long> selectedSet, MultiToggleHelper helper) {
530        Cursor c = mListAdapter.getCursor();
531        boolean anyWereFound = false;
532        boolean allWereSet = true;
533
534        c.moveToPosition(-1);
535        while (c.moveToNext()) {
536            long id = c.getInt(MessageListAdapter.COLUMN_ID);
537            if (selectedSet.contains(Long.valueOf(id))) {
538                anyWereFound = true;
539                if (!helper.getField(id, c)) {
540                    allWereSet = false;
541                    break;
542                }
543            }
544        }
545
546        int numChanged = 0;
547
548        if (anyWereFound) {
549            boolean newValue = !allWereSet;
550            c.moveToPosition(-1);
551            while (c.moveToNext()) {
552                long id = c.getInt(MessageListAdapter.COLUMN_ID);
553                if (selectedSet.contains(Long.valueOf(id))) {
554                    if (helper.setField(id, c, newValue)) {
555                        ++numChanged;
556                    }
557                }
558            }
559        }
560
561        return numChanged;
562    }
563
564    /**
565     * Show or hide the panel of multi-select options
566     */
567    private void showMultiPanel(boolean show) {
568        if (show && mMultiSelectPanel.getVisibility() != View.VISIBLE) {
569            mMultiSelectPanel.setVisibility(View.VISIBLE);
570            mMultiSelectPanel.startAnimation(
571                    AnimationUtils.loadAnimation(this, R.anim.footer_appear));
572
573        } else if (!show && mMultiSelectPanel.getVisibility() != View.GONE) {
574            mMultiSelectPanel.setVisibility(View.GONE);
575            mMultiSelectPanel.startAnimation(
576                        AnimationUtils.loadAnimation(this, R.anim.footer_disappear));
577        }
578    }
579
580    /**
581     * Async task for finding a single mailbox by type (possibly even going to the network).
582     *
583     * This is much too complex, as implemented.  It uses this AsyncTask to check for a mailbox,
584     * then (if not found) a Controller call to refresh mailboxes from the server, and a handler
585     * to relaunch this task (a 2nd time) to read the results of the network refresh.  The core
586     * problem is that we have two different non-UI-thread jobs (reading DB and reading network)
587     * and two different paradigms for dealing with them.  Some unification would be needed here
588     * to make this cleaner.
589     *
590     * TODO: If this problem spreads to other operations, find a cleaner way to handle it.
591     */
592    private class FindMailboxTask extends AsyncTask<Void, Void, Long> {
593
594        private long mAccountId;
595        private int mMailboxType;
596        private boolean mOkToRecurse;
597
598        /**
599         * Special constructor to cache some local info
600         */
601        public FindMailboxTask(long accountId, int mailboxType, boolean okToRecurse) {
602            mAccountId = accountId;
603            mMailboxType = mailboxType;
604            mOkToRecurse = okToRecurse;
605        }
606
607        @Override
608        protected Long doInBackground(Void... params) {
609            // See if we can find the requested mailbox in the DB.
610            long mailboxId = Mailbox.findMailboxOfType(MessageList.this, mAccountId, mMailboxType);
611            if (mailboxId == -1 && mOkToRecurse) {
612                // Not found - launch network lookup
613                EmailContent.Account account =
614                    EmailContent.Account.restoreAccountWithId(MessageList.this, mAccountId);
615                mControllerCallback.mWaitForMailboxType = mMailboxType;
616                mHandler.progress(true);
617                Controller.getInstance(getApplication()).updateMailboxList(
618                        account, mControllerCallback);
619            }
620            return mailboxId;
621        }
622
623        @Override
624        protected void onPostExecute(Long mailboxId) {
625            if (mailboxId != -1) {
626                mMailboxId = mailboxId;
627                mLoadMessagesTask = new LoadMessagesTask(mMailboxId);
628                mLoadMessagesTask.execute();
629            }
630        }
631    }
632
633    /**
634     * Async task for loading a single folder out of the UI thread
635     *
636     * The code here (for merged boxes) is a placeholder/hack and should be replaced.  Some
637     * specific notes:
638     * TODO:  Move the double query into a specialized URI that returns all inbox messages
639     * and do the dirty work in raw SQL in the provider.
640     * TODO:  Generalize the query generation so we can reuse it in MessageView (for next/prev)
641     */
642    private class LoadMessagesTask extends AsyncTask<Void, Void, Cursor> {
643
644        private long mMailboxKey;
645
646        /**
647         * Special constructor to cache some local info
648         */
649        public LoadMessagesTask(long mailboxKey) {
650            mMailboxKey = mailboxKey;
651        }
652
653        @Override
654        protected Cursor doInBackground(Void... params) {
655            // Setup default selection & args, then add to it as necessary
656            StringBuilder selection = new StringBuilder(
657                    Message.FLAG_LOADED + "!=" + Message.NOT_LOADED + " AND ");
658            String[] selArgs = null;
659
660            if (mMailboxKey == QUERY_ALL_INBOXES || mMailboxKey == QUERY_ALL_DRAFTS ||
661                    mMailboxKey == QUERY_ALL_OUTBOX) {
662                // query for all mailboxes of type INBOX, DRAFTS, or OUTBOX
663                int type;
664                if (mMailboxKey == QUERY_ALL_INBOXES) {
665                    type = Mailbox.TYPE_INBOX;
666                } else if (mMailboxKey == QUERY_ALL_DRAFTS) {
667                    type = Mailbox.TYPE_DRAFTS;
668                } else {
669                    type = Mailbox.TYPE_OUTBOX;
670                }
671                StringBuilder inboxes = new StringBuilder();
672                Cursor c = MessageList.this.getContentResolver().query(
673                        Mailbox.CONTENT_URI,
674                        MAILBOX_FIND_INBOX_PROJECTION,
675                        MailboxColumns.TYPE + "=? AND " + MailboxColumns.FLAG_VISIBLE + "=1",
676                        new String[] { Integer.toString(type) }, null);
677                // build a long WHERE list
678                // TODO do this directly in the provider
679                while (c.moveToNext()) {
680                    if (inboxes.length() != 0) {
681                        inboxes.append(" OR ");
682                    }
683                    inboxes.append(MessageColumns.MAILBOX_KEY + "=");
684                    inboxes.append(c.getLong(MAILBOX_FIND_INBOX_COLUMN_ID));
685                }
686                c.close();
687                // This is a hack - if there were no matching mailboxes, the empty selection string
688                // would match *all* messages.  Instead, force a "non-matching" selection, which
689                // generates an empty Message cursor.
690                // TODO: handle this properly when we move the compound lookup into the provider
691                if (inboxes.length() == 0) {
692                    inboxes.append(Message.RECORD_ID + "=-1");
693                }
694                // make that the selection
695                selection.append(inboxes);
696            } else  if (mMailboxKey == QUERY_ALL_UNREAD) {
697                selection.append(Message.FLAG_READ + "=0");
698            } else if (mMailboxKey == QUERY_ALL_FAVORITES) {
699                selection.append(Message.FLAG_FAVORITE + "=1");
700            } else {
701                selection.append(MessageColumns.MAILBOX_KEY + "=?");
702                selArgs = new String[] { String.valueOf(mMailboxKey) };
703            }
704            return MessageList.this.managedQuery(
705                    EmailContent.Message.CONTENT_URI,
706                    MessageList.this.mListAdapter.PROJECTION,
707                    selection.toString(), selArgs,
708                    EmailContent.MessageColumns.TIMESTAMP + " DESC");
709        }
710
711        @Override
712        protected void onPostExecute(Cursor cursor) {
713            MessageList.this.mListAdapter.changeCursor(cursor);
714
715            // TODO: remove this hack and only update at the right time
716            if (cursor != null && cursor.getCount() == 0) {
717                onRefresh();
718            }
719        }
720    }
721
722    /**
723     * Handler for UI-thread operations (when called from callbacks or any other threads)
724     */
725    class MessageListHandler extends Handler {
726        private static final int MSG_PROGRESS = 1;
727        private static final int MSG_LOOKUP_MAILBOX_TYPE = 2;
728
729        @Override
730        public void handleMessage(android.os.Message msg) {
731            switch (msg.what) {
732                case MSG_PROGRESS:
733                    setProgressBarIndeterminateVisibility(msg.arg1 != 0);
734                    break;
735                case MSG_LOOKUP_MAILBOX_TYPE:
736                    // kill running async task, if any
737                    if (mFindMailboxTask != null &&
738                            mFindMailboxTask.getStatus() != FindMailboxTask.Status.FINISHED) {
739                        mFindMailboxTask.cancel(true);
740                        mFindMailboxTask = null;
741                    }
742                    // start new one.  do not recurse back to controller.
743                    long accountId = ((Long)msg.obj).longValue();
744                    int mailboxType = msg.arg1;
745                    mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false);
746                    mFindMailboxTask.execute();
747                    break;
748                default:
749                    super.handleMessage(msg);
750            }
751        }
752
753        /**
754         * Call from any thread to start/stop progress indicator(s)
755         * @param progress true to start, false to stop
756         */
757        public void progress(boolean progress) {
758            android.os.Message msg = android.os.Message.obtain();
759            msg.what = MSG_PROGRESS;
760            msg.arg1 = progress ? 1 : 0;
761            sendMessage(msg);
762        }
763
764        /**
765         * Called from any thread to look for a mailbox of a specific type.  This is designed
766         * to be called from the Controller's MailboxList callback;  It instructs the async task
767         * not to recurse, in case the mailbox is not found after this.
768         *
769         * See FindMailboxTask for more notes on this handler.
770         */
771        public void lookupMailboxType(long accountId, int mailboxType) {
772            android.os.Message msg = android.os.Message.obtain();
773            msg.what = MSG_LOOKUP_MAILBOX_TYPE;
774            msg.arg1 = mailboxType;
775            msg.obj = Long.valueOf(accountId);
776            sendMessage(msg);
777        }
778    }
779
780    /**
781     * Callback for async Controller results.  This is all a placeholder until we figure out the
782     * final way to do this.
783     */
784    private class ControllerResults implements Controller.Result {
785
786        // These are preset for use by updateMailboxListCallback
787        int mWaitForMailboxType = -1;
788
789        public void updateMailboxListCallback(MessagingException result, long accountKey) {
790            if (mWaitForMailboxType != -1) {
791                mHandler.progress(false);
792                if (result == null) {
793                    mHandler.lookupMailboxType(accountKey, mWaitForMailboxType);
794                }
795            }
796        }
797
798        public void updateMailboxCallback(MessagingException result, long accountKey,
799                long mailboxKey, int totalMessagesInMailbox, int numNewMessages) {
800            mHandler.progress(false);
801        }
802
803        public void loadAttachmentCallback(MessagingException result, long messageId,
804                long attachmentId, int progress) {
805        }
806    }
807
808    /**
809     * This class implements the adapter for displaying messages based on cursors.
810     */
811    /* package */ class MessageListAdapter extends CursorAdapter {
812
813        public static final int COLUMN_ID = 0;
814        public static final int COLUMN_MAILBOX_KEY = 1;
815        public static final int COLUMN_ACCOUNT_KEY = 2;
816        public static final int COLUMN_DISPLAY_NAME = 3;
817        public static final int COLUMN_SUBJECT = 4;
818        public static final int COLUMN_DATE = 5;
819        public static final int COLUMN_READ = 6;
820        public static final int COLUMN_FAVORITE = 7;
821        public static final int COLUMN_ATTACHMENTS = 8;
822
823        public final String[] PROJECTION = new String[] {
824            EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
825            MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP,
826            MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT,
827        };
828
829        Context mContext;
830        private LayoutInflater mInflater;
831        private Drawable mAttachmentIcon;
832        private Drawable mFavoriteIconOn;
833        private Drawable mFavoriteIconOff;
834        private Drawable mSelectedIconOn;
835        private Drawable mSelectedIconOff;
836
837        private java.text.DateFormat mDateFormat;
838        private java.text.DateFormat mDayFormat;
839        private java.text.DateFormat mTimeFormat;
840
841        private HashSet<Long> mChecked = new HashSet<Long>();
842
843        public MessageListAdapter(Context context) {
844            super(context, null);
845            mContext = context;
846            mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
847
848            Resources resources = context.getResources();
849            mAttachmentIcon = resources.getDrawable(R.drawable.ic_mms_attachment_small);
850            mFavoriteIconOn = resources.getDrawable(android.R.drawable.star_on);
851            mFavoriteIconOff = resources.getDrawable(android.R.drawable.star_off);
852            mSelectedIconOn = resources.getDrawable(R.drawable.btn_check_buttonless_on);
853            mSelectedIconOff = resources.getDrawable(R.drawable.btn_check_buttonless_off);
854
855            mDateFormat = android.text.format.DateFormat.getDateFormat(context);    // short date
856            mDayFormat = android.text.format.DateFormat.getDateFormat(context);     // TODO: day
857            mTimeFormat = android.text.format.DateFormat.getTimeFormat(context);    // 12/24 time
858        }
859
860        public Set<Long> getSelectedSet() {
861            return mChecked;
862        }
863
864        @Override
865        public void bindView(View view, Context context, Cursor cursor) {
866            // Reset the view (in case it was recycled) and prepare for binding
867            MessageListItem itemView = (MessageListItem) view;
868            itemView.bindViewInit(this, true);
869
870            // Load the public fields in the view (for later use)
871            itemView.mMessageId = cursor.getLong(COLUMN_ID);
872            itemView.mMailboxId = cursor.getLong(COLUMN_MAILBOX_KEY);
873            itemView.mAccountId = cursor.getLong(COLUMN_ACCOUNT_KEY);
874            itemView.mRead = cursor.getInt(COLUMN_READ) != 0;
875            itemView.mFavorite = cursor.getInt(COLUMN_FAVORITE) != 0;
876            itemView.mSelected = mChecked.contains(Long.valueOf(itemView.mMessageId));
877
878            // Load the UI
879            View chipView = view.findViewById(R.id.chip);
880            int chipResId = mColorChipResIds[(int)itemView.mAccountId % mColorChipResIds.length];
881            chipView.setBackgroundResource(chipResId);
882            // TODO always display chip.  Use other indications (e.g. boldface) for read/unread
883            chipView.getBackground().setAlpha(itemView.mRead ? 100 : 255);
884
885            TextView fromView = (TextView) view.findViewById(R.id.from);
886            String text = cursor.getString(COLUMN_DISPLAY_NAME);
887            if (text != null) fromView.setText(text);
888
889            boolean hasAttachments = cursor.getInt(COLUMN_ATTACHMENTS) != 0;
890            fromView.setCompoundDrawablesWithIntrinsicBounds(null, null,
891                    hasAttachments ? mAttachmentIcon : null, null);
892
893            TextView subjectView = (TextView) view.findViewById(R.id.subject);
894            text = cursor.getString(COLUMN_SUBJECT);
895            if (text != null) subjectView.setText(text);
896
897            // TODO ui spec suggests "time", "day", "date" - implement "day"
898            TextView dateView = (TextView) view.findViewById(R.id.date);
899            long timestamp = cursor.getLong(COLUMN_DATE);
900            Date date = new Date(timestamp);
901            if (Utility.isDateToday(date)) {
902                text = mTimeFormat.format(date);
903            } else {
904                text = mDateFormat.format(date);
905            }
906            dateView.setText(text);
907
908            ImageView selectedView = (ImageView) view.findViewById(R.id.selected);
909            selectedView.setImageDrawable(itemView.mSelected ? mSelectedIconOn : mSelectedIconOff);
910
911            ImageView favoriteView = (ImageView) view.findViewById(R.id.favorite);
912            favoriteView.setImageDrawable(itemView.mFavorite ? mFavoriteIconOn : mFavoriteIconOff);
913        }
914
915        @Override
916        public View newView(Context context, Cursor cursor, ViewGroup parent) {
917            return mInflater.inflate(R.layout.message_list_item, parent, false);
918        }
919
920        /**
921         * This is used as a callback from the list items, to set the selected state
922         *
923         * @param itemView the item being changed
924         * @param newSelected the new value of the selected flag (checkbox state)
925         */
926        public void updateSelected(MessageListItem itemView, boolean newSelected) {
927            ImageView selectedView = (ImageView) itemView.findViewById(R.id.selected);
928            selectedView.setImageDrawable(newSelected ? mSelectedIconOn : mSelectedIconOff);
929
930            // Set checkbox state in list, and show/hide panel if necessary
931            Long id = Long.valueOf(itemView.mMessageId);
932            if (newSelected) {
933                mChecked.add(id);
934            } else {
935                mChecked.remove(id);
936            }
937
938            MessageList.this.showMultiPanel(mChecked.size() > 0);
939        }
940
941        /**
942         * This is used as a callback from the list items, to set the favorite state
943         *
944         * @param itemView the item being changed
945         * @param newFavorite the new value of the favorite flag (star state)
946         */
947        public void updateFavorite(MessageListItem itemView, boolean newFavorite) {
948            ImageView favoriteView = (ImageView) itemView.findViewById(R.id.favorite);
949            favoriteView.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff);
950            onSetMessageFavorite(itemView.mMessageId, newFavorite);
951        }
952    }
953}
954