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