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