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