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 android.app.Activity;
     20 import android.app.FragmentTransaction;
     21 import android.app.ListFragment;
     22 import android.app.LoaderManager.LoaderCallbacks;
     23 import android.content.ContentValues;
     24 import android.content.Context;
     25 import android.content.CursorLoader;
     26 import android.content.Loader;
     27 import android.content.res.Resources;
     28 import android.database.Cursor;
     29 import android.database.MatrixCursor;
     30 import android.database.MatrixCursor.RowBuilder;
     31 import android.database.MergeCursor;
     32 import android.net.Uri;
     33 import android.os.Bundle;
     34 import android.view.View;
     35 import android.widget.AdapterView;
     36 import android.widget.AdapterView.OnItemClickListener;
     37 import android.widget.ListView;
     38 import android.widget.SimpleCursorAdapter;
     39 
     40 import com.android.email.R;
     41 import com.android.emailcommon.provider.Account;
     42 import com.android.emailcommon.provider.EmailContent.AccountColumns;
     43 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
     44 import com.android.emailcommon.provider.HostAuth;
     45 import com.android.emailcommon.provider.Mailbox;
     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 final static String[] ACCOUNT_FROM_COLUMNS = new String[] {
    169             AccountColumns.DISPLAY_NAME,
    170         };
    171 
    172         @Override
    173         public void onActivityCreated(Bundle savedInstanceState) {
    174             super.onActivityCreated(savedInstanceState);
    175             getActivity().setTitle(R.string.account_shortcut_picker_title);
    176         }
    177 
    178         @Override
    179         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    180             Cursor cursor = (Cursor) parent.getItemAtPosition(position);
    181             selectAccountCursor(cursor, true);
    182         }
    183 
    184         @Override
    185         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    186             Context context = getActivity();
    187             return new AccountPickerLoader(
    188                 context, Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null, null);
    189         }
    190 
    191         @Override
    192         public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    193             // if there is only one account, auto-select it
    194             // No accounts; close the dialog
    195             if (data.getCount() == 0) {
    196                 mCallback.onMissingData(true, false);
    197                 return;
    198             }
    199             if (data.getCount() == 1 && data.moveToFirst()) {
    200                 selectAccountCursor(data, false);
    201                 return;
    202             }
    203             super.onLoadFinished(loader, data);
    204         }
    205 
    206         @Override
    207         String[] getFromColumns() {
    208             return ACCOUNT_FROM_COLUMNS;
    209         }
    210 
    211         /** Selects the account specified by the given cursor */
    212         private void selectAccountCursor(Cursor cursor, boolean allowBack) {
    213             Account account = new Account();
    214             account.restore(cursor);
    215             ShortcutPickerFragment fragment = MailboxShortcutPickerFragment.newInstance(
    216                     getActivity(), account, mCallback.buildFilter(account));
    217             FragmentTransaction transaction = getFragmentManager().beginTransaction();
    218             transaction.replace(R.id.shortcut_list, fragment);
    219             if (allowBack) {
    220                 transaction.addToBackStack(null);
    221             }
    222             transaction.commitAllowingStateLoss();
    223         }
    224     }
    225 
    226     // TODO if we add meta-mailboxes to the database, remove this class entirely
    227     private static final class MailboxPickerLoader extends CursorLoader {
    228         private final long mAccountId;
    229         private final boolean mAllowUnread;
    230         public MailboxPickerLoader(Context context, Uri uri, String[] projection, String selection,
    231                 String[] selectionArgs, String sortOrder, long accountId, boolean allowUnread) {
    232             super(context, uri, projection, selection, selectionArgs, sortOrder);
    233             mAccountId = accountId;
    234             mAllowUnread = allowUnread;
    235         }
    236 
    237         @Override
    238         public Cursor loadInBackground() {
    239             MatrixCursor unreadCursor =
    240                     new MatrixCursor(MailboxShortcutPickerFragment.MATRIX_PROJECTION);
    241             Context context = getContext();
    242             if (mAllowUnread) {
    243                 // For the special mailboxes, their ID is < 0. The UI list does not deal with
    244                 // negative values very well, so, add MAX_VALUE to ensure they're positive, but,
    245                 // don't clash with legitimate mailboxes.
    246                 String mailboxName = context.getString(R.string.picker_mailbox_name_all_unread);
    247                 unreadCursor.addRow(
    248                         new Object[] {
    249                             Integer.MAX_VALUE + Mailbox.QUERY_ALL_UNREAD,
    250                             Mailbox.QUERY_ALL_UNREAD,
    251                             mailboxName,
    252                         });
    253             }
    254 
    255             if (mAccountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
    256                 // Do something special for the "combined" view
    257                 MatrixCursor combinedMailboxesCursor =
    258                         new MatrixCursor(MailboxShortcutPickerFragment.MATRIX_PROJECTION);
    259                 // For the special mailboxes, their ID is < 0. The UI list does not deal with
    260                 // negative values very well, so, add MAX_VALUE to ensure they're positive, but,
    261                 // don't clash with legitimate mailboxes.
    262                 String mailboxName = context.getString(R.string.picker_mailbox_name_all_inbox);
    263                 combinedMailboxesCursor.addRow(
    264                         new Object[] {
    265                             Integer.MAX_VALUE + Mailbox.QUERY_ALL_INBOXES,
    266                             Mailbox.QUERY_ALL_INBOXES,
    267                             mailboxName
    268                         });
    269                 return new MergeCursor(new Cursor[] { combinedMailboxesCursor, unreadCursor });
    270             }
    271 
    272             // Loading for a regular account; perform a normal load
    273             return new MergeCursor(new Cursor[] { super.loadInBackground(), unreadCursor });
    274         }
    275     }
    276 
    277     /** Mailbox picker */
    278     public static class MailboxShortcutPickerFragment extends ShortcutPickerFragment {
    279         /** Allow all mailboxes in the mailbox list */
    280         public static int FILTER_ALLOW_ALL    = 0;
    281         /** Only allow an account's INBOX */
    282         public static int FILTER_INBOX_ONLY   = 1 << 0;
    283         /** Allow an "unread" mailbox; this is not affected by {@link #FILTER_INBOX_ONLY} */
    284         public static int FILTER_ALLOW_UNREAD = 1 << 1;
    285         /** Fragment argument to set filter values */
    286         static final String ARG_FILTER  = "MailboxShortcutPickerFragment.filter";
    287         static final String ARG_ACCOUNT = "MailboxShortcutPickerFragment.account";
    288 
    289         private final static String REAL_ID = "realId";
    290         private final static String[] MAILBOX_FROM_COLUMNS = new String[] {
    291             MailboxColumns.DISPLAY_NAME,
    292         };
    293         /** Loader projection used for IMAP & POP3 accounts */
    294         private final static String[] IMAP_PROJECTION = new String [] {
    295             MailboxColumns.ID, MailboxColumns.ID + " as " + REAL_ID,
    296             MailboxColumns.SERVER_ID + " as " + MailboxColumns.DISPLAY_NAME
    297         };
    298         /** Loader projection used for EAS accounts */
    299         private final static String[] EAS_PROJECTION = new String [] {
    300             MailboxColumns.ID, MailboxColumns.ID + " as " + REAL_ID,
    301             MailboxColumns.DISPLAY_NAME
    302         };
    303         /** Loader projection used for a matrix cursor */
    304         private final static String[] MATRIX_PROJECTION = new String [] {
    305             MailboxColumns.ID, REAL_ID, MailboxColumns.DISPLAY_NAME
    306         };
    307         // TODO #ALL_MAILBOX_SELECTION is identical to MailboxesAdapter#ALL_MAILBOX_SELECTION;
    308         // create a common selection. Move this to the Mailbox class?
    309         /** Selection for all visible mailboxes for an account */
    310         private final static String ALL_MAILBOX_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?" +
    311                 " AND " + Mailbox.USER_VISIBLE_MAILBOX_SELECTION;
    312         /** Selection for just the INBOX of an account */
    313         private final static String INBOX_ONLY_SELECTION = ALL_MAILBOX_SELECTION +
    314                     " AND " + MailboxColumns.TYPE + " = " + Mailbox.TYPE_INBOX;
    315         /** The currently selected account */
    316         private Account mAccount;
    317         /** The filter values; default to allow all mailboxes */
    318         private Integer mFilter;
    319 
    320         /**
    321          * Builds a mailbox shortcut picker for the given account.
    322          */
    323         public static MailboxShortcutPickerFragment newInstance(
    324                 Context context, Account account, Integer filter) {
    325 
    326             MailboxShortcutPickerFragment fragment = new MailboxShortcutPickerFragment();
    327             Bundle args = new Bundle();
    328             args.putParcelable(ARG_ACCOUNT, account);
    329             args.putInt(ARG_FILTER, filter);
    330             fragment.setArguments(args);
    331             return fragment;
    332         }
    333 
    334         /** Returns the mailbox filter */
    335         int getFilter() {
    336             if (mFilter == null) {
    337                 mFilter = getArguments().getInt(ARG_FILTER, FILTER_ALLOW_ALL);
    338             }
    339             return mFilter;
    340         }
    341 
    342         @Override
    343         public void onAttach(Activity activity) {
    344             // Need to setup the account first thing
    345             mAccount = getArguments().getParcelable(ARG_ACCOUNT);
    346             super.onAttach(activity);
    347         }
    348 
    349         @Override
    350         public void onActivityCreated(Bundle savedInstanceState) {
    351             super.onActivityCreated(savedInstanceState);
    352             getActivity().setTitle(R.string.mailbox_shortcut_picker_title);
    353         }
    354 
    355         @Override
    356         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    357             Cursor cursor = (Cursor) parent.getItemAtPosition(position);
    358             long mailboxId = cursor.getLong(cursor.getColumnIndex(REAL_ID));
    359             mCallback.onSelected(mAccount, mailboxId);
    360         }
    361 
    362         @Override
    363         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    364             Context context = getActivity();
    365             // TODO Create a fully-qualified path name for Exchange accounts [code should also work
    366             //      for MoveMessageToDialog.java]
    367             HostAuth recvAuth = mAccount.getOrCreateHostAuthRecv(context);
    368             final String[] projection;
    369             final String orderBy;
    370             final String selection;
    371             if (recvAuth.isEasConnection()) {
    372                 projection = EAS_PROJECTION;
    373                 orderBy = MailboxColumns.DISPLAY_NAME;
    374             } else {
    375                 projection = IMAP_PROJECTION;
    376                 orderBy = MailboxColumns.SERVER_ID;
    377             }
    378             if ((getFilter() & FILTER_INBOX_ONLY) == 0) {
    379                 selection = ALL_MAILBOX_SELECTION;
    380             } else {
    381                 selection = INBOX_ONLY_SELECTION;
    382             }
    383             return new MailboxPickerLoader(
    384                 context, Mailbox.CONTENT_URI, projection, selection,
    385                 new String[] { Long.toString(mAccount.mId) }, orderBy, mAccount.mId,
    386                 (getFilter() & FILTER_ALLOW_UNREAD) != 0);
    387         }
    388 
    389         @Override
    390         public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    391             // No accounts; close the dialog
    392             if (data.getCount() == 0) {
    393                 mCallback.onMissingData(false, true);
    394                 return;
    395             }
    396             // if there is only one mailbox, auto-select it
    397             if (data.getCount() == 1 && data.moveToFirst()) {
    398                 long mailboxId = data.getLong(data.getColumnIndex(REAL_ID));
    399                 mCallback.onSelected(mAccount, mailboxId);
    400                 return;
    401             }
    402             super.onLoadFinished(loader, data);
    403         }
    404 
    405         @Override
    406         String[] getFromColumns() {
    407             return MAILBOX_FROM_COLUMNS;
    408         }
    409     }
    410 }
    411