Home | History | Annotate | Download | only in activity
      1 /*
      2  * Copyright (C) 2011 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 
     17 package com.android.email.activity;
     18 
     19 import com.android.email.R;
     20 import com.android.emailcommon.provider.Account;
     21 import com.android.emailcommon.provider.EmailContent.AccountColumns;
     22 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
     23 import com.android.emailcommon.provider.HostAuth;
     24 import com.android.emailcommon.provider.Mailbox;
     25 
     26 import android.app.Activity;
     27 import android.app.FragmentTransaction;
     28 import android.app.ListFragment;
     29 import android.app.LoaderManager.LoaderCallbacks;
     30 import android.content.ContentValues;
     31 import android.content.Context;
     32 import android.content.CursorLoader;
     33 import android.content.Loader;
     34 import android.content.res.Resources;
     35 import android.database.Cursor;
     36 import android.database.MatrixCursor;
     37 import android.database.MatrixCursor.RowBuilder;
     38 import android.database.MergeCursor;
     39 import android.net.Uri;
     40 import android.os.Bundle;
     41 import android.view.View;
     42 import android.widget.AdapterView;
     43 import android.widget.AdapterView.OnItemClickListener;
     44 import android.widget.ListView;
     45 import android.widget.SimpleCursorAdapter;
     46 
     47 /**
     48  * Fragment containing a list of accounts to show during shortcut creation.
     49  * <p>
     50  * NOTE: In order to receive callbacks, the activity containing this fragment must implement
     51  * the {@link PickerCallback} interface.
     52  */
     53 public abstract class ShortcutPickerFragment extends ListFragment
     54         implements OnItemClickListener, LoaderCallbacks<Cursor> {
     55     /** Callback methods. Enclosing activities must implement to receive fragment notifications. */
     56     public static interface PickerCallback {
     57         /** Builds a mailbox filter for the given account. See MailboxShortcutPickerFragment. */
     58         public Integer buildFilter(Account account);
     59         /** Invoked when an account and mailbox have been selected. */
     60         public void onSelected(Account account, long mailboxId);
     61         /** Required data is missing; either the account and/or mailbox */
     62         public void onMissingData(boolean missingAccount, boolean missingMailbox);
     63     }
     64 
     65     /** A no-op callback */
     66     private final PickerCallback EMPTY_CALLBACK = new PickerCallback() {
     67         @Override public Integer buildFilter(Account account) { return null; }
     68         @Override public void onSelected(Account account, long mailboxId){ getActivity().finish(); }
     69         @Override public void onMissingData(boolean missingAccount, boolean missingMailbox) { }
     70     };
     71     private final static int LOADER_ID = 0;
     72     private final static int[] TO_VIEWS = new int[] {
     73         android.R.id.text1,
     74     };
     75 
     76     PickerCallback mCallback = EMPTY_CALLBACK;
     77     /** Cursor adapter that provides either the account or mailbox list */
     78     private SimpleCursorAdapter mAdapter;
     79 
     80     @Override
     81     public void onAttach(Activity activity) {
     82         super.onAttach(activity);
     83 
     84         if (activity instanceof PickerCallback) {
     85             mCallback = (PickerCallback) activity;
     86         }
     87         final String[] fromColumns = getFromColumns();
     88         mAdapter = new SimpleCursorAdapter(activity,
     89             android.R.layout.simple_expandable_list_item_1, null, fromColumns, TO_VIEWS, 0);
     90         setListAdapter(mAdapter);
     91 
     92         getLoaderManager().initLoader(LOADER_ID, null, this);
     93     }
     94 
     95     @Override
     96     public void onActivityCreated(Bundle savedInstanceState) {
     97         super.onActivityCreated(savedInstanceState);
     98 
     99         ListView listView = getListView();
    100         listView.setOnItemClickListener(this);
    101         listView.setItemsCanFocus(false);
    102     }
    103 
    104     @Override
    105     public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    106         mAdapter.swapCursor(data);
    107     }
    108 
    109     @Override
    110     public void onLoaderReset(Loader<Cursor> loader) {
    111         mAdapter.swapCursor(null);
    112     }
    113 
    114     /** Returns the cursor columns to map into list */
    115     abstract String[] getFromColumns();
    116 
    117     // TODO if we add meta-accounts to the database, remove this class entirely
    118     private static final class AccountPickerLoader extends CursorLoader {
    119         public AccountPickerLoader(Context context, Uri uri, String[] projection, String selection,
    120                 String[] selectionArgs, String sortOrder) {
    121             super(context, uri, projection, selection, selectionArgs, sortOrder);
    122         }
    123 
    124         @Override
    125         public Cursor loadInBackground() {
    126             Cursor parentCursor = super.loadInBackground();
    127             int cursorCount = parentCursor.getCount();
    128             final Cursor returnCursor;
    129 
    130             if (cursorCount > 1) {
    131                 // Only add "All accounts" if there is more than 1 account defined
    132                 MatrixCursor allAccountCursor = new MatrixCursor(getProjection());
    133                 addCombinedAccountRow(allAccountCursor, cursorCount);
    134                 returnCursor = new MergeCursor(new Cursor[] { allAccountCursor, parentCursor });
    135             } else {
    136                 returnCursor = parentCursor;
    137             }
    138             return returnCursor;
    139         }
    140 
    141         /** Adds a row for "All Accounts" into the given cursor */
    142         private void addCombinedAccountRow(MatrixCursor cursor, int accountCount) {
    143             Context context = getContext();
    144             Account account = new Account();
    145             account.mId = Account.ACCOUNT_ID_COMBINED_VIEW;
    146             Resources res = context.getResources();
    147             String countString = res.getQuantityString(R.plurals.picker_combined_view_account_count,
    148                     accountCount, accountCount);
    149             account.mDisplayName = res.getString(R.string.picker_combined_view_fmt, countString);
    150             ContentValues values = account.toContentValues();
    151             RowBuilder row = cursor.newRow();
    152             for (String rowName : cursor.getColumnNames()) {
    153                 // special case some of the rows ...
    154                 if (AccountColumns.ID.equals(rowName)) {
    155                     row.add(Account.ACCOUNT_ID_COMBINED_VIEW);
    156                     continue;
    157                 } else if (AccountColumns.IS_DEFAULT.equals(rowName)) {
    158                     row.add(0);
    159                     continue;
    160                 }
    161                 row.add(values.get(rowName));
    162             }
    163         }
    164     }
    165 
    166     /** Account picker */
    167     public static class AccountShortcutPickerFragment extends ShortcutPickerFragment {
    168         private volatile Boolean mLoadFinished = new Boolean(false);
    169         private final static String[] ACCOUNT_FROM_COLUMNS = new String[] {
    170             AccountColumns.DISPLAY_NAME,
    171         };
    172 
    173         @Override
    174         public void onActivityCreated(Bundle savedInstanceState) {
    175             super.onActivityCreated(savedInstanceState);
    176             getActivity().setTitle(R.string.account_shortcut_picker_title);
    177             if (!mLoadFinished) {
    178                 getActivity().setVisible(false);
    179             }
    180         }
    181 
    182         @Override
    183         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    184             Cursor cursor = (Cursor) parent.getItemAtPosition(position);
    185             selectAccountCursor(cursor, true);
    186         }
    187 
    188         @Override
    189         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    190             Context context = getActivity();
    191             return new AccountPickerLoader(
    192                 context, Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null, null);
    193         }
    194 
    195         @Override
    196         public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    197             // if there is only one account, auto-select it
    198             // No accounts; close the dialog
    199             if (data.getCount() == 0) {
    200                 mCallback.onMissingData(true, false);
    201                 return;
    202             }
    203             if (data.getCount() == 1 && data.moveToFirst()) {
    204                 selectAccountCursor(data, false);
    205                 return;
    206             }
    207             super.onLoadFinished(loader, data);
    208             mLoadFinished = true;
    209             getActivity().setVisible(true);
    210         }
    211 
    212         @Override
    213         String[] getFromColumns() {
    214             return ACCOUNT_FROM_COLUMNS;
    215         }
    216 
    217         /** Selects the account specified by the given cursor */
    218         private void selectAccountCursor(Cursor cursor, boolean allowBack) {
    219             Account account = new Account();
    220             account.restore(cursor);
    221             ShortcutPickerFragment fragment = MailboxShortcutPickerFragment.newInstance(
    222                     getActivity(), account, mCallback.buildFilter(account));
    223             FragmentTransaction transaction = getFragmentManager().beginTransaction();
    224             transaction.replace(R.id.shortcut_list, fragment);
    225             if (allowBack) {
    226                 transaction.addToBackStack(null);
    227             }
    228             transaction.commitAllowingStateLoss();
    229         }
    230     }
    231 
    232     // TODO if we add meta-mailboxes to the database, remove this class entirely
    233     private static final class MailboxPickerLoader extends CursorLoader {
    234         private final long mAccountId;
    235         private final boolean mAllowUnread;
    236         public MailboxPickerLoader(Context context, Uri uri, String[] projection, String selection,
    237                 String[] selectionArgs, String sortOrder, long accountId, boolean allowUnread) {
    238             super(context, uri, projection, selection, selectionArgs, sortOrder);
    239             mAccountId = accountId;
    240             mAllowUnread = allowUnread;
    241         }
    242 
    243         @Override
    244         public Cursor loadInBackground() {
    245             MatrixCursor unreadCursor =
    246                     new MatrixCursor(MailboxShortcutPickerFragment.MATRIX_PROJECTION);
    247             Context context = getContext();
    248             if (mAllowUnread) {
    249                 // For the special mailboxes, their ID is < 0. The UI list does not deal with
    250                 // negative values very well, so, add MAX_VALUE to ensure they're positive, but,
    251                 // don't clash with legitimate mailboxes.
    252                 String mailboxName = context.getString(R.string.picker_mailbox_name_all_unread);
    253                 unreadCursor.addRow(
    254                         new Object[] {
    255                             Integer.MAX_VALUE + Mailbox.QUERY_ALL_UNREAD,
    256                             Mailbox.QUERY_ALL_UNREAD,
    257                             mailboxName,
    258                         });
    259             }
    260 
    261             if (mAccountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
    262                 // Do something special for the "combined" view
    263                 MatrixCursor combinedMailboxesCursor =
    264                         new MatrixCursor(MailboxShortcutPickerFragment.MATRIX_PROJECTION);
    265                 // For the special mailboxes, their ID is < 0. The UI list does not deal with
    266                 // negative values very well, so, add MAX_VALUE to ensure they're positive, but,
    267                 // don't clash with legitimate mailboxes.
    268                 String mailboxName = context.getString(R.string.picker_mailbox_name_all_inbox);
    269                 combinedMailboxesCursor.addRow(
    270                         new Object[] {
    271                             Integer.MAX_VALUE + Mailbox.QUERY_ALL_INBOXES,
    272                             Mailbox.QUERY_ALL_INBOXES,
    273                             mailboxName
    274                         });
    275                 return new MergeCursor(new Cursor[] { combinedMailboxesCursor, unreadCursor });
    276             }
    277 
    278             // Loading for a regular account; perform a normal load
    279             return new MergeCursor(new Cursor[] { super.loadInBackground(), unreadCursor });
    280         }
    281     }
    282 
    283     /** Mailbox picker */
    284     public static class MailboxShortcutPickerFragment extends ShortcutPickerFragment {
    285         /** Allow all mailboxes in the mailbox list */
    286         public static int FILTER_ALLOW_ALL    = 0;
    287         /** Only allow an account's INBOX */
    288         public static int FILTER_INBOX_ONLY   = 1 << 0;
    289         /** Allow an "unread" mailbox; this is not affected by {@link #FILTER_INBOX_ONLY} */
    290         public static int FILTER_ALLOW_UNREAD = 1 << 1;
    291         /** Fragment argument to set filter values */
    292         static final String ARG_FILTER  = "MailboxShortcutPickerFragment.filter";
    293         static final String ARG_ACCOUNT = "MailboxShortcutPickerFragment.account";
    294 
    295         private final static String REAL_ID = "realId";
    296         private final static String[] MAILBOX_FROM_COLUMNS = new String[] {
    297             MailboxColumns.DISPLAY_NAME,
    298         };
    299         /** Loader projection used for IMAP & POP3 accounts */
    300         private final static String[] IMAP_PROJECTION = new String [] {
    301             MailboxColumns.ID, MailboxColumns.ID + " as " + REAL_ID,
    302             MailboxColumns.SERVER_ID + " as " + MailboxColumns.DISPLAY_NAME
    303         };
    304         /** Loader projection used for EAS accounts */
    305         private final static String[] EAS_PROJECTION = new String [] {
    306             MailboxColumns.ID, MailboxColumns.ID + " as " + REAL_ID,
    307             MailboxColumns.DISPLAY_NAME
    308         };
    309         /** Loader projection used for a matrix cursor */
    310         private final static String[] MATRIX_PROJECTION = new String [] {
    311             MailboxColumns.ID, REAL_ID, MailboxColumns.DISPLAY_NAME
    312         };
    313         // TODO #ALL_MAILBOX_SELECTION is identical to MailboxesAdapter#ALL_MAILBOX_SELECTION;
    314         // create a common selection. Move this to the Mailbox class?
    315         /** Selection for all visible mailboxes for an account */
    316         private final static String ALL_MAILBOX_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?" +
    317                 " AND " + Mailbox.USER_VISIBLE_MAILBOX_SELECTION;
    318         /** Selection for just the INBOX of an account */
    319         private final static String INBOX_ONLY_SELECTION = ALL_MAILBOX_SELECTION +
    320                     " AND " + MailboxColumns.TYPE + " = " + Mailbox.TYPE_INBOX;
    321         private volatile Boolean mLoadFinished = new Boolean(false);
    322         /** The currently selected account */
    323         private Account mAccount;
    324         /** The filter values; default to allow all mailboxes */
    325         private Integer mFilter;
    326 
    327         /**
    328          * Builds a mailbox shortcut picker for the given account.
    329          */
    330         public static MailboxShortcutPickerFragment newInstance(
    331                 Context context, Account account, Integer filter) {
    332 
    333             MailboxShortcutPickerFragment fragment = new MailboxShortcutPickerFragment();
    334             Bundle args = new Bundle();
    335             args.putParcelable(ARG_ACCOUNT, account);
    336             args.putInt(ARG_FILTER, filter);
    337             fragment.setArguments(args);
    338             return fragment;
    339         }
    340 
    341         /** Returns the mailbox filter */
    342         int getFilter() {
    343             if (mFilter == null) {
    344                 mFilter = getArguments().getInt(ARG_FILTER, FILTER_ALLOW_ALL);
    345             }
    346             return mFilter;
    347         }
    348 
    349         @Override
    350         public void onAttach(Activity activity) {
    351             // Need to setup the account first thing
    352             mAccount = getArguments().getParcelable(ARG_ACCOUNT);
    353             super.onAttach(activity);
    354         }
    355 
    356         @Override
    357         public void onActivityCreated(Bundle savedInstanceState) {
    358             super.onActivityCreated(savedInstanceState);
    359             getActivity().setTitle(R.string.mailbox_shortcut_picker_title);
    360             if (!mLoadFinished) {
    361                 getActivity().setVisible(false);
    362             }
    363         }
    364 
    365         @Override
    366         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    367             Cursor cursor = (Cursor) parent.getItemAtPosition(position);
    368             long mailboxId = cursor.getLong(cursor.getColumnIndex(REAL_ID));
    369             mCallback.onSelected(mAccount, mailboxId);
    370         }
    371 
    372         @Override
    373         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    374             Context context = getActivity();
    375             // TODO Create a fully-qualified path name for Exchange accounts [code should also work
    376             //      for MoveMessageToDialog.java]
    377             HostAuth recvAuth = mAccount.getOrCreateHostAuthRecv(context);
    378             final String[] projection;
    379             final String orderBy;
    380             final String selection;
    381             if (recvAuth.isEasConnection()) {
    382                 projection = EAS_PROJECTION;
    383                 orderBy = MailboxColumns.DISPLAY_NAME;
    384             } else {
    385                 projection = IMAP_PROJECTION;
    386                 orderBy = MailboxColumns.SERVER_ID;
    387             }
    388             if ((getFilter() & FILTER_INBOX_ONLY) == 0) {
    389                 selection = ALL_MAILBOX_SELECTION;
    390             } else {
    391                 selection = INBOX_ONLY_SELECTION;
    392             }
    393             return new MailboxPickerLoader(
    394                 context, Mailbox.CONTENT_URI, projection, selection,
    395                 new String[] { Long.toString(mAccount.mId) }, orderBy, mAccount.mId,
    396                 (getFilter() & FILTER_ALLOW_UNREAD) != 0);
    397         }
    398 
    399         @Override
    400         public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    401             // No accounts; close the dialog
    402             if (data.getCount() == 0) {
    403                 mCallback.onMissingData(false, true);
    404                 return;
    405             }
    406             // if there is only one mailbox, auto-select it
    407             if (data.getCount() == 1 && data.moveToFirst()) {
    408                 long mailboxId = data.getLong(data.getColumnIndex(REAL_ID));
    409                 mCallback.onSelected(mAccount, mailboxId);
    410                 return;
    411             }
    412             super.onLoadFinished(loader, data);
    413             mLoadFinished = true;
    414             getActivity().setVisible(true);
    415         }
    416 
    417         @Override
    418         String[] getFromColumns() {
    419             return MAILBOX_FROM_COLUMNS;
    420         }
    421     }
    422 }
    423