1/*
2 * Copyright (C) 2010 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 android.content.ContentResolver;
20import android.content.ContentUris;
21import android.content.Context;
22import android.content.Loader;
23import android.database.Cursor;
24import android.database.MatrixCursor;
25import android.view.LayoutInflater;
26import android.view.View;
27import android.view.ViewGroup;
28import android.widget.AdapterView;
29import android.widget.CursorAdapter;
30import android.widget.TextView;
31
32import com.android.email.FolderProperties;
33import com.android.email.R;
34import com.android.email.ResourceHelper;
35import com.android.email.data.ClosingMatrixCursor;
36import com.android.email.data.ThrottlingCursorLoader;
37import com.android.emailcommon.provider.Account;
38import com.android.emailcommon.provider.EmailContent;
39import com.android.emailcommon.provider.EmailContent.AccountColumns;
40import com.android.emailcommon.provider.EmailContent.MailboxColumns;
41import com.android.emailcommon.provider.Mailbox;
42import com.android.emailcommon.utility.Utility;
43import com.google.common.annotations.VisibleForTesting;
44import com.google.common.base.Preconditions;
45
46import java.util.ArrayList;
47import java.util.Collection;
48
49/**
50 * Account selector spinner.
51 *
52 * TODO Test it!
53 */
54public class AccountSelectorAdapter extends CursorAdapter {
55    /** meta data column for an message count (unread or total, depending on row) */
56    private static final String MESSAGE_COUNT = "unreadCount";
57
58    /** meta data column for the row type; used for display purposes */
59    private static final String ROW_TYPE = "rowType";
60
61    /** meta data position of the currently selected account in the drop-down list */
62    private static final String ACCOUNT_POSITION = "accountPosition";
63
64    /** "account id" virtual column name for the matrix cursor */
65    private static final String ACCOUNT_ID = "accountId";
66
67    private static final int ROW_TYPE_HEADER = AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
68    @SuppressWarnings("unused")
69    private static final int ROW_TYPE_MAILBOX = 0;
70    private static final int ROW_TYPE_ACCOUNT = 1;
71    private static final int ITEM_VIEW_TYPE_ACCOUNT = 0;
72    static final int UNKNOWN_POSITION = -1;
73    /** Projection for account database query */
74    private static final String[] ACCOUNT_PROJECTION = new String[] {
75        EmailContent.RECORD_ID,
76        Account.DISPLAY_NAME,
77        Account.EMAIL_ADDRESS,
78    };
79    /**
80     * Projection used for the selector display; we add meta data that doesn't exist in the
81     * account database, so, this should be a super-set of {@link #ACCOUNT_PROJECTION}.
82     */
83    private static final String[] ADAPTER_PROJECTION = new String[] {
84        ROW_TYPE,
85        EmailContent.RECORD_ID,
86        Account.DISPLAY_NAME,
87        Account.EMAIL_ADDRESS,
88        MESSAGE_COUNT,
89        ACCOUNT_POSITION, // TODO Probably we don't really need this
90        ACCOUNT_ID,
91    };
92
93    /** Sort order.  Show the default account first. */
94    private static final String ORDER_BY = Account.IS_DEFAULT + " desc, " + Account.RECORD_ID;
95
96    @SuppressWarnings("hiding")
97    private final Context mContext;
98    private final LayoutInflater mInflater;
99    private final ResourceHelper mResourceHelper;
100
101    /**
102     * Returns a loader that can populate the account spinner.
103     * @param context a context
104     * @param accountId the ID of the currently viewed account
105     */
106    public static Loader<Cursor> createLoader(Context context, long accountId, long mailboxId) {
107        return new AccountsLoader(context, accountId, mailboxId, UiUtilities.useTwoPane(context));
108    }
109
110    public AccountSelectorAdapter(Context context) {
111        super(context, null, 0 /* no auto-requery */);
112        mContext = context;
113        mInflater = LayoutInflater.from(context);
114        mResourceHelper = ResourceHelper.getInstance(context);
115    }
116
117    /**
118     * {@inheritDoc}
119     *
120     * The account selector view can contain one of four types of row data:
121     * <ol>
122     * <li>headers</li>
123     * <li>accounts</li>
124     * <li>recent mailboxes</li>
125     * <li>"show all folders"</li>
126     * </ol>
127     * Headers are handled separately as they have a unique layout and cannot be interacted with.
128     * Accounts, recent mailboxes and "show all folders" all have the same interaction model and
129     * share a very similar layout. The single difference is that both accounts and recent
130     * mailboxes display an unread count; whereas "show all folders" does not. To determine
131     * if a particular row is "show all folders" verify that a) it's not an account row and
132     * b) it's ID is {@link Mailbox#NO_MAILBOX}.
133     *
134     * TODO Use recycled views.  ({@link #getViewTypeCount} and {@link #getItemViewType})
135     */
136    @Override
137    public View getView(int position, View convertView, ViewGroup parent) {
138        Cursor c = getCursor();
139        c.moveToPosition(position);
140        View view;
141        if (c.getInt(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_HEADER) {
142            view = mInflater.inflate(R.layout.action_bar_spinner_dropdown_header, parent, false);
143            final TextView displayNameView = (TextView) view.findViewById(R.id.display_name);
144            final String displayName = getDisplayName(c);
145            displayNameView.setText(displayName);
146        } else {
147            view = mInflater.inflate(R.layout.action_bar_spinner_dropdown, parent, false);
148            final TextView displayNameView = (TextView) view.findViewById(R.id.display_name);
149            final TextView emailAddressView = (TextView) view.findViewById(R.id.email_address);
150            final TextView unreadCountView = (TextView) view.findViewById(R.id.unread_count);
151            final View chipView = view.findViewById(R.id.color_chip);
152
153            final String displayName = getDisplayName(c);
154            final String emailAddress = getAccountEmailAddress(c);
155
156            displayNameView.setText(displayName);
157
158            // Show the email address only when it's different from the display name.
159            boolean isAccount = isAccountItem(c);
160            if (displayName.equals(emailAddress) || !isAccount) {
161                emailAddressView.setVisibility(View.GONE);
162            } else {
163                emailAddressView.setVisibility(View.VISIBLE);
164                emailAddressView.setText(emailAddress);
165            }
166
167            long id = getId(c);
168            if (isAccount || id != Mailbox.NO_MAILBOX) {
169                unreadCountView.setVisibility(View.VISIBLE);
170                unreadCountView.setText(UiUtilities.getMessageCountForUi(mContext,
171                        getAccountUnreadCount(c), true));
172
173                // If we're on a combined account, show the color chip indicators for all real
174                // accounts so it can be used as a legend.
175                boolean isCombinedActive =
176                        ((CursorWithExtras) c).getAccountId() == Account.ACCOUNT_ID_COMBINED_VIEW;
177
178                if (isCombinedActive && Account.isNormalAccount(id)) {
179                    chipView.setBackgroundColor(mResourceHelper.getAccountColor(id));
180                    chipView.setVisibility(View.VISIBLE);
181                } else {
182                    chipView.setVisibility(View.GONE);
183                }
184            } else {
185                unreadCountView.setVisibility(View.INVISIBLE);
186                chipView.setVisibility(View.GONE);
187            }
188
189        }
190        return view;
191    }
192
193    @Override
194    public View newView(Context context, Cursor cursor, ViewGroup parent) {
195        return null; // we don't reuse views.  This method never gets called.
196    }
197
198    @Override
199    public void bindView(View view, Context context, Cursor cursor) {
200        // we don't reuse views.  This method never gets called.
201    }
202
203    @Override
204    public int getViewTypeCount() {
205        return 2;
206    }
207
208    @Override
209    public int getItemViewType(int position) {
210        Cursor c = getCursor();
211        c.moveToPosition(position);
212        return c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_HEADER
213                ? AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER
214                : ITEM_VIEW_TYPE_ACCOUNT;
215    }
216
217    @Override
218    public boolean areAllItemsEnabled() {
219        return false;
220    }
221
222    @Override
223    public boolean isEnabled(int position) {
224        return (getItemViewType(position) != AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER);
225    }
226
227    public boolean isAccountItem(int position) {
228        Cursor c = getCursor();
229        c.moveToPosition(position);
230        return isAccountItem(c);
231    }
232
233    public boolean isAccountItem(Cursor c) {
234        return (c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_ACCOUNT);
235    }
236
237    public boolean isMailboxItem(int position) {
238        Cursor c = getCursor();
239        c.moveToPosition(position);
240        return (c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_MAILBOX);
241    }
242
243    private int getAccountUnreadCount(Cursor c) {
244        return getMessageCount(c);
245    }
246
247    /**
248     * Returns the account/mailbox ID extracted from the given cursor.
249     */
250    private static long getId(Cursor c) {
251        return c.getLong(c.getColumnIndex(EmailContent.RECORD_ID));
252    }
253
254    /**
255     * @return ID of the account / mailbox for a row
256     */
257    public long getId(int position) {
258        final Cursor c = getCursor();
259        return c.moveToPosition(position) ? getId(c) : Account.NO_ACCOUNT;
260    }
261
262    /**
263     * @return ID of the account for a row
264     */
265    public long getAccountId(int position) {
266        final Cursor c = getCursor();
267        return c.moveToPosition(position)
268                ? c.getLong(c.getColumnIndex(ACCOUNT_ID))
269                : Account.NO_ACCOUNT;
270    }
271
272    /** Returns the account name extracted from the given cursor. */
273    static String getDisplayName(Cursor cursor) {
274        return cursor.getString(cursor.getColumnIndex(Account.DISPLAY_NAME));
275    }
276
277    /** Returns the email address extracted from the given cursor. */
278    private static String getAccountEmailAddress(Cursor cursor) {
279        return cursor.getString(cursor.getColumnIndex(Account.EMAIL_ADDRESS));
280    }
281
282    /**
283     * Returns the message count (unread or total, depending on row) extracted from the given
284     * cursor.
285     */
286    private static int getMessageCount(Cursor cursor) {
287        return cursor.getInt(cursor.getColumnIndex(MESSAGE_COUNT));
288    }
289
290    private static String sCombinedViewDisplayName;
291    private static String getCombinedViewDisplayName(Context c) {
292        if (sCombinedViewDisplayName == null) {
293            sCombinedViewDisplayName = c.getResources().getString(
294                    R.string.mailbox_list_account_selector_combined_view);
295        }
296        return sCombinedViewDisplayName;
297    }
298
299    /**
300     * Load the account list.  The resulting cursor contains
301     * - Account info
302     * - # of unread messages in inbox
303     * - The "Combined view" row if there's more than one account.
304     */
305    @VisibleForTesting
306    static class AccountsLoader extends ThrottlingCursorLoader {
307        private final Context mContext;
308        private final long mAccountId;
309        private final long mMailboxId;
310        private final boolean mUseTwoPane; // Injectable for test
311        private final FolderProperties mFolderProperties;
312
313        @VisibleForTesting
314        AccountsLoader(Context context, long accountId, long mailboxId, boolean useTwoPane) {
315            // Super class loads a regular account cursor, but we replace it in loadInBackground().
316            super(context, Account.CONTENT_URI, ACCOUNT_PROJECTION, null, null,
317                    ORDER_BY);
318            mContext = context;
319            mAccountId = accountId;
320            mMailboxId = mailboxId;
321            mFolderProperties = FolderProperties.getInstance(mContext);
322            mUseTwoPane = useTwoPane;
323        }
324
325        @Override
326        public Cursor loadInBackground() {
327            final Cursor accountsCursor = super.loadInBackground();
328            // Use ClosingMatrixCursor so that accountsCursor gets closed too when it's closed.
329            final CursorWithExtras resultCursor
330                    = new CursorWithExtras(ADAPTER_PROJECTION, accountsCursor);
331            final int accountPosition = addAccountsToCursor(resultCursor, accountsCursor);
332            addMailboxesToCursor(resultCursor, accountPosition);
333
334            resultCursor.setAccountMailboxInfo(getContext(), mAccountId, mMailboxId);
335            return resultCursor;
336        }
337
338        /** Adds the account list [with extra meta data] to the given matrix cursor */
339        private int addAccountsToCursor(CursorWithExtras matrixCursor, Cursor accountCursor) {
340            int accountPosition = UNKNOWN_POSITION;
341            accountCursor.moveToPosition(-1);
342
343            matrixCursor.mAccountCount = accountCursor.getCount();
344            int totalUnread = 0;
345            while (accountCursor.moveToNext()) {
346                // Add account, with its unread count.
347                final long accountId = accountCursor.getLong(0);
348                final int unread = Mailbox.getUnreadCountByAccountAndMailboxType(
349                        mContext, accountId, Mailbox.TYPE_INBOX);
350                final String name = getDisplayName(accountCursor);
351                final String emailAddress = getAccountEmailAddress(accountCursor);
352                addRow(matrixCursor, ROW_TYPE_ACCOUNT, accountId, name, emailAddress, unread,
353                    UNKNOWN_POSITION, accountId);
354                totalUnread += unread;
355                if (accountId == mAccountId) {
356                    accountPosition = accountCursor.getPosition();
357                }
358            }
359            // Add "combined view" if more than one account exists
360            final int countAccounts = accountCursor.getCount();
361            if (countAccounts > 1) {
362                final String accountCount = mContext.getResources().getQuantityString(
363                        R.plurals.number_of_accounts, countAccounts, countAccounts);
364                addRow(matrixCursor, ROW_TYPE_ACCOUNT, Account.ACCOUNT_ID_COMBINED_VIEW,
365                        getCombinedViewDisplayName(mContext),
366                        accountCount, totalUnread, UNKNOWN_POSITION,
367                        Account.ACCOUNT_ID_COMBINED_VIEW);
368
369                // Increment the account count for the combined account.
370                matrixCursor.mAccountCount++;
371            }
372            return accountPosition;
373        }
374
375        /**
376         * Adds the recent mailbox list / "show all folders" to the given cursor.
377         *
378         * @param matrixCursor the cursor to add the list to
379         * @param accountPosition the cursor position of the currently selected account
380         */
381        private void addMailboxesToCursor(CursorWithExtras matrixCursor, int accountPosition) {
382            if (mAccountId == Account.NO_ACCOUNT) {
383                return; // Account not selected
384            }
385            if (mAccountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
386                if (!mUseTwoPane) {
387                    // TODO We may want a header for this to separate it from the account list
388                    addShowAllFoldersRow(matrixCursor, accountPosition);
389                }
390                return;
391            }
392            String emailAddress = null;
393            if (accountPosition != UNKNOWN_POSITION) {
394                matrixCursor.moveToPosition(accountPosition);
395                emailAddress =
396                        matrixCursor.getString(matrixCursor.getColumnIndex(Account.EMAIL_ADDRESS));
397            }
398            RecentMailboxManager mailboxManager = RecentMailboxManager.getInstance(mContext);
399            ArrayList<Long> recentMailboxes = null;
400            if (!mUseTwoPane) {
401                // Do not display recent mailboxes in the account spinner for the two pane view
402                recentMailboxes = mailboxManager.getMostRecent(mAccountId, mUseTwoPane);
403            }
404            final int recentCount = (recentMailboxes == null) ? 0 : recentMailboxes.size();
405            matrixCursor.mRecentCount = recentCount;
406
407            if (!mUseTwoPane) {
408                // "Recent mailboxes" header
409                addHeaderRow(matrixCursor, mContext.getString(
410                        R.string.mailbox_list_account_selector_mailbox_header_fmt, emailAddress));
411            }
412
413            if (recentCount > 0) {
414                addMailboxRows(matrixCursor, accountPosition, recentMailboxes);
415            }
416
417            if (!mUseTwoPane) {
418                addShowAllFoldersRow(matrixCursor, accountPosition);
419            }
420        }
421
422        private void addShowAllFoldersRow(CursorWithExtras matrixCursor, int accountPosition) {
423            matrixCursor.mHasShowAllFolders = true;
424            String name = mContext.getString(
425                    R.string.mailbox_list_account_selector_show_all_folders);
426            addRow(matrixCursor, ROW_TYPE_MAILBOX, Mailbox.NO_MAILBOX, name, null, 0,
427                    accountPosition, mAccountId);
428        }
429
430
431        private static final String[] RECENT_MAILBOX_INFO_PROJECTION = new String[] {
432            MailboxColumns.ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE,
433            MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT
434        };
435
436        private void addMailboxRows(MatrixCursor matrixCursor, int accountPosition,
437                Collection<Long> mailboxIds) {
438            Cursor c = mContext.getContentResolver().query(
439                    Mailbox.CONTENT_URI, RECENT_MAILBOX_INFO_PROJECTION,
440                    Utility.buildInSelection(MailboxColumns.ID, mailboxIds), null,
441                    RecentMailboxManager.RECENT_MAILBOXES_SORT_ORDER);
442            try {
443                c.moveToPosition(-1);
444                while (c.moveToNext()) {
445                    addRow(matrixCursor, ROW_TYPE_MAILBOX,
446                            c.getLong(c.getColumnIndex(MailboxColumns.ID)),
447                            mFolderProperties.getDisplayName(c), null,
448                            mFolderProperties.getMessageCount(c), accountPosition, mAccountId);
449                }
450            } finally {
451                c.close();
452            }
453        }
454
455        private void addHeaderRow(MatrixCursor cursor, String name) {
456            addRow(cursor, ROW_TYPE_HEADER, 0L, name, null, 0, UNKNOWN_POSITION,
457                    Account.NO_ACCOUNT);
458        }
459
460        /** Adds a row to the given cursor */
461        private void addRow(MatrixCursor cursor, int rowType, long id, String name,
462                String emailAddress, int messageCount, int listPosition, long accountId) {
463            cursor.newRow()
464                .add(rowType)
465                .add(id)
466                .add(name)
467                .add(emailAddress)
468                .add(messageCount)
469                .add(listPosition)
470                .add(accountId);
471        }
472    }
473
474    /** Cursor with some extra meta data. */
475    static class CursorWithExtras extends ClosingMatrixCursor {
476
477        /** Number of account elements, including the combined account row. */
478        private int mAccountCount;
479        /** Number of recent mailbox elements */
480        private int mRecentCount;
481        private boolean mHasShowAllFolders;
482
483        private boolean mAccountExists;
484
485        /**
486         * Account ID that's loaded.
487         */
488        private long mAccountId;
489        private String mAccountDisplayName;
490
491        /**
492         * Mailbox ID that's loaded.
493         */
494        private long mMailboxId;
495        private String mMailboxDisplayName;
496        private int mMailboxMessageCount;
497
498        @VisibleForTesting
499        CursorWithExtras(String[] columnNames, Cursor innerCursor) {
500            super(columnNames, innerCursor);
501        }
502
503        private static final String[] ACCOUNT_INFO_PROJECTION = new String[] {
504            AccountColumns.DISPLAY_NAME,
505        };
506        private static final String[] MAILBOX_INFO_PROJECTION = new String[] {
507            MailboxColumns.ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE,
508            MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT
509        };
510
511        /**
512         * Set the current account/mailbox info.
513         */
514        @VisibleForTesting
515        void setAccountMailboxInfo(Context context, long accountId, long mailboxId) {
516            mAccountId = accountId;
517            mMailboxId = mailboxId;
518
519            // Get account info
520            if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
521                // We need to treat ACCOUNT_ID_COMBINED_VIEW specially...
522                mAccountExists = true;
523                mAccountDisplayName = getCombinedViewDisplayName(context);
524                if (mailboxId != Mailbox.NO_MAILBOX) {
525                    setCombinedMailboxInfo(context, mailboxId);
526                }
527                return;
528            }
529
530            mAccountDisplayName = Utility.getFirstRowString(context,
531                    ContentUris.withAppendedId(Account.CONTENT_URI, accountId),
532                    ACCOUNT_INFO_PROJECTION, null, null, null, 0, null);
533            if (mAccountDisplayName == null) {
534                // Account gone!
535                mAccountExists = false;
536                return;
537            }
538            mAccountExists = true;
539
540            // If mailbox not specified, done.
541            if (mMailboxId == Mailbox.NO_MAILBOX) {
542                return;
543            }
544            // Combined mailbox?
545            // Unfortunately this can happen even when account != ACCOUNT_ID_COMBINED_VIEW,
546            // when you open "starred" on 2-pane on non-combined view.
547            if (mMailboxId < 0) {
548                setCombinedMailboxInfo(context, mailboxId);
549                return;
550            }
551
552            // Get mailbox info
553            final ContentResolver r = context.getContentResolver();
554            final Cursor mailboxCursor = r.query(
555                    ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
556                    MAILBOX_INFO_PROJECTION, null, null, null);
557            try {
558                if (mailboxCursor.moveToFirst()) {
559                    final FolderProperties fp = FolderProperties.getInstance(context);
560                    mMailboxDisplayName = fp.getDisplayName(mailboxCursor);
561                    mMailboxMessageCount = fp.getMessageCount(mailboxCursor);
562                }
563            } finally {
564                mailboxCursor.close();
565            }
566        }
567
568        private void setCombinedMailboxInfo(Context context, long mailboxId) {
569            Preconditions.checkState(mailboxId < -1, "Not combined mailbox");
570            mMailboxDisplayName = FolderProperties.getInstance(context)
571                    .getCombinedMailboxName(mMailboxId);
572
573            mMailboxMessageCount = FolderProperties.getMessageCountForCombinedMailbox(
574                    context, mailboxId);
575        }
576
577        /**
578         * Returns the cursor position of the item with the given ID. Or {@link #UNKNOWN_POSITION}
579         * if the given ID does not exist.
580         */
581        int getPosition(long id) {
582            moveToPosition(-1);
583            while(moveToNext()) {
584                if (id == getId(this)) {
585                    return getPosition();
586                }
587            }
588            return UNKNOWN_POSITION;
589        }
590
591        public int getAccountCount() {
592            return mAccountCount;
593        }
594
595        @VisibleForTesting
596        public int getRecentMailboxCount() {
597            return mRecentCount;
598        }
599
600        /**
601         * @return true if the cursor has more than one selectable item so we should enable the
602         *     spinner.
603         */
604        public boolean shouldEnableSpinner() {
605            return mHasShowAllFolders || (mAccountCount + mRecentCount > 1);
606        }
607
608        public long getAccountId() {
609            return mAccountId;
610        }
611
612        public String getAccountDisplayName() {
613            return mAccountDisplayName;
614        }
615
616        @VisibleForTesting
617        public long getMailboxId() {
618            return mMailboxId;
619        }
620
621        public String getMailboxDisplayName() {
622            return mMailboxDisplayName;
623        }
624
625        public int getMailboxMessageCount() {
626            return mMailboxMessageCount;
627        }
628
629        /**
630         * @return {@code true} if the specified accuont exists.
631         */
632        public boolean accountExists() {
633            return mAccountExists;
634        }
635    }
636}
637