EmailActivity.java revision 3d9b8e76f0a396370a5c0be99a34bf7c24bd20dd
1/* 2 * Copyright (C) 2010 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.Controller; 20import com.android.email.ControllerResultUiThreadWrapper; 21import com.android.email.Email; 22import com.android.email.MessagingExceptionStrings; 23import com.android.email.R; 24import com.android.emailcommon.Logging; 25import com.android.emailcommon.mail.MessagingException; 26import com.android.emailcommon.provider.EmailContent.Account; 27import com.android.emailcommon.provider.EmailContent.MailboxColumns; 28import com.android.emailcommon.provider.EmailContent.Message; 29import com.android.emailcommon.provider.Mailbox; 30import com.android.emailcommon.utility.EmailAsyncTask; 31import com.android.emailcommon.utility.Utility; 32 33import android.app.Activity; 34import android.app.AlertDialog; 35import android.app.Dialog; 36import android.app.Fragment; 37import android.app.SearchManager; 38import android.content.ContentResolver; 39import android.content.ContentUris; 40import android.content.ContentValues; 41import android.content.Context; 42import android.content.DialogInterface; 43import android.content.Intent; 44import android.os.Bundle; 45import android.os.Handler; 46import android.text.TextUtils; 47import android.util.Log; 48import android.view.Menu; 49import android.view.MenuItem; 50import android.view.View; 51import android.widget.TextView; 52 53import java.util.ArrayList; 54 55/** 56 * The main Email activity, which is used on both the tablet and the phone. 57 * 58 * Because this activity is device agnostic, so most of the UI aren't owned by this, but by 59 * the UIController. 60 */ 61public class EmailActivity extends Activity implements View.OnClickListener, FragmentInstallable { 62 private static final String EXTRA_ACCOUNT_ID = "ACCOUNT_ID"; 63 private static final String EXTRA_MAILBOX_ID = "MAILBOX_ID"; 64 private static final String EXTRA_MESSAGE_ID = "MESSAGE_ID"; 65 private static final String EXTRA_FORCE_PANE_MODE = "FORCE_PANE_MODE"; 66 67 /** Loader IDs starting with this is safe to use from UIControllers. */ 68 static final int UI_CONTROLLER_LOADER_ID_BASE = 100; 69 70 /** Loader IDs starting with this is safe to use from ActionBarController. */ 71 static final int ACTION_BAR_CONTROLLER_LOADER_ID_BASE = 200; 72 73 private static final int MAILBOX_SYNC_FREQUENCY_DIALOG = 1; 74 private static final int MAILBOX_SYNC_LOOKBACK_DIALOG = 2; 75 76 private Context mContext; 77 private Controller mController; 78 private Controller.Result mControllerResult; 79 80 private UIControllerBase mUIController; 81 82 private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker(); 83 84 /** Banner to display errors */ 85 private BannerController mErrorBanner; 86 /** Id of the account that had a messaging exception most recently. */ 87 private long mLastErrorAccountId; 88 89 // STOPSHIP Temporary mailbox settings UI 90 private int mDialogSelection = -1; 91 92 /** 93 * Create an intent to launch and open account's inbox. 94 * 95 * @param accountId If -1, default account will be used. 96 */ 97 public static Intent createOpenAccountIntent(Activity fromActivity, long accountId) { 98 Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class); 99 if (accountId != -1) { 100 i.putExtra(EXTRA_ACCOUNT_ID, accountId); 101 } 102 return i; 103 } 104 105 /** 106 * Create an intent to launch and open a mailbox. 107 * 108 * @param accountId must not be -1. 109 * @param mailboxId must not be -1. Magic mailboxes IDs (such as 110 * {@link Mailbox#QUERY_ALL_INBOXES}) don't work. 111 */ 112 public static Intent createOpenMailboxIntent(Activity fromActivity, long accountId, 113 long mailboxId) { 114 if (accountId == -1 || mailboxId == -1) { 115 throw new IllegalArgumentException(); 116 } 117 Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class); 118 i.putExtra(EXTRA_ACCOUNT_ID, accountId); 119 i.putExtra(EXTRA_MAILBOX_ID, mailboxId); 120 return i; 121 } 122 123 /** 124 * Create an intent to launch and open a message. 125 * 126 * @param accountId must not be -1. 127 * @param mailboxId must not be -1. Magic mailboxes IDs (such as 128 * {@link Mailbox#QUERY_ALL_INBOXES}) don't work. 129 * @param messageId must not be -1. 130 */ 131 public static Intent createOpenMessageIntent(Activity fromActivity, long accountId, 132 long mailboxId, long messageId) { 133 if (accountId == -1 || mailboxId == -1 || messageId == -1) { 134 throw new IllegalArgumentException(); 135 } 136 Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class); 137 i.putExtra(EXTRA_ACCOUNT_ID, accountId); 138 i.putExtra(EXTRA_MAILBOX_ID, mailboxId); 139 i.putExtra(EXTRA_MESSAGE_ID, messageId); 140 return i; 141 } 142 143 /** 144 * Set a debug flag to an intent to force open in 1-pane or 2-pane. 145 * 146 * @param useTwoPane true to open in 2-pane. false to open in 1-pane. 147 */ 148 public static void forcePaneMode(Intent i, boolean useTwoPane) { 149 i.putExtra(EXTRA_FORCE_PANE_MODE, useTwoPane ? 2 : 1); 150 } 151 152 /** 153 * Initialize {@link #mUIController}. 154 */ 155 private void initUIController() { 156 final boolean twoPane; 157 switch (getIntent().getIntExtra(EXTRA_FORCE_PANE_MODE, -1)) { 158 case 1: 159 twoPane = false; 160 break; 161 case 2: 162 twoPane = true; 163 break; 164 default: 165 twoPane = getResources().getBoolean(R.bool.use_two_pane); 166 break; 167 } 168 mUIController = twoPane ? new UIControllerTwoPane(this) : new UIControllerOnePane(this); 169 } 170 171 @Override 172 protected void onCreate(Bundle savedInstanceState) { 173 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onCreate"); 174 175 // UIController is used in onPrepareOptionsMenu(), which can be called from within 176 // super.onCreate(), so we need to initialize it here. 177 initUIController(); 178 179 super.onCreate(savedInstanceState); 180 ActivityHelper.debugSetWindowFlags(this); 181 setContentView(mUIController.getLayoutId()); 182 183 mUIController.onActivityViewReady(); 184 185 mContext = getApplicationContext(); 186 mController = Controller.getInstance(this); 187 mControllerResult = new ControllerResultUiThreadWrapper<ControllerResult>(new Handler(), 188 new ControllerResult()); 189 mController.addResultCallback(mControllerResult); 190 191 // Set up views 192 // TODO Probably better to extract mErrorMessageView related code into a separate class, 193 // so that it'll be easy to reuse for the phone activities. 194 TextView errorMessage = (TextView) findViewById(R.id.error_message); 195 errorMessage.setOnClickListener(this); 196 int errorBannerHeight = getResources().getDimensionPixelSize(R.dimen.error_message_height); 197 mErrorBanner = new BannerController(this, errorMessage, errorBannerHeight); 198 199 if (savedInstanceState != null) { 200 mUIController.restoreInstanceState(savedInstanceState); 201 } else { 202 // This needs to be done after installRestoredFragments. 203 // See UIControllerTwoPane.preFragmentTransactionCheck() 204 initFromIntent(); 205 } 206 mUIController.onActivityCreated(); 207 } 208 209 private void initFromIntent() { 210 final Intent i = getIntent(); 211 final long accountId = i.getLongExtra(EXTRA_ACCOUNT_ID, -1); 212 final long mailboxId = i.getLongExtra(EXTRA_MAILBOX_ID, -1); 213 final long messageId = i.getLongExtra(EXTRA_MESSAGE_ID, -1); 214 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 215 Log.d(Logging.LOG_TAG, String.format("initFromIntent: %d %d", accountId, mailboxId)); 216 } 217 218 if (accountId != -1) { 219 mUIController.open(accountId, mailboxId, messageId); 220 } 221 } 222 223 @Override 224 protected void onSaveInstanceState(Bundle outState) { 225 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 226 Log.d(Logging.LOG_TAG, this + " onSaveInstanceState"); 227 } 228 super.onSaveInstanceState(outState); 229 mUIController.onSaveInstanceState(outState); 230 } 231 232 @Override 233 public void onInstallFragment(Fragment fragment) { 234 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 235 Log.d(Logging.LOG_TAG, this + " onInstallFragment fragment=" + fragment); 236 } 237 mUIController.onInstallFragment(fragment); 238 } 239 240 @Override 241 protected void onStart() { 242 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStart"); 243 super.onStart(); 244 mUIController.onActivityStart(); 245 246 // STOPSHIP Temporary search UI 247 Intent intent = getIntent(); 248 if (Intent.ACTION_SEARCH.equals(intent.getAction())) { 249 // TODO Very temporary (e.g. no database access in UI thread) 250 Bundle appData = getIntent().getBundleExtra(SearchManager.APP_DATA); 251 if (appData == null) return; // ?? 252 final long accountId = appData.getLong(EXTRA_ACCOUNT_ID); 253 final long mailboxId = appData.getLong(EXTRA_MAILBOX_ID); 254 final String queryString = intent.getStringExtra(SearchManager.QUERY); 255 Log.d(Logging.LOG_TAG, queryString); 256 // Switch to search mailbox 257 // TODO How to handle search from within the search mailbox?? 258 final Controller controller = Controller.getInstance(mContext); 259 final Mailbox searchMailbox = controller.getSearchMailbox(accountId); 260 if (searchMailbox == null) return; 261 262 // Delete contents, add a placeholder 263 ContentResolver resolver = mContext.getContentResolver(); 264 resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailbox.mId, 265 null); 266 ContentValues cv = new ContentValues(); 267 cv.put(Mailbox.DISPLAY_NAME, queryString); 268 resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailbox.mId), cv, 269 null, null); 270 Message msg = new Message(); 271 msg.mMailboxKey = searchMailbox.mId; 272 msg.mAccountKey = accountId; 273 msg.mDisplayName = "Searching for " + queryString; 274 msg.mTimeStamp = Long.MAX_VALUE; // Sort on top 275 msg.save(mContext); 276 277 startActivity(createOpenMessageIntent(EmailActivity.this, 278 accountId, searchMailbox.mId, msg.mId)); 279 Utility.runAsync(new Runnable() { 280 @Override 281 public void run() { 282 controller.searchMessages(accountId, mailboxId, true, queryString, 10, 0, 283 searchMailbox.mId); 284 }}); 285 return; 286 } 287 } 288 289 @Override 290 protected void onResume() { 291 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onResume"); 292 super.onResume(); 293 mUIController.onActivityResume(); 294 /** 295 * In {@link MessageList#onResume()}, we go back to {@link Welcome} if an account 296 * has been added/removed. We don't need to do that here, because we fetch the most 297 * up-to-date account list. Additionally, we detect and do the right thing if all 298 * of the accounts have been removed. 299 */ 300 } 301 302 @Override 303 protected void onPause() { 304 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onPause"); 305 super.onPause(); 306 mUIController.onActivityPause(); 307 } 308 309 @Override 310 protected void onStop() { 311 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStop"); 312 super.onStop(); 313 mUIController.onActivityStop(); 314 } 315 316 @Override 317 protected void onDestroy() { 318 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onDestroy"); 319 mController.removeResultCallback(mControllerResult); 320 mTaskTracker.cancellAllInterrupt(); 321 mUIController.onActivityDestroy(); 322 super.onDestroy(); 323 } 324 325 @Override 326 public void onBackPressed() { 327 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 328 Log.d(Logging.LOG_TAG, this + " onBackPressed"); 329 } 330 if (!mUIController.onBackPressed(true)) { 331 // Not handled by UIController -- perform the default. i.e. close the app. 332 super.onBackPressed(); 333 } 334 } 335 336 @Override 337 public void onClick(View v) { 338 switch (v.getId()) { 339 case R.id.error_message: 340 dismissErrorMessage(); 341 break; 342 } 343 } 344 345 /** 346 * Force dismiss the error banner. 347 */ 348 private void dismissErrorMessage() { 349 mErrorBanner.dismiss(); 350 } 351 352 @Override 353 public boolean onCreateOptionsMenu(Menu menu) { 354 return mUIController.onCreateOptionsMenu(getMenuInflater(), menu); 355 } 356 357 @Override 358 public boolean onPrepareOptionsMenu(Menu menu) { 359 // STOPSHIP Temporary search/sync options UI 360 // Only show search/sync options for EAS 361 boolean isEas = false; 362 long accountId = mUIController.getActualAccountId(); 363 if (accountId > 0) { 364 if ("eas".equals(Account.getProtocol(mContext, accountId))) { 365 isEas = true; 366 } 367 } 368 // Should use an isSearchable call to prevent search on inappropriate accounts/boxes 369 menu.findItem(R.id.search).setVisible(isEas); 370 // Should use an isSyncable call to prevent drafts/outbox from allowing this 371 menu.findItem(R.id.sync_lookback).setVisible(isEas); 372 menu.findItem(R.id.sync_frequency).setVisible(isEas); 373 374 return mUIController.onPrepareOptionsMenu(getMenuInflater(), menu); 375 } 376 377 @Override 378 public boolean onSearchRequested() { 379 Bundle bundle = new Bundle(); 380 bundle.putLong(EXTRA_ACCOUNT_ID, mUIController.getActualAccountId()); 381 bundle.putLong(EXTRA_MAILBOX_ID, mUIController.getSearchMailboxId()); 382 startSearch(null, false, bundle, false); 383 return true; 384 } 385 386 // STOPSHIP Set column from user options 387 private void setMailboxColumn(long mailboxId, String column, String value) { 388 if (mailboxId > 0) { 389 ContentValues cv = new ContentValues(); 390 cv.put(column, value); 391 getContentResolver().update( 392 ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId), 393 cv, null, null); 394 mUIController.onRefresh(); 395 } 396 } 397 // STOPSHIP Temporary mailbox settings UI. If this ends up being useful, it should 398 // be moved to Utility (emailcommon) 399 private int findInStringArray(String[] array, String item) { 400 int i = 0; 401 for (String str: array) { 402 if (str.equals(item)) { 403 return i; 404 } 405 i++; 406 } 407 return -1; 408 } 409 410 // STOPSHIP Temporary mailbox settings UI 411 private final DialogInterface.OnClickListener mSelectionListener = 412 new DialogInterface.OnClickListener() { 413 public void onClick(DialogInterface dialog, int which) { 414 mDialogSelection = which; 415 } 416 }; 417 418 // STOPSHIP Temporary mailbox settings UI 419 private final DialogInterface.OnClickListener mCancelListener = 420 new DialogInterface.OnClickListener() { 421 public void onClick(DialogInterface dialog, int which) { 422 } 423 }; 424 425 // STOPSHIP Temporary mailbox settings UI 426 @Override 427 @Deprecated 428 protected Dialog onCreateDialog(int id, Bundle args) { 429 final long mailboxId = mUIController.getMailboxSettingsMailboxId(); 430 if (mailboxId < 0) { 431 return null; 432 } 433 final Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mailboxId); 434 if (mailbox == null) return null; 435 switch (id) { 436 case MAILBOX_SYNC_FREQUENCY_DIALOG: 437 String freq = Integer.toString(mailbox.mSyncInterval); 438 final String[] freqValues = getResources().getStringArray( 439 R.array.account_settings_check_frequency_values_push); 440 int selection = findInStringArray(freqValues, freq); 441 // If not found, this is a push mailbox; trust me on this 442 if (selection == -1) selection = 0; 443 return new AlertDialog.Builder(this) 444 .setIconAttribute(android.R.attr.dialogIcon) 445 .setTitle(R.string.mailbox_options_check_frequency_label) 446 .setSingleChoiceItems(R.array.account_settings_check_frequency_entries_push, 447 selection, 448 mSelectionListener) 449 .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() { 450 public void onClick(DialogInterface dialog, int which) { 451 setMailboxColumn(mailboxId, MailboxColumns.SYNC_INTERVAL, 452 freqValues[mDialogSelection]); 453 }}) 454 .setNegativeButton(R.string.cancel_action, mCancelListener) 455 .create(); 456 457 case MAILBOX_SYNC_LOOKBACK_DIALOG: 458 freq = Integer.toString(mailbox.mSyncLookback); 459 final String[] windowValues = getResources().getStringArray( 460 R.array.account_settings_mail_window_values); 461 selection = findInStringArray(windowValues, freq); 462 return new AlertDialog.Builder(this) 463 .setIconAttribute(android.R.attr.dialogIcon) 464 .setTitle(R.string.mailbox_options_lookback_label) 465 .setSingleChoiceItems(R.array.account_settings_mail_window_entries, 466 selection, 467 mSelectionListener) 468 .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() { 469 public void onClick(DialogInterface dialog, int which) { 470 setMailboxColumn(mailboxId, MailboxColumns.SYNC_LOOKBACK, 471 windowValues[mDialogSelection]); 472 }}) 473 .setNegativeButton(R.string.cancel_action, mCancelListener) 474 .create(); 475 } 476 return null; 477 } 478 479 @Override 480 @SuppressWarnings("deprecation") 481 public boolean onOptionsItemSelected(MenuItem item) { 482 if (mUIController.onOptionsItemSelected(item)) { 483 return true; 484 } 485 switch (item.getItemId()) { 486 // STOPSHIP Temporary mailbox settings UI 487 case R.id.sync_lookback: 488 showDialog(MAILBOX_SYNC_LOOKBACK_DIALOG); 489 return true; 490 // STOPSHIP Temporary mailbox settings UI 491 case R.id.sync_frequency: 492 showDialog(MAILBOX_SYNC_FREQUENCY_DIALOG); 493 return true; 494 case R.id.search: 495 onSearchRequested(); 496 return true; 497 } 498 return super.onOptionsItemSelected(item); 499 } 500 501 502 /** 503 * A {@link Controller.Result} to detect connection status. 504 */ 505 private class ControllerResult extends Controller.Result { 506 @Override 507 public void sendMailCallback( 508 MessagingException result, long accountId, long messageId, int progress) { 509 handleError(result, accountId, progress); 510 } 511 512 @Override 513 public void serviceCheckMailCallback( 514 MessagingException result, long accountId, long mailboxId, int progress, long tag) { 515 handleError(result, accountId, progress); 516 } 517 518 @Override 519 public void updateMailboxCallback(MessagingException result, long accountId, long mailboxId, 520 int progress, int numNewMessages, ArrayList<Long> addedMessages) { 521 handleError(result, accountId, progress); 522 } 523 524 @Override 525 public void updateMailboxListCallback( 526 MessagingException result, long accountId, int progress) { 527 handleError(result, accountId, progress); 528 } 529 530 @Override 531 public void loadAttachmentCallback(MessagingException result, long accountId, 532 long messageId, long attachmentId, int progress) { 533 handleError(result, accountId, progress); 534 } 535 536 @Override 537 public void loadMessageForViewCallback(MessagingException result, long accountId, 538 long messageId, int progress) { 539 handleError(result, accountId, progress); 540 } 541 542 private void handleError(final MessagingException result, final long accountId, 543 int progress) { 544 if (accountId == -1) { 545 return; 546 } 547 if (result == null) { 548 if (progress > 0) { 549 // Connection now working; clear the error message banner 550 if (mLastErrorAccountId == accountId) { 551 dismissErrorMessage(); 552 } 553 } 554 } else { 555 // Connection error; show the error message banner 556 new EmailAsyncTask<Void, Void, String>(mTaskTracker) { 557 @Override 558 protected String doInBackground(Void... params) { 559 Account account = 560 Account.restoreAccountWithId(EmailActivity.this, accountId); 561 return (account == null) ? null : account.mDisplayName; 562 } 563 564 @Override 565 protected void onPostExecute(String accountName) { 566 String message = 567 MessagingExceptionStrings.getErrorString(EmailActivity.this, result); 568 if (!TextUtils.isEmpty(accountName)) { 569 // TODO Use properly designed layout. Don't just concatenate strings; 570 // which is generally poor for I18N. 571 message = message + " (" + accountName + ")"; 572 } 573 if (mErrorBanner.show(message)) { 574 mLastErrorAccountId = accountId; 575 } 576 } 577 }.executeParallel(); 578 } 579 } 580 } 581} 582