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