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.MatrixCursor.RowBuilder;
48import android.database.MergeCursor;
49import android.net.Uri;
50import android.os.AsyncTask;
51import android.os.Bundle;
52import android.os.Handler;
53import android.view.ContextMenu;
54import android.view.ContextMenu.ContextMenuInfo;
55import android.view.KeyEvent;
56import android.view.LayoutInflater;
57import android.view.Menu;
58import android.view.MenuItem;
59import android.view.View;
60import android.view.ViewGroup;
61import android.view.Window;
62import android.widget.AdapterView;
63import android.widget.AdapterView.OnItemClickListener;
64import android.widget.CursorAdapter;
65import android.widget.ImageView;
66import android.widget.ListAdapter;
67import android.widget.ListView;
68import android.widget.ProgressBar;
69import android.widget.TextView;
70import android.widget.Toast;
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 final int[] SECRET_KEY_CODES = {
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            Cursor c1 = null;
345            Cursor c2 = null;
346            Long defaultAccount = null;
347            if (!isCancelled()) {
348                // Create the summaries cursor
349                c1 = getSummaryChildCursor();
350            }
351
352            if (!isCancelled()) {
353                // TODO use a custom projection and don't have to sample all of these columns
354                c2 = getContentResolver().query(
355                        EmailContent.Account.CONTENT_URI,
356                        EmailContent.Account.CONTENT_PROJECTION, null, null, null);
357            }
358
359            if (!isCancelled()) {
360                defaultAccount = Account.getDefaultAccountId(AccountFolderList.this);
361            }
362
363            if (isCancelled()) {
364                if (c1 != null) c1.close();
365                if (c2 != null) c2.close();
366                return null;
367            }
368            return new Object[] { c1, c2 , defaultAccount};
369        }
370
371        @Override
372        protected void onPostExecute(Object[] params) {
373            if (isCancelled() || params == null) {
374                if (params != null) {
375                    Cursor c1 = (Cursor)params[0];
376                    if (c1 != null) {
377                        c1.close();
378                    }
379                    Cursor c2 = (Cursor)params[1];
380                    if (c2 != null) {
381                        c2.close();
382                    }
383                }
384                return;
385            }
386            // Before writing a new list adapter into the listview, we need to
387            // shut down the old one (if any).
388            ListAdapter oldAdapter = mListView.getAdapter();
389            if (oldAdapter != null && oldAdapter instanceof CursorAdapter) {
390                ((CursorAdapter)oldAdapter).changeCursor(null);
391            }
392            // Now create a new list adapter and install it
393            mListAdapter = AccountsAdapter.getInstance((Cursor)params[0], (Cursor)params[1],
394                    AccountFolderList.this, (Long)params[2]);
395            mListView.setAdapter(mListAdapter);
396        }
397    }
398
399    private class DeleteAccountTask extends AsyncTask<Void, Void, Void> {
400        private final long mAccountId;
401        private final String mAccountUri;
402
403        public DeleteAccountTask(long accountId, String accountUri) {
404            mAccountId = accountId;
405            mAccountUri = accountUri;
406        }
407
408        @Override
409        protected Void doInBackground(Void... params) {
410            try {
411                // Delete Remote store at first.
412                Store.getInstance(mAccountUri, getApplication(), null).delete();
413                // Remove the Store instance from cache.
414                Store.removeInstance(mAccountUri);
415                Uri uri = ContentUris.withAppendedId(
416                        EmailContent.Account.CONTENT_URI, mAccountId);
417                AccountFolderList.this.getContentResolver().delete(uri, null, null);
418                // Update the backup (side copy) of the accounts
419                AccountBackupRestore.backupAccounts(AccountFolderList.this);
420                // Release or relax device administration, if relevant
421                SecurityPolicy.getInstance(AccountFolderList.this).reducePolicies();
422            } catch (Exception e) {
423                    // Ignore
424            }
425            Email.setServicesEnabled(AccountFolderList.this);
426            return null;
427        }
428
429        @Override
430        protected void onPostExecute(Void v) {
431            if (!isCancelled()) {
432                updateAccounts();
433            }
434        }
435    }
436
437    private void updateAccounts() {
438        Utility.cancelTaskInterrupt(mLoadAccountsTask);
439        mLoadAccountsTask = (LoadAccountsTask) new LoadAccountsTask().execute();
440    }
441
442    private void onAddNewAccount() {
443        AccountSetupBasics.actionNewAccount(this);
444    }
445
446    private void onEditAccount(long accountId) {
447        AccountSettings.actionSettings(this, accountId);
448    }
449
450    /**
451     * Refresh one or all accounts
452     * @param accountId A specific id to refresh folders only, or -1 to refresh everything
453     */
454    private void onRefresh(long accountId) {
455        if (accountId == -1) {
456            // TODO implement a suitable "Refresh all accounts" / "check mail" comment in Controller
457            // TODO this is temp
458            Toast.makeText(this, getString(R.string.account_folder_list_refresh_toast),
459                    Toast.LENGTH_LONG).show();
460        } else {
461            mHandler.progress(true);
462            Controller.getInstance(getApplication()).updateMailboxList(
463                    accountId, mControllerCallback);
464        }
465    }
466
467    private void onCompose(long accountId) {
468        if (accountId == -1) {
469            accountId = Account.getDefaultAccountId(this);
470        }
471        if (accountId != -1) {
472            MessageCompose.actionCompose(this, accountId);
473        } else {
474            onAddNewAccount();
475        }
476    }
477
478    private void onDeleteAccount(long accountId) {
479        mSelectedContextAccount = Account.restoreAccountWithId(this, accountId);
480        showDialog(DIALOG_REMOVE_ACCOUNT);
481    }
482
483    @Override
484    public Dialog onCreateDialog(int id) {
485        switch (id) {
486            case DIALOG_REMOVE_ACCOUNT:
487                return createRemoveAccountDialog();
488        }
489        return super.onCreateDialog(id);
490    }
491
492    private Dialog createRemoveAccountDialog() {
493        return new AlertDialog.Builder(this)
494            .setIcon(android.R.drawable.ic_dialog_alert)
495            .setTitle(R.string.account_delete_dlg_title)
496            .setMessage(getString(R.string.account_delete_dlg_instructions_fmt,
497                    mSelectedContextAccount.getDisplayName()))
498            .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() {
499                public void onClick(DialogInterface dialog, int whichButton) {
500                    dismissDialog(DIALOG_REMOVE_ACCOUNT);
501                    // Clear notifications, which may become stale here
502                    NotificationManager notificationManager = (NotificationManager)
503                            getSystemService(Context.NOTIFICATION_SERVICE);
504                    notificationManager.cancel(MailService.NOTIFICATION_ID_NEW_MESSAGES);
505                    int numAccounts = EmailContent.count(AccountFolderList.this,
506                            Account.CONTENT_URI, null, null);
507                    mListAdapter.addOnDeletingAccount(mSelectedContextAccount.mId);
508
509                    mDeleteAccountTask = (DeleteAccountTask) new DeleteAccountTask(
510                            mSelectedContextAccount.mId,
511                            mSelectedContextAccount.getStoreUri(AccountFolderList.this)).execute();
512                    if (numAccounts == 1) {
513                        AccountSetupBasics.actionNewAccount(AccountFolderList.this);
514                        finish();
515                    }
516                }
517            })
518            .setNegativeButton(R.string.cancel_action, new DialogInterface.OnClickListener() {
519                public void onClick(DialogInterface dialog, int whichButton) {
520                    dismissDialog(DIALOG_REMOVE_ACCOUNT);
521                }
522            })
523            .create();
524    }
525
526    /**
527     * Update a cached dialog with current values (e.g. account name)
528     */
529    @Override
530    public void onPrepareDialog(int id, Dialog dialog) {
531        switch (id) {
532            case DIALOG_REMOVE_ACCOUNT:
533                AlertDialog alert = (AlertDialog) dialog;
534                alert.setMessage(getString(R.string.account_delete_dlg_instructions_fmt,
535                        mSelectedContextAccount.getDisplayName()));
536        }
537    }
538
539    @Override
540    public boolean onContextItemSelected(MenuItem item) {
541        AdapterView.AdapterContextMenuInfo menuInfo =
542            (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
543
544        if (mListAdapter.isMailbox(menuInfo.position)) {
545            Cursor c = (Cursor) mListView.getItemAtPosition(menuInfo.position);
546            long id = c.getLong(MAILBOX_COLUMN_ID);
547            switch (item.getItemId()) {
548                case R.id.open_folder:
549                    MessageList.actionHandleMailbox(this, id);
550                    break;
551                case R.id.check_mail:
552                    onRefresh(-1);
553                    break;
554            }
555            return false;
556        } else if (mListAdapter.isAccount(menuInfo.position)) {
557            Cursor c = (Cursor) mListView.getItemAtPosition(menuInfo.position);
558            long accountId = c.getLong(Account.CONTENT_ID_COLUMN);
559            switch (item.getItemId()) {
560                case R.id.open_folder:
561                    MailboxList.actionHandleAccount(this, accountId);
562                    break;
563                case R.id.compose:
564                    onCompose(accountId);
565                    break;
566                case R.id.refresh_account:
567                    onRefresh(accountId);
568                    break;
569                case R.id.edit_account:
570                    onEditAccount(accountId);
571                    break;
572                case R.id.delete_account:
573                    onDeleteAccount(accountId);
574                    break;
575            }
576            return true;
577        }
578        return false;
579    }
580
581    @Override
582    public boolean onOptionsItemSelected(MenuItem item) {
583        switch (item.getItemId()) {
584            case R.id.add_new_account:
585                onAddNewAccount();
586                break;
587            case R.id.check_mail:
588                onRefresh(-1);
589                break;
590            case R.id.compose:
591                onCompose(-1);
592                break;
593            default:
594                return super.onOptionsItemSelected(item);
595        }
596        return true;
597    }
598
599    public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
600        return true;
601    }
602
603    @Override
604    public boolean onCreateOptionsMenu(Menu menu) {
605        super.onCreateOptionsMenu(menu);
606        getMenuInflater().inflate(R.menu.account_folder_list_option, menu);
607        return true;
608    }
609
610    @Override
611    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo info) {
612        super.onCreateContextMenu(menu, v, info);
613        AdapterView.AdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo) info;
614        if (mListAdapter.isMailbox(menuInfo.position)) {
615            Cursor c = (Cursor) mListView.getItemAtPosition(menuInfo.position);
616            String displayName = c.getString(Account.CONTENT_DISPLAY_NAME_COLUMN);
617            menu.setHeaderTitle(displayName);
618            getMenuInflater().inflate(R.menu.account_folder_list_smart_folder_context, menu);
619        } else if (mListAdapter.isAccount(menuInfo.position)) {
620            Cursor c = (Cursor) mListView.getItemAtPosition(menuInfo.position);
621            String accountName = c.getString(Account.CONTENT_DISPLAY_NAME_COLUMN);
622            menu.setHeaderTitle(accountName);
623            getMenuInflater().inflate(R.menu.account_folder_list_context, menu);
624        }
625    }
626
627    @Override
628    public boolean onKeyDown(int keyCode, KeyEvent event) {
629        if (event.getKeyCode() == SECRET_KEY_CODES[mSecretKeyCodeIndex]) {
630            mSecretKeyCodeIndex++;
631            if (mSecretKeyCodeIndex == SECRET_KEY_CODES.length) {
632                mSecretKeyCodeIndex = 0;
633                startActivity(new Intent(this, Debug.class));
634            }
635        } else {
636            mSecretKeyCodeIndex = 0;
637        }
638        return super.onKeyDown(keyCode, event);
639    }
640
641    /**
642     * Handler for UI-thread operations (when called from callbacks or any other threads)
643     */
644    private class MessageListHandler extends Handler {
645        private static final int MSG_PROGRESS = 1;
646
647        @Override
648        public void handleMessage(android.os.Message msg) {
649            switch (msg.what) {
650                case MSG_PROGRESS:
651                    boolean showProgress = (msg.arg1 != 0);
652                    if (showProgress) {
653                        mProgressIcon.setVisibility(View.VISIBLE);
654                    } else {
655                        mProgressIcon.setVisibility(View.GONE);
656                    }
657                    break;
658                default:
659                    super.handleMessage(msg);
660            }
661        }
662
663        /**
664         * Call from any thread to start/stop progress indicator(s)
665         * @param progress true to start, false to stop
666         */
667        public void progress(boolean progress) {
668            android.os.Message msg = android.os.Message.obtain();
669            msg.what = MSG_PROGRESS;
670            msg.arg1 = progress ? 1 : 0;
671            sendMessage(msg);
672        }
673    }
674
675    /**
676     * Callback for async Controller results.
677     */
678    private class ControllerResults implements Controller.Result {
679        public void updateMailboxListCallback(MessagingException result, long accountKey,
680                int progress) {
681            updateProgress(result, progress);
682        }
683
684        public void updateMailboxCallback(MessagingException result, long accountKey,
685                long mailboxKey, int progress, int numNewMessages) {
686            if (result != null || progress == 100) {
687                Email.updateMailboxRefreshTime(mailboxKey);
688            }
689            if (progress == 100) {
690                updateAccounts();
691            }
692            updateProgress(result, progress);
693        }
694
695        public void loadMessageForViewCallback(MessagingException result, long messageId,
696                int progress) {
697        }
698
699        public void loadAttachmentCallback(MessagingException result, long messageId,
700                long attachmentId, int progress) {
701        }
702
703        public void serviceCheckMailCallback(MessagingException result, long accountId,
704                long mailboxId, int progress, long tag) {
705            updateProgress(result, progress);
706        }
707
708        public void sendMailCallback(MessagingException result, long accountId, long messageId,
709                int progress) {
710            if (progress == 100) {
711                updateAccounts();
712            }
713        }
714
715        private void updateProgress(MessagingException result, int progress) {
716            if (result != null || progress == 100) {
717                mHandler.progress(false);
718            } else if (progress == 0) {
719                mHandler.progress(true);
720            }
721        }
722    }
723
724    /* package */ static class AccountsAdapter extends CursorAdapter {
725
726        private final Context mContext;
727        private final LayoutInflater mInflater;
728        private final int mMailboxesCount;
729        private final int mSeparatorPosition;
730        private final long mDefaultAccountId;
731        private final ArrayList<Long> mOnDeletingAccounts = new ArrayList<Long>();
732
733        public static AccountsAdapter getInstance(Cursor mailboxesCursor, Cursor accountsCursor,
734                Context context, long defaultAccountId) {
735            Cursor[] cursors = new Cursor[] { mailboxesCursor, accountsCursor };
736            Cursor mc = new MergeCursor(cursors);
737            return new AccountsAdapter(mc, context, mailboxesCursor.getCount(), defaultAccountId);
738        }
739
740        public AccountsAdapter(Cursor c, Context context, int mailboxesCount,
741                long defaultAccountId) {
742            super(context, c, true);
743            mContext = context;
744            mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
745            mMailboxesCount = mailboxesCount;
746            mSeparatorPosition = mailboxesCount;
747            mDefaultAccountId = defaultAccountId;
748        }
749
750        public boolean isMailbox(int position) {
751            return position < mMailboxesCount;
752        }
753
754        public boolean isAccount(int position) {
755            return position >= mMailboxesCount;
756        }
757
758        public void addOnDeletingAccount(long accountId) {
759            mOnDeletingAccounts.add(accountId);
760        }
761
762        public boolean isOnDeletingAccountView(long accountId) {
763            return mOnDeletingAccounts.contains(accountId);
764        }
765
766        /**
767         * This is used as a callback from the list items, for clicks in the folder "button"
768         *
769         * @param itemView the item in which the click occurred
770         */
771        public void onClickFolder(AccountFolderListItem itemView) {
772            MailboxList.actionHandleAccount(mContext, itemView.mAccountId);
773        }
774
775        @Override
776        public void bindView(View view, Context context, Cursor cursor) {
777            if (cursor.getPosition() < mMailboxesCount) {
778                bindMailboxItem(view, context, cursor, false);
779            } else {
780                bindAccountItem(view, context, cursor, false);
781            }
782        }
783
784        private void bindMailboxItem(View view, Context context, Cursor cursor, boolean isLastChild)
785                {
786            // Reset the view (in case it was recycled) and prepare for binding
787            AccountFolderListItem itemView = (AccountFolderListItem) view;
788            itemView.bindViewInit(this, false);
789
790            // Invisible (not "gone") to maintain spacing
791            view.findViewById(R.id.chip).setVisibility(View.INVISIBLE);
792
793            String text = cursor.getString(MAILBOX_DISPLAY_NAME);
794            if (text != null) {
795                TextView nameView = (TextView) view.findViewById(R.id.name);
796                nameView.setText(text);
797            }
798
799            // TODO get/track live folder status
800            text = null;
801            TextView statusView = (TextView) view.findViewById(R.id.status);
802            if (text != null) {
803                statusView.setText(text);
804                statusView.setVisibility(View.VISIBLE);
805            } else {
806                statusView.setVisibility(View.GONE);
807            }
808
809            int count = -1;
810            text = cursor.getString(MAILBOX_UNREAD_COUNT);
811            if (text != null) {
812                count = Integer.valueOf(text);
813            }
814            TextView unreadCountView = (TextView) view.findViewById(R.id.new_message_count);
815            TextView allCountView = (TextView) view.findViewById(R.id.all_message_count);
816            int id = cursor.getInt(MAILBOX_COLUMN_ID);
817            // If the unread count is zero, not to show countView.
818            if (count > 0) {
819                if (id == Mailbox.QUERY_ALL_FAVORITES
820                        || id == Mailbox.QUERY_ALL_DRAFTS
821                        || id == Mailbox.QUERY_ALL_OUTBOX) {
822                    unreadCountView.setVisibility(View.GONE);
823                    allCountView.setVisibility(View.VISIBLE);
824                    allCountView.setText(text);
825                } else {
826                    allCountView.setVisibility(View.GONE);
827                    unreadCountView.setVisibility(View.VISIBLE);
828                    unreadCountView.setText(text);
829                }
830            } else {
831                allCountView.setVisibility(View.GONE);
832                unreadCountView.setVisibility(View.GONE);
833            }
834
835            view.findViewById(R.id.folder_button).setVisibility(View.GONE);
836            view.findViewById(R.id.folder_separator).setVisibility(View.GONE);
837            view.findViewById(R.id.default_sender).setVisibility(View.GONE);
838            view.findViewById(R.id.folder_icon).setVisibility(View.VISIBLE);
839            ((ImageView)view.findViewById(R.id.folder_icon)).setImageDrawable(
840                    Utility.FolderProperties.getInstance(context).getSummaryMailboxIconIds(id));
841        }
842
843        private void bindAccountItem(View view, Context context, Cursor cursor, boolean isExpanded)
844                {
845            // Reset the view (in case it was recycled) and prepare for binding
846            AccountFolderListItem itemView = (AccountFolderListItem) view;
847            itemView.bindViewInit(this, true);
848            itemView.mAccountId = cursor.getLong(Account.CONTENT_ID_COLUMN);
849
850            long accountId = cursor.getLong(Account.CONTENT_ID_COLUMN);
851            View chipView = view.findViewById(R.id.chip);
852            chipView.setBackgroundResource(Email.getAccountColorResourceId(accountId));
853            chipView.setVisibility(View.VISIBLE);
854
855            String text = cursor.getString(Account.CONTENT_DISPLAY_NAME_COLUMN);
856            if (text != null) {
857                TextView descriptionView = (TextView) view.findViewById(R.id.name);
858                descriptionView.setText(text);
859            }
860
861            text = cursor.getString(Account.CONTENT_EMAIL_ADDRESS_COLUMN);
862            if (text != null) {
863                TextView emailView = (TextView) view.findViewById(R.id.status);
864                emailView.setText(text);
865                emailView.setVisibility(View.VISIBLE);
866            }
867
868            int unreadMessageCount = 0;
869            Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI,
870                    MAILBOX_UNREAD_COUNT_PROJECTION,
871                    MAILBOX_INBOX_SELECTION,
872                    new String[] { String.valueOf(accountId) }, null);
873
874            try {
875                if (c.moveToFirst()) {
876                    String count = c.getString(MAILBOX_UNREAD_COUNT_COLUMN_UNREAD_COUNT);
877                    if (count != null) {
878                        unreadMessageCount = Integer.valueOf(count);
879                    }
880                }
881            } finally {
882                c.close();
883            }
884
885            view.findViewById(R.id.all_message_count).setVisibility(View.GONE);
886            TextView unreadCountView = (TextView) view.findViewById(R.id.new_message_count);
887            if (unreadMessageCount > 0) {
888                unreadCountView.setText(String.valueOf(unreadMessageCount));
889                unreadCountView.setVisibility(View.VISIBLE);
890            } else {
891                unreadCountView.setVisibility(View.GONE);
892            }
893
894            view.findViewById(R.id.folder_icon).setVisibility(View.GONE);
895            view.findViewById(R.id.folder_button).setVisibility(View.VISIBLE);
896            view.findViewById(R.id.folder_separator).setVisibility(View.VISIBLE);
897            if (accountId == mDefaultAccountId) {
898                view.findViewById(R.id.default_sender).setVisibility(View.VISIBLE);
899            } else {
900                view.findViewById(R.id.default_sender).setVisibility(View.GONE);
901            }
902        }
903
904        @Override
905        public View newView(Context context, Cursor cursor, ViewGroup parent) {
906            return mInflater.inflate(R.layout.account_folder_list_item, parent, false);
907        }
908
909        /*
910         * The following series of overrides insert the "Accounts" separator
911         */
912
913        /**
914         * Prevents the separator view from recycling into the other views
915         */
916        @Override
917        public int getItemViewType(int position) {
918            if (position == mSeparatorPosition) {
919                return IGNORE_ITEM_VIEW_TYPE;
920            }
921            return super.getItemViewType(position);
922        }
923
924        /**
925         * Injects the separator view when required, and fudges the cursor for other views
926         */
927        @Override
928        public View getView(int position, View convertView, ViewGroup parent) {
929            // The base class's getView() checks for mDataValid at the beginning, but we don't have
930            // to do that, because if the cursor is invalid getCount() returns 0, in which case this
931            // method wouldn't get called.
932
933            // Handle the separator here - create & bind
934            if (position == mSeparatorPosition) {
935                TextView view;
936                view = (TextView) mInflater.inflate(R.layout.list_separator, parent, false);
937                view.setText(R.string.account_folder_list_separator_accounts);
938                return view;
939            }
940            return super.getView(getRealPosition(position), convertView, parent);
941        }
942
943        /**
944         * Forces navigation to skip over the separator
945         */
946        @Override
947        public boolean areAllItemsEnabled() {
948            return false;
949        }
950
951        /**
952         * Forces navigation to skip over the separator
953         */
954        @Override
955        public boolean isEnabled(int position) {
956            if (position == mSeparatorPosition) {
957                return false;
958            } else if (isAccount(position)) {
959                Long id = ((MergeCursor)getItem(position)).getLong(Account.CONTENT_ID_COLUMN);
960                return !isOnDeletingAccountView(id);
961            } else {
962                return true;
963            }
964        }
965
966        /**
967         * Adjusts list count to include separator
968         */
969        @Override
970        public int getCount() {
971            int count = super.getCount();
972            if (count > 0 && (mSeparatorPosition != ListView.INVALID_POSITION)) {
973                // Increment for separator, if we have anything to show.
974                count += 1;
975            }
976            return count;
977        }
978
979        /**
980         * Converts list position to cursor position
981         */
982        private int getRealPosition(int pos) {
983            if (mSeparatorPosition == ListView.INVALID_POSITION) {
984                // No separator, identity map
985                return pos;
986            } else if (pos <= mSeparatorPosition) {
987                // Before or at the separator, identity map
988                return pos;
989            } else {
990                // After the separator, remove 1 from the pos to get the real underlying pos
991                return pos - 1;
992            }
993        }
994
995        /**
996         * Returns the item using external position numbering (no separator)
997         */
998        @Override
999        public Object getItem(int pos) {
1000            return super.getItem(getRealPosition(pos));
1001        }
1002
1003        /**
1004         * Returns the item id using external position numbering (no separator)
1005         */
1006        @Override
1007        public long getItemId(int pos) {
1008            return super.getItemId(getRealPosition(pos));
1009        }
1010    }
1011}
1012
1013
1014