EmailActivity.java revision b0b6eb56f716f3ac0c153b1d4a1b7b2bdfba4335
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 android.app.Activity; 20import android.app.AlertDialog; 21import android.app.Dialog; 22import android.app.Fragment; 23import android.content.ContentUris; 24import android.content.ContentValues; 25import android.content.Context; 26import android.content.DialogInterface; 27import android.content.Intent; 28import android.os.Bundle; 29import android.os.Handler; 30import android.text.TextUtils; 31import android.util.Log; 32import android.view.Menu; 33import android.view.MenuItem; 34import android.view.View; 35import android.widget.TextView; 36 37import com.android.email.Controller; 38import com.android.email.ControllerResultUiThreadWrapper; 39import com.android.email.Email; 40import com.android.email.MessageListContext; 41import com.android.email.MessagingExceptionStrings; 42import com.android.email.R; 43import com.android.emailcommon.Logging; 44import com.android.emailcommon.mail.MessagingException; 45import com.android.emailcommon.provider.Account; 46import com.android.emailcommon.provider.EmailContent.MailboxColumns; 47import com.android.emailcommon.provider.EmailContent.Message; 48import com.android.emailcommon.provider.Mailbox; 49import com.android.emailcommon.utility.EmailAsyncTask; 50import com.google.common.base.Preconditions; 51 52import java.util.ArrayList; 53 54/** 55 * The main Email activity, which is used on both the tablet and the phone. 56 * 57 * Because this activity is device agnostic, so most of the UI aren't owned by this, but by 58 * the UIController. 59 */ 60public class EmailActivity extends Activity implements View.OnClickListener, FragmentInstallable { 61 public static final String EXTRA_ACCOUNT_ID = "ACCOUNT_ID"; 62 public static final String EXTRA_MAILBOX_ID = "MAILBOX_ID"; 63 public static final String EXTRA_MESSAGE_ID = "MESSAGE_ID"; 64 public static final String EXTRA_QUERY_STRING = "QUERY_STRING"; 65 66 /** Loader IDs starting with this is safe to use from UIControllers. */ 67 static final int UI_CONTROLLER_LOADER_ID_BASE = 100; 68 69 /** Loader IDs starting with this is safe to use from ActionBarController. */ 70 static final int ACTION_BAR_CONTROLLER_LOADER_ID_BASE = 200; 71 72 private static final int MAILBOX_SYNC_FREQUENCY_DIALOG = 1; 73 private static final int MAILBOX_SYNC_LOOKBACK_DIALOG = 2; 74 75 private Controller mController; 76 private Controller.Result mControllerResult; 77 78 private UIControllerBase mUIController; 79 80 private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker(); 81 82 /** Banner to display errors */ 83 private BannerController mErrorBanner; 84 /** Id of the account that had a messaging exception most recently. */ 85 private long mLastErrorAccountId; 86 87 // STOPSHIP Temporary mailbox settings UI 88 private int mDialogSelection = -1; 89 90 /** 91 * Create an intent to launch and open account's inbox. 92 * 93 * @param accountId If -1, default account will be used. 94 */ 95 public static Intent createOpenAccountIntent(Activity fromActivity, long accountId) { 96 Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class); 97 if (accountId != -1) { 98 i.putExtra(EXTRA_ACCOUNT_ID, accountId); 99 } 100 return i; 101 } 102 103 /** 104 * Create an intent to launch and open a mailbox. 105 * 106 * @param accountId must not be -1. 107 * @param mailboxId must not be -1. Magic mailboxes IDs (such as 108 * {@link Mailbox#QUERY_ALL_INBOXES}) don't work. 109 */ 110 public static Intent createOpenMailboxIntent(Activity fromActivity, long accountId, 111 long mailboxId) { 112 if (accountId == -1 || mailboxId == -1) { 113 throw new IllegalArgumentException(); 114 } 115 Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class); 116 i.putExtra(EXTRA_ACCOUNT_ID, accountId); 117 i.putExtra(EXTRA_MAILBOX_ID, mailboxId); 118 return i; 119 } 120 121 /** 122 * Create an intent to launch and open a message. 123 * 124 * @param accountId must not be -1. 125 * @param mailboxId must not be -1. Magic mailboxes IDs (such as 126 * {@link Mailbox#QUERY_ALL_INBOXES}) don't work. 127 * @param messageId must not be -1. 128 */ 129 public static Intent createOpenMessageIntent(Activity fromActivity, long accountId, 130 long mailboxId, long messageId) { 131 if (accountId == -1 || mailboxId == -1 || messageId == -1) { 132 throw new IllegalArgumentException(); 133 } 134 Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class); 135 i.putExtra(EXTRA_ACCOUNT_ID, accountId); 136 i.putExtra(EXTRA_MAILBOX_ID, mailboxId); 137 i.putExtra(EXTRA_MESSAGE_ID, messageId); 138 return i; 139 } 140 141 /** 142 * Create an intent to launch search activity. 143 * 144 * @param accountId ID of the account for the mailbox. Must not be {@link Account#NO_ACCOUNT}. 145 * @param mailboxId ID of the mailbox to search, or {@link Mailbox#NO_MAILBOX} to perform 146 * global search. 147 * @param query query string. 148 */ 149 public static Intent createSearchIntent(Activity fromActivity, long accountId, 150 long mailboxId, String query) { 151 Preconditions.checkArgument(Account.isNormalAccount(accountId), 152 "Can only search in normal accounts"); 153 154 // Note that a search doesn't use a restart intent, as we want another instance of 155 // the activity to sit on the stack for search. 156 Intent i = new Intent(fromActivity, EmailActivity.class); 157 i.putExtra(EXTRA_ACCOUNT_ID, accountId); 158 i.putExtra(EXTRA_MAILBOX_ID, mailboxId); 159 i.putExtra(EXTRA_QUERY_STRING, query); 160 i.setAction(Intent.ACTION_SEARCH); 161 return i; 162 } 163 164 /** 165 * Initialize {@link #mUIController}. 166 */ 167 private void initUIController() { 168 mUIController = UiUtilities.useTwoPane(this) 169 ? 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 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.onRestoreInstanceState(savedInstanceState); 201 } else { 202 initFromIntent(); 203 } 204 mUIController.onActivityCreated(); 205 } 206 207 private void initFromIntent() { 208 final Intent intent = getIntent(); 209 final MessageListContext viewContext = MessageListContext.forIntent(this, intent); 210 final long messageId = intent.getLongExtra(EXTRA_MESSAGE_ID, Message.NO_MESSAGE); 211 212 if (viewContext.isSearch()) { 213 EmailAsyncTask.runAsyncParallel(new Runnable() { 214 @Override 215 public void run() { 216 try { 217 Controller controller = Controller.getInstance(EmailActivity.this); 218 controller.searchMessages( 219 viewContext.mAccountId, viewContext.getSearchParams()); 220 } catch (MessagingException e) { 221 // TODO: handle. 222 Log.e(Logging.LOG_TAG, "Got exception while searching " + e); 223 } 224 }}); 225 } 226 227 mUIController.open(viewContext, messageId); 228 } 229 230 @Override 231 protected void onSaveInstanceState(Bundle outState) { 232 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 233 Log.d(Logging.LOG_TAG, this + " onSaveInstanceState"); 234 } 235 super.onSaveInstanceState(outState); 236 mUIController.onSaveInstanceState(outState); 237 } 238 239 // FragmentInstallable 240 @Override 241 public void onInstallFragment(Fragment fragment) { 242 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 243 Log.d(Logging.LOG_TAG, this + " onInstallFragment fragment=" + fragment); 244 } 245 mUIController.onInstallFragment(fragment); 246 } 247 248 // FragmentInstallable 249 @Override 250 public void onUninstallFragment(Fragment fragment) { 251 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 252 Log.d(Logging.LOG_TAG, this + " onUninstallFragment fragment=" + fragment); 253 } 254 mUIController.onUninstallFragment(fragment); 255 } 256 257 @Override 258 protected void onStart() { 259 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStart"); 260 super.onStart(); 261 mUIController.onActivityStart(); 262 } 263 264 @Override 265 protected void onResume() { 266 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onResume"); 267 super.onResume(); 268 mUIController.onActivityResume(); 269 /** 270 * In {@link MessageList#onResume()}, we go back to {@link Welcome} if an account 271 * has been added/removed. We don't need to do that here, because we fetch the most 272 * up-to-date account list. Additionally, we detect and do the right thing if all 273 * of the accounts have been removed. 274 */ 275 } 276 277 @Override 278 protected void onPause() { 279 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onPause"); 280 super.onPause(); 281 mUIController.onActivityPause(); 282 } 283 284 @Override 285 protected void onStop() { 286 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStop"); 287 super.onStop(); 288 mUIController.onActivityStop(); 289 } 290 291 @Override 292 protected void onDestroy() { 293 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onDestroy"); 294 mController.removeResultCallback(mControllerResult); 295 mTaskTracker.cancellAllInterrupt(); 296 mUIController.onActivityDestroy(); 297 super.onDestroy(); 298 } 299 300 @Override 301 public void onBackPressed() { 302 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 303 Log.d(Logging.LOG_TAG, this + " onBackPressed"); 304 } 305 if (!mUIController.onBackPressed(true)) { 306 // Not handled by UIController -- perform the default. i.e. close the app. 307 super.onBackPressed(); 308 } 309 } 310 311 @Override 312 public void onClick(View v) { 313 switch (v.getId()) { 314 case R.id.error_message: 315 dismissErrorMessage(); 316 break; 317 } 318 } 319 320 /** 321 * Force dismiss the error banner. 322 */ 323 private void dismissErrorMessage() { 324 mErrorBanner.dismiss(); 325 } 326 327 @Override 328 public boolean onCreateOptionsMenu(Menu menu) { 329 return mUIController.onCreateOptionsMenu(getMenuInflater(), menu); 330 } 331 332 @Override 333 public boolean onPrepareOptionsMenu(Menu menu) { 334 return mUIController.onPrepareOptionsMenu(getMenuInflater(), menu); 335 } 336 337 /** 338 * Called when the search key is pressd. 339 * 340 * Use the below command to emulate the key press on devices without the search key. 341 * adb shell input keyevent 84 342 */ 343 @Override 344 public boolean onSearchRequested() { 345 if (Email.DEBUG) { 346 Log.d(Logging.LOG_TAG, this + " onSearchRequested"); 347 } 348 mUIController.onSearchRequested(); 349 return true; // Event handled. 350 } 351 352 // STOPSHIP Set column from user options 353 private void setMailboxColumn(long mailboxId, String column, String value) { 354 if (mailboxId > 0) { 355 ContentValues cv = new ContentValues(); 356 cv.put(column, value); 357 getContentResolver().update( 358 ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId), 359 cv, null, null); 360 mUIController.onRefresh(); 361 } 362 } 363 // STOPSHIP Temporary mailbox settings UI. If this ends up being useful, it should 364 // be moved to Utility (emailcommon) 365 private int findInStringArray(String[] array, String item) { 366 int i = 0; 367 for (String str: array) { 368 if (str.equals(item)) { 369 return i; 370 } 371 i++; 372 } 373 return -1; 374 } 375 376 // STOPSHIP Temporary mailbox settings UI 377 private final DialogInterface.OnClickListener mSelectionListener = 378 new DialogInterface.OnClickListener() { 379 public void onClick(DialogInterface dialog, int which) { 380 mDialogSelection = which; 381 } 382 }; 383 384 // STOPSHIP Temporary mailbox settings UI 385 private final DialogInterface.OnClickListener mCancelListener = 386 new DialogInterface.OnClickListener() { 387 public void onClick(DialogInterface dialog, int which) { 388 } 389 }; 390 391 // STOPSHIP Temporary mailbox settings UI 392 @Override 393 @Deprecated 394 protected Dialog onCreateDialog(int id, Bundle args) { 395 final long mailboxId = mUIController.getMailboxSettingsMailboxId(); 396 if (mailboxId < 0) { 397 return null; 398 } 399 final Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mailboxId); 400 if (mailbox == null) return null; 401 switch (id) { 402 case MAILBOX_SYNC_FREQUENCY_DIALOG: 403 String freq = Integer.toString(mailbox.mSyncInterval); 404 final String[] freqValues = getResources().getStringArray( 405 R.array.account_settings_check_frequency_values_push); 406 int selection = findInStringArray(freqValues, freq); 407 // If not found, this is a push mailbox; trust me on this 408 if (selection == -1) selection = 0; 409 return new AlertDialog.Builder(this) 410 .setIconAttribute(android.R.attr.dialogIcon) 411 .setTitle(R.string.mailbox_options_check_frequency_label) 412 .setSingleChoiceItems(R.array.account_settings_check_frequency_entries_push, 413 selection, 414 mSelectionListener) 415 .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() { 416 public void onClick(DialogInterface dialog, int which) { 417 setMailboxColumn(mailboxId, MailboxColumns.SYNC_INTERVAL, 418 freqValues[mDialogSelection]); 419 }}) 420 .setNegativeButton(R.string.cancel_action, mCancelListener) 421 .create(); 422 423 case MAILBOX_SYNC_LOOKBACK_DIALOG: 424 freq = Integer.toString(mailbox.mSyncLookback); 425 final String[] windowValues = getResources().getStringArray( 426 R.array.account_settings_mail_window_values); 427 selection = findInStringArray(windowValues, freq); 428 return new AlertDialog.Builder(this) 429 .setIconAttribute(android.R.attr.dialogIcon) 430 .setTitle(R.string.mailbox_options_lookback_label) 431 .setSingleChoiceItems(R.array.account_settings_mail_window_entries, 432 selection, 433 mSelectionListener) 434 .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() { 435 public void onClick(DialogInterface dialog, int which) { 436 setMailboxColumn(mailboxId, MailboxColumns.SYNC_LOOKBACK, 437 windowValues[mDialogSelection]); 438 }}) 439 .setNegativeButton(R.string.cancel_action, mCancelListener) 440 .create(); 441 } 442 return null; 443 } 444 445 @Override 446 @SuppressWarnings("deprecation") 447 public boolean onOptionsItemSelected(MenuItem item) { 448 if (mUIController.onOptionsItemSelected(item)) { 449 return true; 450 } 451 switch (item.getItemId()) { 452 // STOPSHIP Temporary mailbox settings UI 453 case R.id.sync_lookback: 454 showDialog(MAILBOX_SYNC_LOOKBACK_DIALOG); 455 return true; 456 // STOPSHIP Temporary mailbox settings UI 457 case R.id.sync_frequency: 458 showDialog(MAILBOX_SYNC_FREQUENCY_DIALOG); 459 return true; 460 } 461 return super.onOptionsItemSelected(item); 462 } 463 464 465 /** 466 * A {@link Controller.Result} to detect connection status. 467 */ 468 private class ControllerResult extends Controller.Result { 469 @Override 470 public void sendMailCallback( 471 MessagingException result, long accountId, long messageId, int progress) { 472 handleError(result, accountId, progress); 473 } 474 475 @Override 476 public void serviceCheckMailCallback( 477 MessagingException result, long accountId, long mailboxId, int progress, long tag) { 478 handleError(result, accountId, progress); 479 } 480 481 @Override 482 public void updateMailboxCallback(MessagingException result, long accountId, long mailboxId, 483 int progress, int numNewMessages, ArrayList<Long> addedMessages) { 484 handleError(result, accountId, progress); 485 } 486 487 @Override 488 public void updateMailboxListCallback( 489 MessagingException result, long accountId, int progress) { 490 handleError(result, accountId, progress); 491 } 492 493 @Override 494 public void loadAttachmentCallback(MessagingException result, long accountId, 495 long messageId, long attachmentId, int progress) { 496 handleError(result, accountId, progress); 497 } 498 499 @Override 500 public void loadMessageForViewCallback(MessagingException result, long accountId, 501 long messageId, int progress) { 502 handleError(result, accountId, progress); 503 } 504 505 private void handleError(final MessagingException result, final long accountId, 506 int progress) { 507 if (accountId == -1) { 508 return; 509 } 510 if (result == null) { 511 if (progress > 0) { 512 // Connection now working; clear the error message banner 513 if (mLastErrorAccountId == accountId) { 514 dismissErrorMessage(); 515 } 516 } 517 } else { 518 // Connection error; show the error message banner 519 new EmailAsyncTask<Void, Void, String>(mTaskTracker) { 520 @Override 521 protected String doInBackground(Void... params) { 522 Account account = 523 Account.restoreAccountWithId(EmailActivity.this, accountId); 524 return (account == null) ? null : account.mDisplayName; 525 } 526 527 @Override 528 protected void onPostExecute(String accountName) { 529 String message = 530 MessagingExceptionStrings.getErrorString(EmailActivity.this, result); 531 if (!TextUtils.isEmpty(accountName)) { 532 // TODO Use properly designed layout. Don't just concatenate strings; 533 // which is generally poor for I18N. 534 message = message + " (" + accountName + ")"; 535 } 536 if (mErrorBanner.show(message)) { 537 mLastErrorAccountId = accountId; 538 } 539 } 540 }.executeParallel(); 541 } 542 } 543 } 544} 545