MessageList.java revision 8f7f93a7b36d873d5adba65f4da54819880c0285
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            Controller.getInstance(getApplication()).updateMailbox(
386                    mailbox.mAccountKey, mailbox, mControllerCallback);
387        }
388    }
389
390    private void onAccounts() {
391        AccountFolderList.actionShowAccounts(this);
392        finish();
393    }
394
395    private void onCompose() {
396        // TODO: Select correct account to send from when there are multiple mailboxes
397        // TODO: Should not be reading from DB in UI thread
398        if (mMailboxId >= 0) {
399            EmailContent.Mailbox mailbox =
400                    EmailContent.Mailbox.restoreMailboxWithId(this, mMailboxId);
401            MessageCompose.actionCompose(this, mailbox.mAccountKey);
402        }
403    }
404
405    private void onEditAccount() {
406        // TODO: Select correct account to edit when there are multiple mailboxes
407        // TODO: Should not be reading from DB in UI thread
408        if (mMailboxId >= 0) {
409            EmailContent.Mailbox mailbox =
410                    EmailContent.Mailbox.restoreMailboxWithId(this, mMailboxId);
411            AccountSettings.actionSettings(this, mailbox.mAccountKey);
412        }
413    }
414
415    public void onOpenMessage(long messageId, long mailboxId) {
416        // TODO: Should not be reading from DB in UI thread
417        EmailContent.Mailbox mailbox = EmailContent.Mailbox.restoreMailboxWithId(this, mailboxId);
418
419        if (mailbox.mType == EmailContent.Mailbox.TYPE_DRAFTS) {
420            MessageCompose.actionEditDraft(this, messageId);
421        } else {
422            MessageView.actionView(this, messageId);
423        }
424    }
425
426    private void onDelete(long messageId, long accountId) {
427        Controller.getInstance(getApplication()).deleteMessage(messageId, accountId);
428        Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show();
429    }
430
431    private void onSetMessageRead(long messageId, boolean newRead) {
432        Controller.getInstance(getApplication()).setMessageRead(messageId, newRead);
433    }
434
435    private void onSetMessageFavorite(long messageId, boolean newFavorite) {
436        Controller.getInstance(getApplication()).setMessageFavorite(messageId, newFavorite);
437    }
438
439    /**
440     * Toggles a set read/unread states.  Note, the default behavior is "mark unread", so the
441     * sense of the helper methods is "true=unread".
442     *
443     * @param selectedSet The current list of selected items
444     */
445    private void onMultiToggleRead(Set<Long> selectedSet) {
446        int numChanged = toggleMultiple(selectedSet, new MultiToggleHelper() {
447
448            public boolean getField(long messageId, Cursor c) {
449                return c.getInt(MessageListAdapter.COLUMN_READ) == 0;
450            }
451
452            public boolean setField(long messageId, Cursor c, boolean newValue) {
453                boolean oldValue = getField(messageId, c);
454                if (oldValue != newValue) {
455                    onSetMessageRead(messageId, !newValue);
456                    return true;
457                }
458                return false;
459            }
460        });
461    }
462
463    /**
464     * Toggles a set of favorites (stars)
465     *
466     * @param selectedSet The current list of selected items
467     */
468    private void onMultiToggleFavorite(Set<Long> selectedSet) {
469        int numChanged = toggleMultiple(selectedSet, new MultiToggleHelper() {
470
471            public boolean getField(long messageId, Cursor c) {
472                return c.getInt(MessageListAdapter.COLUMN_FAVORITE) != 0;
473            }
474
475            public boolean setField(long messageId, Cursor c, boolean newValue) {
476                boolean oldValue = getField(messageId, c);
477                if (oldValue != newValue) {
478                    onSetMessageFavorite(messageId, newValue);
479                    return true;
480                }
481                return false;
482            }
483        });
484    }
485
486    private void onMultiDelete(Set<Long> selectedSet) {
487        // Clone the set, because deleting is going to thrash things
488        HashSet<Long> cloneSet = new HashSet<Long>(selectedSet);
489        for (Long id : cloneSet) {
490            Controller.getInstance(getApplication()).deleteMessage(id, -1);
491        }
492        // TODO: count messages and show "n messages deleted"
493        Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show();
494        selectedSet.clear();
495        showMultiPanel(false);
496    }
497
498    private interface MultiToggleHelper {
499        /**
500         * Return true if the field of interest is "set".  If one or more are false, then our
501         * bulk action will be to "set".  If all are set, our bulk action will be to "clear".
502         * @param messageId the message id of the current message
503         * @param c the cursor, positioned to the item of interest
504         * @return true if the field at this row is "set"
505         */
506        public boolean getField(long messageId, Cursor c);
507
508        /**
509         * Set or clear the field of interest.  Return true if a change was made.
510         * @param messageId the message id of the current message
511         * @param c the cursor, positioned to the item of interest
512         * @param newValue the new value to be set at this row
513         * @return true if a change was actually made
514         */
515        public boolean setField(long messageId, Cursor c, boolean newValue);
516    }
517
518    /**
519     * Toggle multiple fields in a message, using the following logic:  If one or more fields
520     * are "clear", then "set" them.  If all fields are "set", then "clear" them all.
521     *
522     * @param selectedSet the set of messages that are selected
523     * @param helper functions to implement the specific getter & setter
524     * @return the number of messages that were updated
525     */
526    private int toggleMultiple(Set<Long> selectedSet, MultiToggleHelper helper) {
527        Cursor c = mListAdapter.getCursor();
528        boolean anyWereFound = false;
529        boolean allWereSet = true;
530
531        c.moveToPosition(-1);
532        while (c.moveToNext()) {
533            long id = c.getInt(MessageListAdapter.COLUMN_ID);
534            if (selectedSet.contains(Long.valueOf(id))) {
535                anyWereFound = true;
536                if (!helper.getField(id, c)) {
537                    allWereSet = false;
538                    break;
539                }
540            }
541        }
542
543        int numChanged = 0;
544
545        if (anyWereFound) {
546            boolean newValue = !allWereSet;
547            c.moveToPosition(-1);
548            while (c.moveToNext()) {
549                long id = c.getInt(MessageListAdapter.COLUMN_ID);
550                if (selectedSet.contains(Long.valueOf(id))) {
551                    if (helper.setField(id, c, newValue)) {
552                        ++numChanged;
553                    }
554                }
555            }
556        }
557
558        return numChanged;
559    }
560
561    /**
562     * Show or hide the panel of multi-select options
563     */
564    private void showMultiPanel(boolean show) {
565        if (show && mMultiSelectPanel.getVisibility() != View.VISIBLE) {
566            mMultiSelectPanel.setVisibility(View.VISIBLE);
567            mMultiSelectPanel.startAnimation(
568                    AnimationUtils.loadAnimation(this, R.anim.footer_appear));
569
570        } else if (!show && mMultiSelectPanel.getVisibility() != View.GONE) {
571            mMultiSelectPanel.setVisibility(View.GONE);
572            mMultiSelectPanel.startAnimation(
573                        AnimationUtils.loadAnimation(this, R.anim.footer_disappear));
574        }
575    }
576
577    /**
578     * Async task for finding a single mailbox by type (possibly even going to the network).
579     *
580     * This is much too complex, as implemented.  It uses this AsyncTask to check for a mailbox,
581     * then (if not found) a Controller call to refresh mailboxes from the server, and a handler
582     * to relaunch this task (a 2nd time) to read the results of the network refresh.  The core
583     * problem is that we have two different non-UI-thread jobs (reading DB and reading network)
584     * and two different paradigms for dealing with them.  Some unification would be needed here
585     * to make this cleaner.
586     *
587     * TODO: If this problem spreads to other operations, find a cleaner way to handle it.
588     */
589    private class FindMailboxTask extends AsyncTask<Void, Void, Long> {
590
591        private long mAccountId;
592        private int mMailboxType;
593        private boolean mOkToRecurse;
594
595        /**
596         * Special constructor to cache some local info
597         */
598        public FindMailboxTask(long accountId, int mailboxType, boolean okToRecurse) {
599            mAccountId = accountId;
600            mMailboxType = mailboxType;
601            mOkToRecurse = okToRecurse;
602        }
603
604        @Override
605        protected Long doInBackground(Void... params) {
606            // See if we can find the requested mailbox in the DB.
607            long mailboxId = Mailbox.findMailboxOfType(MessageList.this, mAccountId, mMailboxType);
608            if (mailboxId == -1 && mOkToRecurse) {
609                // Not found - launch network lookup
610                mControllerCallback.mWaitForMailboxType = mMailboxType;
611                Controller.getInstance(getApplication()).updateMailboxList(
612                        mAccountId, mControllerCallback);
613            }
614            return mailboxId;
615        }
616
617        @Override
618        protected void onPostExecute(Long mailboxId) {
619            if (mailboxId != -1) {
620                mMailboxId = mailboxId;
621                mLoadMessagesTask = new LoadMessagesTask(mMailboxId);
622                mLoadMessagesTask.execute();
623            }
624        }
625    }
626
627    /**
628     * Async task for loading a single folder out of the UI thread
629     *
630     * The code here (for merged boxes) is a placeholder/hack and should be replaced.  Some
631     * specific notes:
632     * TODO:  Move the double query into a specialized URI that returns all inbox messages
633     * and do the dirty work in raw SQL in the provider.
634     * TODO:  Generalize the query generation so we can reuse it in MessageView (for next/prev)
635     */
636    private class LoadMessagesTask extends AsyncTask<Void, Void, Cursor> {
637
638        private long mMailboxKey;
639
640        /**
641         * Special constructor to cache some local info
642         */
643        public LoadMessagesTask(long mailboxKey) {
644            mMailboxKey = mailboxKey;
645        }
646
647        @Override
648        protected Cursor doInBackground(Void... params) {
649            // Setup default selection & args, then add to it as necessary
650            StringBuilder selection = new StringBuilder(
651                    Message.FLAG_LOADED + "!=" + Message.NOT_LOADED + " AND ");
652            String[] selArgs = null;
653
654            if (mMailboxKey == QUERY_ALL_INBOXES || mMailboxKey == QUERY_ALL_DRAFTS ||
655                    mMailboxKey == QUERY_ALL_OUTBOX) {
656                // query for all mailboxes of type INBOX, DRAFTS, or OUTBOX
657                int type;
658                if (mMailboxKey == QUERY_ALL_INBOXES) {
659                    type = Mailbox.TYPE_INBOX;
660                } else if (mMailboxKey == QUERY_ALL_DRAFTS) {
661                    type = Mailbox.TYPE_DRAFTS;
662                } else {
663                    type = Mailbox.TYPE_OUTBOX;
664                }
665                StringBuilder inboxes = new StringBuilder();
666                Cursor c = MessageList.this.getContentResolver().query(
667                        Mailbox.CONTENT_URI,
668                        MAILBOX_FIND_INBOX_PROJECTION,
669                        MailboxColumns.TYPE + "=? AND " + MailboxColumns.FLAG_VISIBLE + "=1",
670                        new String[] { Integer.toString(type) }, null);
671                // build a long WHERE list
672                // TODO do this directly in the provider
673                while (c.moveToNext()) {
674                    if (inboxes.length() != 0) {
675                        inboxes.append(" OR ");
676                    }
677                    inboxes.append(MessageColumns.MAILBOX_KEY + "=");
678                    inboxes.append(c.getLong(MAILBOX_FIND_INBOX_COLUMN_ID));
679                }
680                c.close();
681                // This is a hack - if there were no matching mailboxes, the empty selection string
682                // would match *all* messages.  Instead, force a "non-matching" selection, which
683                // generates an empty Message cursor.
684                // TODO: handle this properly when we move the compound lookup into the provider
685                if (inboxes.length() == 0) {
686                    inboxes.append(Message.RECORD_ID + "=-1");
687                }
688                // make that the selection
689                selection.append(inboxes);
690            } else  if (mMailboxKey == QUERY_ALL_UNREAD) {
691                selection.append(Message.FLAG_READ + "=0");
692            } else if (mMailboxKey == QUERY_ALL_FAVORITES) {
693                selection.append(Message.FLAG_FAVORITE + "=1");
694            } else {
695                selection.append(MessageColumns.MAILBOX_KEY + "=?");
696                selArgs = new String[] { String.valueOf(mMailboxKey) };
697            }
698            return MessageList.this.managedQuery(
699                    EmailContent.Message.CONTENT_URI,
700                    MessageList.this.mListAdapter.PROJECTION,
701                    selection.toString(), selArgs,
702                    EmailContent.MessageColumns.TIMESTAMP + " DESC");
703        }
704
705        @Override
706        protected void onPostExecute(Cursor cursor) {
707            MessageList.this.mListAdapter.changeCursor(cursor);
708
709            // TODO: remove this hack and only update at the right time
710            if (cursor != null && cursor.getCount() == 0) {
711                onRefresh();
712            }
713        }
714    }
715
716    /**
717     * Handler for UI-thread operations (when called from callbacks or any other threads)
718     */
719    class MessageListHandler extends Handler {
720        private static final int MSG_PROGRESS = 1;
721        private static final int MSG_LOOKUP_MAILBOX_TYPE = 2;
722
723        @Override
724        public void handleMessage(android.os.Message msg) {
725            switch (msg.what) {
726                case MSG_PROGRESS:
727                    setProgressBarIndeterminateVisibility(msg.arg1 != 0);
728                    break;
729                case MSG_LOOKUP_MAILBOX_TYPE:
730                    // kill running async task, if any
731                    if (mFindMailboxTask != null &&
732                            mFindMailboxTask.getStatus() != FindMailboxTask.Status.FINISHED) {
733                        mFindMailboxTask.cancel(true);
734                        mFindMailboxTask = null;
735                    }
736                    // start new one.  do not recurse back to controller.
737                    long accountId = ((Long)msg.obj).longValue();
738                    int mailboxType = msg.arg1;
739                    mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false);
740                    mFindMailboxTask.execute();
741                    break;
742                default:
743                    super.handleMessage(msg);
744            }
745        }
746
747        /**
748         * Call from any thread to start/stop progress indicator(s)
749         * @param progress true to start, false to stop
750         */
751        public void progress(boolean progress) {
752            android.os.Message msg = android.os.Message.obtain();
753            msg.what = MSG_PROGRESS;
754            msg.arg1 = progress ? 1 : 0;
755            sendMessage(msg);
756        }
757
758        /**
759         * Called from any thread to look for a mailbox of a specific type.  This is designed
760         * to be called from the Controller's MailboxList callback;  It instructs the async task
761         * not to recurse, in case the mailbox is not found after this.
762         *
763         * See FindMailboxTask for more notes on this handler.
764         */
765        public void lookupMailboxType(long accountId, int mailboxType) {
766            android.os.Message msg = android.os.Message.obtain();
767            msg.what = MSG_LOOKUP_MAILBOX_TYPE;
768            msg.arg1 = mailboxType;
769            msg.obj = Long.valueOf(accountId);
770            sendMessage(msg);
771        }
772    }
773
774    /**
775     * Callback for async Controller results.
776     */
777    private class ControllerResults implements Controller.Result {
778
779        // These are preset for use by updateMailboxListCallback
780        int mWaitForMailboxType = -1;
781
782        // TODO report errors into UI
783        // TODO check accountKey and only react to relevant notifications
784        public void updateMailboxListCallback(MessagingException result, long accountKey,
785                int progress) {
786            if (progress == 0) {
787                mHandler.progress(true);
788            }
789            else if (result != null || progress == 100) {
790                mHandler.progress(false);
791                if (mWaitForMailboxType != -1) {
792                    if (result == null) {
793                        mHandler.lookupMailboxType(accountKey, mWaitForMailboxType);
794                    }
795                }
796            }
797        }
798
799        // TODO report errors into UI
800        // TODO check accountKey and only react to relevant notifications
801        public void updateMailboxCallback(MessagingException result, long accountKey,
802                long mailboxKey, int progress, int totalMessagesInMailbox, int numNewMessages) {
803            if (progress == 0) {
804                mHandler.progress(true);
805            }
806            else if (result != null || progress == 100) {
807                mHandler.progress(false);
808            }
809        }
810
811        public void loadAttachmentCallback(MessagingException result, long messageId,
812                long attachmentId, int progress) {
813        }
814    }
815
816    /**
817     * This class implements the adapter for displaying messages based on cursors.
818     */
819    /* package */ class MessageListAdapter extends CursorAdapter {
820
821        public static final int COLUMN_ID = 0;
822        public static final int COLUMN_MAILBOX_KEY = 1;
823        public static final int COLUMN_ACCOUNT_KEY = 2;
824        public static final int COLUMN_DISPLAY_NAME = 3;
825        public static final int COLUMN_SUBJECT = 4;
826        public static final int COLUMN_DATE = 5;
827        public static final int COLUMN_READ = 6;
828        public static final int COLUMN_FAVORITE = 7;
829        public static final int COLUMN_ATTACHMENTS = 8;
830
831        public final String[] PROJECTION = new String[] {
832            EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
833            MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP,
834            MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT,
835        };
836
837        Context mContext;
838        private LayoutInflater mInflater;
839        private Drawable mAttachmentIcon;
840        private Drawable mFavoriteIconOn;
841        private Drawable mFavoriteIconOff;
842        private Drawable mSelectedIconOn;
843        private Drawable mSelectedIconOff;
844
845        private java.text.DateFormat mDateFormat;
846        private java.text.DateFormat mDayFormat;
847        private java.text.DateFormat mTimeFormat;
848
849        private HashSet<Long> mChecked = new HashSet<Long>();
850
851        public MessageListAdapter(Context context) {
852            super(context, null);
853            mContext = context;
854            mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
855
856            Resources resources = context.getResources();
857            mAttachmentIcon = resources.getDrawable(R.drawable.ic_mms_attachment_small);
858            mFavoriteIconOn = resources.getDrawable(android.R.drawable.star_on);
859            mFavoriteIconOff = resources.getDrawable(android.R.drawable.star_off);
860            mSelectedIconOn = resources.getDrawable(R.drawable.btn_check_buttonless_on);
861            mSelectedIconOff = resources.getDrawable(R.drawable.btn_check_buttonless_off);
862
863            mDateFormat = android.text.format.DateFormat.getDateFormat(context);    // short date
864            mDayFormat = android.text.format.DateFormat.getDateFormat(context);     // TODO: day
865            mTimeFormat = android.text.format.DateFormat.getTimeFormat(context);    // 12/24 time
866        }
867
868        public Set<Long> getSelectedSet() {
869            return mChecked;
870        }
871
872        @Override
873        public void bindView(View view, Context context, Cursor cursor) {
874            // Reset the view (in case it was recycled) and prepare for binding
875            MessageListItem itemView = (MessageListItem) view;
876            itemView.bindViewInit(this, true);
877
878            // Load the public fields in the view (for later use)
879            itemView.mMessageId = cursor.getLong(COLUMN_ID);
880            itemView.mMailboxId = cursor.getLong(COLUMN_MAILBOX_KEY);
881            itemView.mAccountId = cursor.getLong(COLUMN_ACCOUNT_KEY);
882            itemView.mRead = cursor.getInt(COLUMN_READ) != 0;
883            itemView.mFavorite = cursor.getInt(COLUMN_FAVORITE) != 0;
884            itemView.mSelected = mChecked.contains(Long.valueOf(itemView.mMessageId));
885
886            // Load the UI
887            View chipView = view.findViewById(R.id.chip);
888            int chipResId = mColorChipResIds[(int)itemView.mAccountId % mColorChipResIds.length];
889            chipView.setBackgroundResource(chipResId);
890            // TODO always display chip.  Use other indications (e.g. boldface) for read/unread
891            chipView.getBackground().setAlpha(itemView.mRead ? 100 : 255);
892
893            TextView fromView = (TextView) view.findViewById(R.id.from);
894            String text = cursor.getString(COLUMN_DISPLAY_NAME);
895            if (text != null) fromView.setText(text);
896
897            boolean hasAttachments = cursor.getInt(COLUMN_ATTACHMENTS) != 0;
898            fromView.setCompoundDrawablesWithIntrinsicBounds(null, null,
899                    hasAttachments ? mAttachmentIcon : null, null);
900
901            TextView subjectView = (TextView) view.findViewById(R.id.subject);
902            text = cursor.getString(COLUMN_SUBJECT);
903            if (text != null) subjectView.setText(text);
904
905            // TODO ui spec suggests "time", "day", "date" - implement "day"
906            TextView dateView = (TextView) view.findViewById(R.id.date);
907            long timestamp = cursor.getLong(COLUMN_DATE);
908            Date date = new Date(timestamp);
909            if (Utility.isDateToday(date)) {
910                text = mTimeFormat.format(date);
911            } else {
912                text = mDateFormat.format(date);
913            }
914            dateView.setText(text);
915
916            ImageView selectedView = (ImageView) view.findViewById(R.id.selected);
917            selectedView.setImageDrawable(itemView.mSelected ? mSelectedIconOn : mSelectedIconOff);
918
919            ImageView favoriteView = (ImageView) view.findViewById(R.id.favorite);
920            favoriteView.setImageDrawable(itemView.mFavorite ? mFavoriteIconOn : mFavoriteIconOff);
921        }
922
923        @Override
924        public View newView(Context context, Cursor cursor, ViewGroup parent) {
925            return mInflater.inflate(R.layout.message_list_item, parent, false);
926        }
927
928        /**
929         * This is used as a callback from the list items, to set the selected state
930         *
931         * @param itemView the item being changed
932         * @param newSelected the new value of the selected flag (checkbox state)
933         */
934        public void updateSelected(MessageListItem itemView, boolean newSelected) {
935            ImageView selectedView = (ImageView) itemView.findViewById(R.id.selected);
936            selectedView.setImageDrawable(newSelected ? mSelectedIconOn : mSelectedIconOff);
937
938            // Set checkbox state in list, and show/hide panel if necessary
939            Long id = Long.valueOf(itemView.mMessageId);
940            if (newSelected) {
941                mChecked.add(id);
942            } else {
943                mChecked.remove(id);
944            }
945
946            MessageList.this.showMultiPanel(mChecked.size() > 0);
947        }
948
949        /**
950         * This is used as a callback from the list items, to set the favorite state
951         *
952         * @param itemView the item being changed
953         * @param newFavorite the new value of the favorite flag (star state)
954         */
955        public void updateFavorite(MessageListItem itemView, boolean newFavorite) {
956            ImageView favoriteView = (ImageView) itemView.findViewById(R.id.favorite);
957            favoriteView.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff);
958            onSetMessageFavorite(itemView.mMessageId, newFavorite);
959        }
960    }
961}
962