• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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. */
buildFilter(Account account)58         public Integer buildFilter(Account account);
59         /** Invoked when an account and mailbox have been selected. */
onSelected(Account account, long mailboxId)60         public void onSelected(Account account, long mailboxId);
61         /** Required data is missing; either the account and/or mailbox */
onMissingData(boolean missingAccount, boolean missingMailbox)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
onAttach(Activity activity)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
onActivityCreated(Bundle savedInstanceState)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
onLoadFinished(Loader<Cursor> loader, Cursor data)105     public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
106         mAdapter.swapCursor(data);
107     }
108 
109     @Override
onLoaderReset(Loader<Cursor> loader)110     public void onLoaderReset(Loader<Cursor> loader) {
111         mAdapter.swapCursor(null);
112     }
113 
114     /** Returns the cursor columns to map into list */
getFromColumns()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 {
AccountPickerLoader(Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)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
loadInBackground()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 */
addCombinedAccountRow(MatrixCursor cursor, int accountCount)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
onActivityCreated(Bundle savedInstanceState)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
onItemClick(AdapterView<?> parent, View view, int position, long id)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
onCreateLoader(int id, Bundle args)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
onLoadFinished(Loader<Cursor> loader, Cursor data)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
getFromColumns()213         String[] getFromColumns() {
214             return ACCOUNT_FROM_COLUMNS;
215         }
216 
217         /** Selects the account specified by the given cursor */
selectAccountCursor(Cursor cursor, boolean allowBack)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;
MailboxPickerLoader(Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, long accountId, boolean allowUnread)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
loadInBackground()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          */
newInstance( Context context, Account account, Integer filter)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 */
getFilter()342         int getFilter() {
343             if (mFilter == null) {
344                 mFilter = getArguments().getInt(ARG_FILTER, FILTER_ALLOW_ALL);
345             }
346             return mFilter;
347         }
348 
349         @Override
onAttach(Activity activity)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
onActivityCreated(Bundle savedInstanceState)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
onItemClick(AdapterView<?> parent, View view, int position, long id)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
onCreateLoader(int id, Bundle args)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
onLoadFinished(Loader<Cursor> loader, Cursor data)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
getFromColumns()418         String[] getFromColumns() {
419             return MAILBOX_FROM_COLUMNS;
420         }
421     }
422 }
423