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