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