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.AccountBackupRestore;
20import com.android.email.Controller;
21import com.android.email.Email;
22import com.android.email.R;
23import com.android.email.SecurityPolicy;
24import com.android.email.Utility;
25import com.android.email.activity.setup.AccountSettings;
26import com.android.email.activity.setup.AccountSetupBasics;
27import com.android.email.mail.MessagingException;
28import com.android.email.mail.Store;
29import com.android.email.provider.EmailContent;
30import com.android.email.provider.EmailContent.Account;
31import com.android.email.provider.EmailContent.Mailbox;
32import com.android.email.provider.EmailContent.MailboxColumns;
33import com.android.email.provider.EmailContent.Message;
34import com.android.email.provider.EmailContent.MessageColumns;
35import com.android.email.service.MailService;
36
37import android.app.AlertDialog;
38import android.app.Dialog;
39import android.app.ListActivity;
40import android.app.NotificationManager;
41import android.content.ContentUris;
42import android.content.Context;
43import android.content.DialogInterface;
44import android.content.Intent;
45import android.database.Cursor;
46import android.database.MatrixCursor;
47import android.database.MergeCursor;
48import android.database.MatrixCursor.RowBuilder;
49import android.net.Uri;
50import android.os.AsyncTask;
51import android.os.Bundle;
52import android.os.Handler;
53import android.view.ContextMenu;
54import android.view.KeyEvent;
55import android.view.LayoutInflater;
56import android.view.Menu;
57import android.view.MenuItem;
58import android.view.View;
59import android.view.ViewGroup;
60import android.view.Window;
61import android.view.ContextMenu.ContextMenuInfo;
62import android.widget.AdapterView;
63import android.widget.CursorAdapter;
64import android.widget.ImageView;
65import android.widget.ListAdapter;
66import android.widget.ListView;
67import android.widget.ProgressBar;
68import android.widget.TextView;
69import android.widget.Toast;
70import android.widget.AdapterView.OnItemClickListener;
71
72import java.util.ArrayList;
73
74public class AccountFolderList extends ListActivity implements OnItemClickListener {
75    private static final int DIALOG_REMOVE_ACCOUNT = 1;
76    /**
77     * Key codes used to open a debug settings screen.
78     */
79    private static int[] secretKeyCodes = {
80            KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_B, KeyEvent.KEYCODE_U,
81            KeyEvent.KEYCODE_G
82    };
83    private int mSecretKeyCodeIndex = 0;
84
85    private static final String ICICLE_SELECTED_ACCOUNT = "com.android.email.selectedAccount";
86    private EmailContent.Account mSelectedContextAccount;
87
88    private ListView mListView;
89    private ProgressBar mProgressIcon;
90
91    private AccountsAdapter mListAdapter;
92
93    private LoadAccountsTask mLoadAccountsTask;
94    private DeleteAccountTask mDeleteAccountTask;
95    private MessageListHandler mHandler;
96    private ControllerResults mControllerCallback;
97
98    /**
99     * Reduced mailbox projection used by AccountsAdapter
100     */
101    public final static int MAILBOX_COLUMN_ID = 0;
102    public final static int MAILBOX_DISPLAY_NAME = 1;
103    public final static int MAILBOX_ACCOUNT_KEY = 2;
104    public final static int MAILBOX_TYPE = 3;
105    public final static int MAILBOX_UNREAD_COUNT = 4;
106    public final static int MAILBOX_FLAG_VISIBLE = 5;
107    public final static int MAILBOX_FLAGS = 6;
108
109    public final static String[] MAILBOX_PROJECTION = new String[] {
110        EmailContent.RECORD_ID, MailboxColumns.DISPLAY_NAME,
111        MailboxColumns.ACCOUNT_KEY, MailboxColumns.TYPE,
112        MailboxColumns.UNREAD_COUNT,
113        MailboxColumns.FLAG_VISIBLE, MailboxColumns.FLAGS
114    };
115
116    private static final String FAVORITE_COUNT_SELECTION =
117        MessageColumns.FLAG_FAVORITE + "= 1";
118
119    private static final String MAILBOX_TYPE_SELECTION =
120        MailboxColumns.TYPE + " =?";
121
122    private static final String MAILBOX_ID_SELECTION =
123        MessageColumns.MAILBOX_KEY + " =?";
124
125    private static final String[] MAILBOX_SUM_OF_UNREAD_COUNT_PROJECTION = new String [] {
126        "sum(" + MailboxColumns.UNREAD_COUNT + ")"
127    };
128
129    private static final String MAILBOX_INBOX_SELECTION =
130        MailboxColumns.ACCOUNT_KEY + " =?" + " AND " + MailboxColumns.TYPE +" = "
131        + Mailbox.TYPE_INBOX;
132
133    private static final int MAILBOX_UNREAD_COUNT_COLUMN_UNREAD_COUNT = 0;
134    private static final String[] MAILBOX_UNREAD_COUNT_PROJECTION = new String [] {
135        MailboxColumns.UNREAD_COUNT
136    };
137
138    /**
139     * Start the Accounts list activity.  Uses the CLEAR_TOP flag which means that other stacked
140     * activities may be killed in order to get back to Accounts.
141     */
142    public static void actionShowAccounts(Context context) {
143        Intent i = new Intent(context, AccountFolderList.class);
144        i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
145        context.startActivity(i);
146    }
147
148    @Override
149    public void onCreate(Bundle icicle) {
150        super.onCreate(icicle);
151
152        requestWindowFeature(Window.FEATURE_CUSTOM_TITLE);
153        setContentView(R.layout.account_folder_list);
154        getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE,
155                R.layout.list_title);
156
157        mHandler = new MessageListHandler();
158        mControllerCallback = new ControllerResults();
159        mProgressIcon = (ProgressBar) findViewById(R.id.title_progress_icon);
160
161        mListView = getListView();
162        mListView.setItemsCanFocus(false);
163        mListView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_INSET);
164        mListView.setOnItemClickListener(this);
165        mListView.setLongClickable(true);
166        registerForContextMenu(mListView);
167
168        if (icicle != null && icicle.containsKey(ICICLE_SELECTED_ACCOUNT)) {
169            mSelectedContextAccount = (Account) icicle.getParcelable(ICICLE_SELECTED_ACCOUNT);
170        }
171
172        ((TextView) findViewById(R.id.title_left_text)).setText(R.string.app_name);
173    }
174
175    @Override
176    public void onSaveInstanceState(Bundle outState) {
177        super.onSaveInstanceState(outState);
178        if (mSelectedContextAccount != null) {
179            outState.putParcelable(ICICLE_SELECTED_ACCOUNT, mSelectedContextAccount);
180        }
181    }
182
183    @Override
184    public void onPause() {
185        super.onPause();
186        Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback);
187    }
188
189    @Override
190    public void onResume() {
191        super.onResume();
192
193        NotificationManager notifMgr = (NotificationManager)
194                getSystemService(Context.NOTIFICATION_SERVICE);
195        notifMgr.cancel(1);
196
197        Controller.getInstance(getApplication()).addResultCallback(mControllerCallback);
198
199        // Exit immediately if the accounts list has changed (e.g. externally deleted)
200        if (Email.getNotifyUiAccountsChanged()) {
201            Welcome.actionStart(this);
202            finish();
203            return;
204        }
205
206        updateAccounts();
207        // TODO: What updates do we need to auto-trigger, now that we have mailboxes in view?
208    }
209
210    @Override
211    protected void onDestroy() {
212        super.onDestroy();
213        Utility.cancelTaskInterrupt(mLoadAccountsTask);
214        mLoadAccountsTask = null;
215
216        // TODO: We shouldn't call cancel() for DeleteAccountTask.  If the task hasn't
217        // started, this will mark it as "don't run", but we always want it to finish.
218        // (But don't just remove this cancel() call.  DeleteAccountTask.onPostExecute() checks if
219        // it's been canceled to decided whether to update the UI.)
220        Utility.cancelTask(mDeleteAccountTask, false); // Don't interrupt if it's running.
221        mDeleteAccountTask = null;
222
223        if (mListAdapter != null) {
224            mListAdapter.changeCursor(null);
225        }
226    }
227
228    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
229        if (mListAdapter.isMailbox(position)) {
230            MessageList.actionHandleMailbox(this, id);
231        } else if (mListAdapter.isAccount(position)) {
232            MessageList.actionHandleAccount(this, id, Mailbox.TYPE_INBOX);
233        }
234    }
235
236    private static int getUnreadCountByMailboxType(Context context, int type) {
237        int count = 0;
238        Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI,
239                MAILBOX_SUM_OF_UNREAD_COUNT_PROJECTION,
240                MAILBOX_TYPE_SELECTION,
241                new String[] { String.valueOf(type) }, null);
242
243        try {
244            if (c.moveToFirst()) {
245                return c.getInt(0);
246            }
247        } finally {
248            c.close();
249        }
250        return count;
251    }
252
253    private static int getCountByMailboxType(Context context, int type) {
254        int count = 0;
255        Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI,
256                EmailContent.ID_PROJECTION, MAILBOX_TYPE_SELECTION,
257                new String[] { String.valueOf(type) }, null);
258
259        try {
260            c.moveToPosition(-1);
261            while (c.moveToNext()) {
262                count += EmailContent.count(context, Message.CONTENT_URI,
263                        MAILBOX_ID_SELECTION,
264                        new String[] {
265                            String.valueOf(c.getLong(EmailContent.ID_PROJECTION_COLUMN)) });
266            }
267        } finally {
268            c.close();
269        }
270        return count;
271    }
272
273    /**
274     * Build the group and child cursors that support the summary views (aka "at a glance").
275     *
276     * This is a placeholder implementation with significant problems that need to be addressed:
277     *
278     * TODO: We should only show summary mailboxes if they are non-empty.  So there needs to be
279     * a more dynamic child-cursor here, probably listening for update notifications on a number
280     * of other internally-held queries such as count-of-inbox, count-of-unread, etc.
281     *
282     * TODO: This simple list is incomplete.  For example, we probably want drafts, outbox, and
283     * (maybe) sent (again, these would be displayed only when non-empty).
284     *
285     * TODO: We need a way to count total unread in all inboxes (probably with some provider help)
286     *
287     * TODO: We need a way to count total # messages in all other summary boxes (probably with
288     * some provider help).
289     *
290     * TODO use narrower account projection (see LoadAccountsTask)
291     */
292    private MatrixCursor getSummaryChildCursor() {
293        MatrixCursor childCursor = new MatrixCursor(MAILBOX_PROJECTION);
294        int count;
295        RowBuilder row;
296        // TYPE_INBOX
297        count = getUnreadCountByMailboxType(this, Mailbox.TYPE_INBOX);
298        row = childCursor.newRow();
299        row.add(Long.valueOf(Mailbox.QUERY_ALL_INBOXES));   // MAILBOX_COLUMN_ID = 0;
300        row.add(getString(R.string.account_folder_list_summary_inbox)); // MAILBOX_DISPLAY_NAME
301        row.add(null);                                          // MAILBOX_ACCOUNT_KEY = 2;
302        row.add(Integer.valueOf(Mailbox.TYPE_INBOX));           // MAILBOX_TYPE = 3;
303        row.add(Integer.valueOf(count));                        // MAILBOX_UNREAD_COUNT = 4;
304        // TYPE_MAIL (FAVORITES)
305        count = EmailContent.count(this, Message.CONTENT_URI, FAVORITE_COUNT_SELECTION, null);
306        if (count > 0) {
307            row = childCursor.newRow();
308            row.add(Long.valueOf(Mailbox.QUERY_ALL_FAVORITES)); // MAILBOX_COLUMN_ID = 0;
309            // MAILBOX_DISPLAY_NAME
310            row.add(getString(R.string.account_folder_list_summary_starred));
311            row.add(null);                                          // MAILBOX_ACCOUNT_KEY = 2;
312            row.add(Integer.valueOf(Mailbox.TYPE_MAIL));            // MAILBOX_TYPE = 3;
313            row.add(Integer.valueOf(count));                        // MAILBOX_UNREAD_COUNT = 4;
314        }
315        // TYPE_DRAFTS
316        count = getCountByMailboxType(this, Mailbox.TYPE_DRAFTS);
317        if (count > 0) {
318            row = childCursor.newRow();
319            row.add(Long.valueOf(Mailbox.QUERY_ALL_DRAFTS));    // MAILBOX_COLUMN_ID = 0;
320            row.add(getString(R.string.account_folder_list_summary_drafts));// MAILBOX_DISPLAY_NAME
321            row.add(null);                                          // MAILBOX_ACCOUNT_KEY = 2;
322            row.add(Integer.valueOf(Mailbox.TYPE_DRAFTS));          // MAILBOX_TYPE = 3;
323            row.add(Integer.valueOf(count));                        // MAILBOX_UNREAD_COUNT = 4;
324        }
325        // TYPE_OUTBOX
326        count = getCountByMailboxType(this, Mailbox.TYPE_OUTBOX);
327        if (count > 0) {
328            row = childCursor.newRow();
329            row.add(Long.valueOf(Mailbox.QUERY_ALL_OUTBOX));    // MAILBOX_COLUMN_ID = 0;
330            row.add(getString(R.string.account_folder_list_summary_outbox));// MAILBOX_DISPLAY_NAME
331            row.add(null);                                          // MAILBOX_ACCOUNT_KEY = 2;
332            row.add(Integer.valueOf(Mailbox.TYPE_OUTBOX));          // MAILBOX_TYPE = 3;
333            row.add(Integer.valueOf(count));                        // MAILBOX_UNREAD_COUNT = 4;
334        }
335        return childCursor;
336    }
337
338    /**
339     * Async task to handle the accounts query outside of the UI thread
340     */
341    private class LoadAccountsTask extends AsyncTask<Void, Void, Object[]> {
342        @Override
343        protected Object[] doInBackground(Void... params) {
344            // Create the summaries cursor
345            Cursor c1 = getSummaryChildCursor();
346
347            // TODO use a custom projection and don't have to sample all of these columns
348            Cursor c2 = getContentResolver().query(
349                    EmailContent.Account.CONTENT_URI,
350                    EmailContent.Account.CONTENT_PROJECTION, null, null, null);
351            Long defaultAccount = Account.getDefaultAccountId(AccountFolderList.this);
352            return new Object[] { c1, c2 , defaultAccount};
353        }
354
355        @Override
356        protected void onPostExecute(Object[] params) {
357            if (isCancelled() || params == null || ((Cursor)params[1]).isClosed()) {
358                return;
359            }
360            // Before writing a new list adapter into the listview, we need to
361            // shut down the old one (if any).
362            ListAdapter oldAdapter = mListView.getAdapter();
363            if (oldAdapter != null && oldAdapter instanceof CursorAdapter) {
364                ((CursorAdapter)oldAdapter).changeCursor(null);
365            }
366            // Now create a new list adapter and install it
367            mListAdapter = AccountsAdapter.getInstance((Cursor)params[0], (Cursor)params[1],
368                    AccountFolderList.this, (Long)params[2]);
369            mListView.setAdapter(mListAdapter);
370        }
371    }
372
373    private class DeleteAccountTask extends AsyncTask<Void, Void, Void> {
374        private final long mAccountId;
375        private final String mAccountUri;
376
377        public DeleteAccountTask(long accountId, String accountUri) {
378            mAccountId = accountId;
379            mAccountUri = accountUri;
380        }
381
382        @Override
383        protected Void doInBackground(Void... params) {
384            try {
385                // Delete Remote store at first.
386                Store.getInstance(mAccountUri, getApplication(), null).delete();
387                // Remove the Store instance from cache.
388                Store.removeInstance(mAccountUri);
389                Uri uri = ContentUris.withAppendedId(
390                        EmailContent.Account.CONTENT_URI, mAccountId);
391                AccountFolderList.this.getContentResolver().delete(uri, null, null);
392                // Update the backup (side copy) of the accounts
393                AccountBackupRestore.backupAccounts(AccountFolderList.this);
394                // Release or relax device administration, if relevant
395                SecurityPolicy.getInstance(AccountFolderList.this).reducePolicies();
396            } catch (Exception e) {
397                    // Ignore
398            }
399            Email.setServicesEnabled(AccountFolderList.this);
400            return null;
401        }
402
403        @Override
404        protected void onPostExecute(Void v) {
405            if (!isCancelled()) {
406                updateAccounts();
407            }
408        }
409    }
410
411    private void updateAccounts() {
412        Utility.cancelTaskInterrupt(mLoadAccountsTask);
413        mLoadAccountsTask = (LoadAccountsTask) new LoadAccountsTask().execute();
414    }
415
416    private void onAddNewAccount() {
417        AccountSetupBasics.actionNewAccount(this);
418    }
419
420    private void onEditAccount(long accountId) {
421        AccountSettings.actionSettings(this, accountId);
422    }
423
424    /**
425     * Refresh one or all accounts
426     * @param accountId A specific id to refresh folders only, or -1 to refresh everything
427     */
428    private void onRefresh(long accountId) {
429        if (accountId == -1) {
430            // TODO implement a suitable "Refresh all accounts" / "check mail" comment in Controller
431            // TODO this is temp
432            Toast.makeText(this, getString(R.string.account_folder_list_refresh_toast),
433                    Toast.LENGTH_LONG).show();
434        } else {
435            mHandler.progress(true);
436            Controller.getInstance(getApplication()).updateMailboxList(
437                    accountId, mControllerCallback);
438        }
439    }
440
441    private void onCompose(long accountId) {
442        if (accountId == -1) {
443            accountId = Account.getDefaultAccountId(this);
444        }
445        if (accountId != -1) {
446            MessageCompose.actionCompose(this, accountId);
447        } else {
448            onAddNewAccount();
449        }
450    }
451
452    private void onDeleteAccount(long accountId) {
453        mSelectedContextAccount = Account.restoreAccountWithId(this, accountId);
454        showDialog(DIALOG_REMOVE_ACCOUNT);
455    }
456
457    @Override
458    public Dialog onCreateDialog(int id) {
459        switch (id) {
460            case DIALOG_REMOVE_ACCOUNT:
461                return createRemoveAccountDialog();
462        }
463        return super.onCreateDialog(id);
464    }
465
466    private Dialog createRemoveAccountDialog() {
467        return new AlertDialog.Builder(this)
468            .setIcon(android.R.drawable.ic_dialog_alert)
469            .setTitle(R.string.account_delete_dlg_title)
470            .setMessage(getString(R.string.account_delete_dlg_instructions_fmt,
471                    mSelectedContextAccount.getDisplayName()))
472            .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() {
473                public void onClick(DialogInterface dialog, int whichButton) {
474                    dismissDialog(DIALOG_REMOVE_ACCOUNT);
475                    // Clear notifications, which may become stale here
476                    NotificationManager notificationManager = (NotificationManager)
477                            getSystemService(Context.NOTIFICATION_SERVICE);
478                    notificationManager.cancel(MailService.NOTIFICATION_ID_NEW_MESSAGES);
479                    int numAccounts = EmailContent.count(AccountFolderList.this,
480                            Account.CONTENT_URI, null, null);
481                    mListAdapter.addOnDeletingAccount(mSelectedContextAccount.mId);
482
483                    mDeleteAccountTask = (DeleteAccountTask) new DeleteAccountTask(
484                            mSelectedContextAccount.mId,
485                            mSelectedContextAccount.getStoreUri(AccountFolderList.this)).execute();
486                    if (numAccounts == 1) {
487                        AccountSetupBasics.actionNewAccount(AccountFolderList.this);
488                        finish();
489                    }
490                }
491            })
492            .setNegativeButton(R.string.cancel_action, new DialogInterface.OnClickListener() {
493                public void onClick(DialogInterface dialog, int whichButton) {
494                    dismissDialog(DIALOG_REMOVE_ACCOUNT);
495                }
496            })
497            .create();
498    }
499
500    /**
501     * Update a cached dialog with current values (e.g. account name)
502     */
503    @Override
504    public void onPrepareDialog(int id, Dialog dialog) {
505        switch (id) {
506            case DIALOG_REMOVE_ACCOUNT:
507                AlertDialog alert = (AlertDialog) dialog;
508                alert.setMessage(getString(R.string.account_delete_dlg_instructions_fmt,
509                        mSelectedContextAccount.getDisplayName()));
510        }
511    }
512
513    @Override
514    public boolean onContextItemSelected(MenuItem item) {
515        AdapterView.AdapterContextMenuInfo menuInfo =
516            (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
517
518        if (mListAdapter.isMailbox(menuInfo.position)) {
519            Cursor c = (Cursor) mListView.getItemAtPosition(menuInfo.position);
520            long id = c.getLong(MAILBOX_COLUMN_ID);
521            switch (item.getItemId()) {
522                case R.id.open_folder:
523                    MessageList.actionHandleMailbox(this, id);
524                    break;
525                case R.id.check_mail:
526                    onRefresh(-1);
527                    break;
528            }
529            return false;
530        } else if (mListAdapter.isAccount(menuInfo.position)) {
531            Cursor c = (Cursor) mListView.getItemAtPosition(menuInfo.position);
532            long accountId = c.getLong(Account.CONTENT_ID_COLUMN);
533            switch (item.getItemId()) {
534                case R.id.open_folder:
535                    MailboxList.actionHandleAccount(this, accountId);
536                    break;
537                case R.id.compose:
538                    onCompose(accountId);
539                    break;
540                case R.id.refresh_account:
541                    onRefresh(accountId);
542                    break;
543                case R.id.edit_account:
544                    onEditAccount(accountId);
545                    break;
546                case R.id.delete_account:
547                    onDeleteAccount(accountId);
548                    break;
549            }
550            return true;
551        }
552        return false;
553    }
554
555    @Override
556    public boolean onOptionsItemSelected(MenuItem item) {
557        switch (item.getItemId()) {
558            case R.id.add_new_account:
559                onAddNewAccount();
560                break;
561            case R.id.check_mail:
562                onRefresh(-1);
563                break;
564            case R.id.compose:
565                onCompose(-1);
566                break;
567            default:
568                return super.onOptionsItemSelected(item);
569        }
570        return true;
571    }
572
573    public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
574        return true;
575    }
576
577    @Override
578    public boolean onCreateOptionsMenu(Menu menu) {
579        super.onCreateOptionsMenu(menu);
580        getMenuInflater().inflate(R.menu.account_folder_list_option, menu);
581        return true;
582    }
583
584    @Override
585    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo info) {
586        super.onCreateContextMenu(menu, v, info);
587        AdapterView.AdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo) info;
588        if (mListAdapter.isMailbox(menuInfo.position)) {
589            Cursor c = (Cursor) mListView.getItemAtPosition(menuInfo.position);
590            String displayName = c.getString(Account.CONTENT_DISPLAY_NAME_COLUMN);
591            menu.setHeaderTitle(displayName);
592            getMenuInflater().inflate(R.menu.account_folder_list_smart_folder_context, menu);
593        } else if (mListAdapter.isAccount(menuInfo.position)) {
594            Cursor c = (Cursor) mListView.getItemAtPosition(menuInfo.position);
595            String accountName = c.getString(Account.CONTENT_DISPLAY_NAME_COLUMN);
596            menu.setHeaderTitle(accountName);
597            getMenuInflater().inflate(R.menu.account_folder_list_context, menu);
598        }
599    }
600
601    @Override
602    public boolean onKeyDown(int keyCode, KeyEvent event) {
603        if (event.getKeyCode() == secretKeyCodes[mSecretKeyCodeIndex]) {
604            mSecretKeyCodeIndex++;
605            if (mSecretKeyCodeIndex == secretKeyCodes.length) {
606                mSecretKeyCodeIndex = 0;
607                startActivity(new Intent(this, Debug.class));
608            }
609        } else {
610            mSecretKeyCodeIndex = 0;
611        }
612        return super.onKeyDown(keyCode, event);
613    }
614
615    /**
616     * Handler for UI-thread operations (when called from callbacks or any other threads)
617     */
618    private class MessageListHandler extends Handler {
619        private static final int MSG_PROGRESS = 1;
620
621        @Override
622        public void handleMessage(android.os.Message msg) {
623            switch (msg.what) {
624                case MSG_PROGRESS:
625                    boolean showProgress = (msg.arg1 != 0);
626                    if (showProgress) {
627                        mProgressIcon.setVisibility(View.VISIBLE);
628                    } else {
629                        mProgressIcon.setVisibility(View.GONE);
630                    }
631                    break;
632                default:
633                    super.handleMessage(msg);
634            }
635        }
636
637        /**
638         * Call from any thread to start/stop progress indicator(s)
639         * @param progress true to start, false to stop
640         */
641        public void progress(boolean progress) {
642            android.os.Message msg = android.os.Message.obtain();
643            msg.what = MSG_PROGRESS;
644            msg.arg1 = progress ? 1 : 0;
645            sendMessage(msg);
646        }
647    }
648
649    /**
650     * Callback for async Controller results.
651     */
652    private class ControllerResults implements Controller.Result {
653        public void updateMailboxListCallback(MessagingException result, long accountKey,
654                int progress) {
655            updateProgress(result, progress);
656        }
657
658        public void updateMailboxCallback(MessagingException result, long accountKey,
659                long mailboxKey, int progress, int numNewMessages) {
660            if (result != null || progress == 100) {
661                Email.updateMailboxRefreshTime(mailboxKey);
662            }
663            if (progress == 100) {
664                updateAccounts();
665            }
666            updateProgress(result, progress);
667        }
668
669        public void loadMessageForViewCallback(MessagingException result, long messageId,
670                int progress) {
671        }
672
673        public void loadAttachmentCallback(MessagingException result, long messageId,
674                long attachmentId, int progress) {
675        }
676
677        public void serviceCheckMailCallback(MessagingException result, long accountId,
678                long mailboxId, int progress, long tag) {
679            updateProgress(result, progress);
680        }
681
682        public void sendMailCallback(MessagingException result, long accountId, long messageId,
683                int progress) {
684            if (progress == 100) {
685                updateAccounts();
686            }
687        }
688
689        private void updateProgress(MessagingException result, int progress) {
690            if (result != null || progress == 100) {
691                mHandler.progress(false);
692            } else if (progress == 0) {
693                mHandler.progress(true);
694            }
695        }
696    }
697
698    /* package */ static class AccountsAdapter extends CursorAdapter {
699
700        private final Context mContext;
701        private final LayoutInflater mInflater;
702        private final int mMailboxesCount;
703        private final int mSeparatorPosition;
704        private final long mDefaultAccountId;
705        private final ArrayList<Long> mOnDeletingAccounts = new ArrayList<Long>();
706
707        public static AccountsAdapter getInstance(Cursor mailboxesCursor, Cursor accountsCursor,
708                Context context, long defaultAccountId) {
709            Cursor[] cursors = new Cursor[] { mailboxesCursor, accountsCursor };
710            Cursor mc = new MergeCursor(cursors);
711            return new AccountsAdapter(mc, context, mailboxesCursor.getCount(), defaultAccountId);
712        }
713
714        public AccountsAdapter(Cursor c, Context context, int mailboxesCount,
715                long defaultAccountId) {
716            super(context, c, true);
717            mContext = context;
718            mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
719            mMailboxesCount = mailboxesCount;
720            mSeparatorPosition = mailboxesCount;
721            mDefaultAccountId = defaultAccountId;
722        }
723
724        public boolean isMailbox(int position) {
725            return position < mMailboxesCount;
726        }
727
728        public boolean isAccount(int position) {
729            return position >= mMailboxesCount;
730        }
731
732        public void addOnDeletingAccount(long accountId) {
733            mOnDeletingAccounts.add(accountId);
734        }
735
736        public boolean isOnDeletingAccountView(long accountId) {
737            return mOnDeletingAccounts.contains(accountId);
738        }
739
740        /**
741         * This is used as a callback from the list items, for clicks in the folder "button"
742         *
743         * @param itemView the item in which the click occurred
744         */
745        public void onClickFolder(AccountFolderListItem itemView) {
746            MailboxList.actionHandleAccount(mContext, itemView.mAccountId);
747        }
748
749        @Override
750        public void bindView(View view, Context context, Cursor cursor) {
751            if (cursor.getPosition() < mMailboxesCount) {
752                bindMailboxItem(view, context, cursor, false);
753            } else {
754                bindAccountItem(view, context, cursor, false);
755            }
756        }
757
758        private void bindMailboxItem(View view, Context context, Cursor cursor, boolean isLastChild)
759                {
760            // Reset the view (in case it was recycled) and prepare for binding
761            AccountFolderListItem itemView = (AccountFolderListItem) view;
762            itemView.bindViewInit(this, false);
763
764            // Invisible (not "gone") to maintain spacing
765            view.findViewById(R.id.chip).setVisibility(View.INVISIBLE);
766
767            String text = cursor.getString(MAILBOX_DISPLAY_NAME);
768            if (text != null) {
769                TextView nameView = (TextView) view.findViewById(R.id.name);
770                nameView.setText(text);
771            }
772
773            // TODO get/track live folder status
774            text = null;
775            TextView statusView = (TextView) view.findViewById(R.id.status);
776            if (text != null) {
777                statusView.setText(text);
778                statusView.setVisibility(View.VISIBLE);
779            } else {
780                statusView.setVisibility(View.GONE);
781            }
782
783            int count = -1;
784            text = cursor.getString(MAILBOX_UNREAD_COUNT);
785            if (text != null) {
786                count = Integer.valueOf(text);
787            }
788            TextView unreadCountView = (TextView) view.findViewById(R.id.new_message_count);
789            TextView allCountView = (TextView) view.findViewById(R.id.all_message_count);
790            int id = cursor.getInt(MAILBOX_COLUMN_ID);
791            // If the unread count is zero, not to show countView.
792            if (count > 0) {
793                if (id == Mailbox.QUERY_ALL_FAVORITES
794                        || id == Mailbox.QUERY_ALL_DRAFTS
795                        || id == Mailbox.QUERY_ALL_OUTBOX) {
796                    unreadCountView.setVisibility(View.GONE);
797                    allCountView.setVisibility(View.VISIBLE);
798                    allCountView.setText(text);
799                } else {
800                    allCountView.setVisibility(View.GONE);
801                    unreadCountView.setVisibility(View.VISIBLE);
802                    unreadCountView.setText(text);
803                }
804            } else {
805                allCountView.setVisibility(View.GONE);
806                unreadCountView.setVisibility(View.GONE);
807            }
808
809            view.findViewById(R.id.folder_button).setVisibility(View.GONE);
810            view.findViewById(R.id.folder_separator).setVisibility(View.GONE);
811            view.findViewById(R.id.default_sender).setVisibility(View.GONE);
812            view.findViewById(R.id.folder_icon).setVisibility(View.VISIBLE);
813            ((ImageView)view.findViewById(R.id.folder_icon)).setImageDrawable(
814                    Utility.FolderProperties.getInstance(context).getSummaryMailboxIconIds(id));
815        }
816
817        private void bindAccountItem(View view, Context context, Cursor cursor, boolean isExpanded)
818                {
819            // Reset the view (in case it was recycled) and prepare for binding
820            AccountFolderListItem itemView = (AccountFolderListItem) view;
821            itemView.bindViewInit(this, true);
822            itemView.mAccountId = cursor.getLong(Account.CONTENT_ID_COLUMN);
823
824            long accountId = cursor.getLong(Account.CONTENT_ID_COLUMN);
825            View chipView = view.findViewById(R.id.chip);
826            chipView.setBackgroundResource(Email.getAccountColorResourceId(accountId));
827            chipView.setVisibility(View.VISIBLE);
828
829            String text = cursor.getString(Account.CONTENT_DISPLAY_NAME_COLUMN);
830            if (text != null) {
831                TextView descriptionView = (TextView) view.findViewById(R.id.name);
832                descriptionView.setText(text);
833            }
834
835            text = cursor.getString(Account.CONTENT_EMAIL_ADDRESS_COLUMN);
836            if (text != null) {
837                TextView emailView = (TextView) view.findViewById(R.id.status);
838                emailView.setText(text);
839                emailView.setVisibility(View.VISIBLE);
840            }
841
842            int unreadMessageCount = 0;
843            Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI,
844                    MAILBOX_UNREAD_COUNT_PROJECTION,
845                    MAILBOX_INBOX_SELECTION,
846                    new String[] { String.valueOf(accountId) }, null);
847
848            try {
849                if (c.moveToFirst()) {
850                    String count = c.getString(MAILBOX_UNREAD_COUNT_COLUMN_UNREAD_COUNT);
851                    if (count != null) {
852                        unreadMessageCount = Integer.valueOf(count);
853                    }
854                }
855            } finally {
856                c.close();
857            }
858
859            view.findViewById(R.id.all_message_count).setVisibility(View.GONE);
860            TextView unreadCountView = (TextView) view.findViewById(R.id.new_message_count);
861            if (unreadMessageCount > 0) {
862                unreadCountView.setText(String.valueOf(unreadMessageCount));
863                unreadCountView.setVisibility(View.VISIBLE);
864            } else {
865                unreadCountView.setVisibility(View.GONE);
866            }
867
868            view.findViewById(R.id.folder_icon).setVisibility(View.GONE);
869            view.findViewById(R.id.folder_button).setVisibility(View.VISIBLE);
870            view.findViewById(R.id.folder_separator).setVisibility(View.VISIBLE);
871            if (accountId == mDefaultAccountId) {
872                view.findViewById(R.id.default_sender).setVisibility(View.VISIBLE);
873            } else {
874                view.findViewById(R.id.default_sender).setVisibility(View.GONE);
875            }
876        }
877
878        @Override
879        public View newView(Context context, Cursor cursor, ViewGroup parent) {
880            return mInflater.inflate(R.layout.account_folder_list_item, parent, false);
881        }
882
883        /*
884         * The following series of overrides insert the "Accounts" separator
885         */
886
887        /**
888         * Prevents the separator view from recycling into the other views
889         */
890        @Override
891        public int getItemViewType(int position) {
892            if (position == mSeparatorPosition) {
893                return IGNORE_ITEM_VIEW_TYPE;
894            }
895            return super.getItemViewType(position);
896        }
897
898        /**
899         * Injects the separator view when required, and fudges the cursor for other views
900         */
901        @Override
902        public View getView(int position, View convertView, ViewGroup parent) {
903            // The base class's getView() checks for mDataValid at the beginning, but we don't have
904            // to do that, because if the cursor is invalid getCount() returns 0, in which case this
905            // method wouldn't get called.
906
907            // Handle the separator here - create & bind
908            if (position == mSeparatorPosition) {
909                TextView view;
910                view = (TextView) mInflater.inflate(R.layout.list_separator, parent, false);
911                view.setText(R.string.account_folder_list_separator_accounts);
912                return view;
913            }
914            return super.getView(getRealPosition(position), convertView, parent);
915        }
916
917        /**
918         * Forces navigation to skip over the separator
919         */
920        @Override
921        public boolean areAllItemsEnabled() {
922            return false;
923        }
924
925        /**
926         * Forces navigation to skip over the separator
927         */
928        @Override
929        public boolean isEnabled(int position) {
930            if (position == mSeparatorPosition) {
931                return false;
932            } else if (isAccount(position)) {
933                Long id = ((MergeCursor)getItem(position)).getLong(Account.CONTENT_ID_COLUMN);
934                return !isOnDeletingAccountView(id);
935            } else {
936                return true;
937            }
938        }
939
940        /**
941         * Adjusts list count to include separator
942         */
943        @Override
944        public int getCount() {
945            int count = super.getCount();
946            if (count > 0 && (mSeparatorPosition != ListView.INVALID_POSITION)) {
947                // Increment for separator, if we have anything to show.
948                count += 1;
949            }
950            return count;
951        }
952
953        /**
954         * Converts list position to cursor position
955         */
956        private int getRealPosition(int pos) {
957            if (mSeparatorPosition == ListView.INVALID_POSITION) {
958                // No separator, identity map
959                return pos;
960            } else if (pos <= mSeparatorPosition) {
961                // Before or at the separator, identity map
962                return pos;
963            } else {
964                // After the separator, remove 1 from the pos to get the real underlying pos
965                return pos - 1;
966            }
967        }
968
969        /**
970         * Returns the item using external position numbering (no separator)
971         */
972        @Override
973        public Object getItem(int pos) {
974            return super.getItem(getRealPosition(pos));
975        }
976
977        /**
978         * Returns the item id using external position numbering (no separator)
979         */
980        @Override
981        public long getItemId(int pos) {
982            return super.getItemId(getRealPosition(pos));
983        }
984    }
985}
986
987
988