EmailActivity.java revision d6decef1d2a8d14aa8a65229bc784e6fdbb31864
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.Fragment; 21import android.content.Intent; 22import android.content.res.Configuration; 23import android.os.Bundle; 24import android.os.Handler; 25import android.text.TextUtils; 26import android.util.Log; 27import android.view.Menu; 28import android.view.MenuItem; 29import android.view.View; 30import android.widget.TextView; 31 32import com.android.email.Controller; 33import com.android.email.ControllerResultUiThreadWrapper; 34import com.android.email.Email; 35import com.android.email.MessageListContext; 36import com.android.email.MessagingExceptionStrings; 37import com.android.email.R; 38import com.android.emailcommon.Logging; 39import com.android.emailcommon.mail.MessagingException; 40import com.android.emailcommon.provider.Account; 41import com.android.emailcommon.provider.EmailContent.Message; 42import com.android.emailcommon.provider.Mailbox; 43import com.android.emailcommon.utility.EmailAsyncTask; 44import com.android.emailcommon.utility.IntentUtilities; 45import com.google.common.base.Preconditions; 46 47import java.util.ArrayList; 48 49/** 50 * The main Email activity, which is used on both the tablet and the phone. 51 * 52 * Because this activity is device agnostic, so most of the UI aren't owned by this, but by 53 * the UIController. 54 */ 55public class EmailActivity extends Activity implements View.OnClickListener, FragmentInstallable { 56 public static final String EXTRA_ACCOUNT_ID = "ACCOUNT_ID"; 57 public static final String EXTRA_MAILBOX_ID = "MAILBOX_ID"; 58 public static final String EXTRA_MESSAGE_ID = "MESSAGE_ID"; 59 public static final String EXTRA_QUERY_STRING = "QUERY_STRING"; 60 61 /** Loader IDs starting with this is safe to use from UIControllers. */ 62 static final int UI_CONTROLLER_LOADER_ID_BASE = 100; 63 64 /** Loader IDs starting with this is safe to use from ActionBarController. */ 65 static final int ACTION_BAR_CONTROLLER_LOADER_ID_BASE = 200; 66 67 private static float sLastFontScale = -1; 68 69 private Controller mController; 70 private Controller.Result mControllerResult; 71 72 private UIControllerBase mUIController; 73 74 private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker(); 75 76 /** Banner to display errors */ 77 private BannerController mErrorBanner; 78 /** Id of the account that had a messaging exception most recently. */ 79 private long mLastErrorAccountId; 80 81 /** 82 * Create an intent to launch and open account's inbox. 83 * 84 * @param accountId If -1, default account will be used. 85 */ 86 public static Intent createOpenAccountIntent(Activity fromActivity, long accountId) { 87 Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class); 88 if (accountId != -1) { 89 i.putExtra(EXTRA_ACCOUNT_ID, accountId); 90 } 91 return i; 92 } 93 94 /** 95 * Create an intent to launch and open a mailbox. 96 * 97 * @param accountId must not be -1. 98 * @param mailboxId must not be -1. Magic mailboxes IDs (such as 99 * {@link Mailbox#QUERY_ALL_INBOXES}) don't work. 100 */ 101 public static Intent createOpenMailboxIntent(Activity fromActivity, long accountId, 102 long mailboxId) { 103 if (accountId == -1 || mailboxId == -1) { 104 throw new IllegalArgumentException(); 105 } 106 Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class); 107 i.putExtra(EXTRA_ACCOUNT_ID, accountId); 108 i.putExtra(EXTRA_MAILBOX_ID, mailboxId); 109 return i; 110 } 111 112 /** 113 * Create an intent to launch and open a message. 114 * 115 * @param accountId must not be -1. 116 * @param mailboxId must not be -1. Magic mailboxes IDs (such as 117 * {@link Mailbox#QUERY_ALL_INBOXES}) don't work. 118 * @param messageId must not be -1. 119 */ 120 public static Intent createOpenMessageIntent(Activity fromActivity, long accountId, 121 long mailboxId, long messageId) { 122 if (accountId == -1 || mailboxId == -1 || messageId == -1) { 123 throw new IllegalArgumentException(); 124 } 125 Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class); 126 i.putExtra(EXTRA_ACCOUNT_ID, accountId); 127 i.putExtra(EXTRA_MAILBOX_ID, mailboxId); 128 i.putExtra(EXTRA_MESSAGE_ID, messageId); 129 return i; 130 } 131 132 /** 133 * Create an intent to launch search activity. 134 * 135 * @param accountId ID of the account for the mailbox. Must not be {@link Account#NO_ACCOUNT}. 136 * @param mailboxId ID of the mailbox to search, or {@link Mailbox#NO_MAILBOX} to perform 137 * global search. 138 * @param query query string. 139 */ 140 public static Intent createSearchIntent(Activity fromActivity, long accountId, 141 long mailboxId, String query) { 142 Preconditions.checkArgument(Account.isNormalAccount(accountId), 143 "Can only search in normal accounts"); 144 145 // Note that a search doesn't use a restart intent, as we want another instance of 146 // the activity to sit on the stack for search. 147 Intent i = new Intent(fromActivity, EmailActivity.class); 148 i.putExtra(EXTRA_ACCOUNT_ID, accountId); 149 i.putExtra(EXTRA_MAILBOX_ID, mailboxId); 150 i.putExtra(EXTRA_QUERY_STRING, query); 151 i.setAction(Intent.ACTION_SEARCH); 152 return i; 153 } 154 155 /** 156 * Initialize {@link #mUIController}. 157 */ 158 private void initUIController() { 159 if (UiUtilities.useTwoPane(this)) { 160 if (getIntent().getAction() != null 161 && Intent.ACTION_SEARCH.equals(getIntent().getAction()) 162 && !UiUtilities.showTwoPaneSearchResults(this)) { 163 mUIController = new UIControllerSearchTwoPane(this); 164 } else { 165 mUIController = new UIControllerTwoPane(this); 166 } 167 } else { 168 mUIController = new UIControllerOnePane(this); 169 } 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 float fontScale = getResources().getConfiguration().fontScale; 177 if (sLastFontScale != -1 && sLastFontScale != fontScale) { 178 // If the font scale has been initialized, and has been detected to be different than 179 // the last time the Activity ran, it means the user changed the font while no 180 // Email Activity was running - we still need to purge static information though. 181 onFontScaleChangeDetected(); 182 } 183 sLastFontScale = fontScale; 184 185 // UIController is used in onPrepareOptionsMenu(), which can be called from within 186 // super.onCreate(), so we need to initialize it here. 187 initUIController(); 188 189 super.onCreate(savedInstanceState); 190 ActivityHelper.debugSetWindowFlags(this); 191 setContentView(mUIController.getLayoutId()); 192 193 mUIController.onActivityViewReady(); 194 195 mController = Controller.getInstance(this); 196 mControllerResult = new ControllerResultUiThreadWrapper<ControllerResult>(new Handler(), 197 new ControllerResult()); 198 mController.addResultCallback(mControllerResult); 199 200 // Set up views 201 // TODO Probably better to extract mErrorMessageView related code into a separate class, 202 // so that it'll be easy to reuse for the phone activities. 203 TextView errorMessage = (TextView) findViewById(R.id.error_message); 204 errorMessage.setOnClickListener(this); 205 int errorBannerHeight = getResources().getDimensionPixelSize(R.dimen.error_message_height); 206 mErrorBanner = new BannerController(this, errorMessage, errorBannerHeight); 207 208 if (savedInstanceState != null) { 209 mUIController.onRestoreInstanceState(savedInstanceState); 210 } else { 211 final Intent intent = getIntent(); 212 final MessageListContext viewContext = MessageListContext.forIntent(this, intent); 213 if (viewContext == null) { 214 // This might happen if accounts were deleted on another thread, and there aren't 215 // any remaining 216 Welcome.actionStart(this); 217 finish(); 218 return; 219 } else { 220 final long messageId = intent.getLongExtra(EXTRA_MESSAGE_ID, Message.NO_MESSAGE); 221 mUIController.open(viewContext, messageId); 222 } 223 } 224 mUIController.onActivityCreated(); 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 // FragmentInstallable 237 @Override 238 public void onInstallFragment(Fragment fragment) { 239 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 240 Log.d(Logging.LOG_TAG, this + " onInstallFragment fragment=" + fragment); 241 } 242 mUIController.onInstallFragment(fragment); 243 } 244 245 // FragmentInstallable 246 @Override 247 public void onUninstallFragment(Fragment fragment) { 248 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 249 Log.d(Logging.LOG_TAG, this + " onUninstallFragment fragment=" + fragment); 250 } 251 mUIController.onUninstallFragment(fragment); 252 } 253 254 @Override 255 protected void onStart() { 256 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStart"); 257 super.onStart(); 258 mUIController.onActivityStart(); 259 } 260 261 @Override 262 protected void onResume() { 263 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onResume"); 264 super.onResume(); 265 mUIController.onActivityResume(); 266 /** 267 * In {@link MessageList#onResume()}, we go back to {@link Welcome} if an account 268 * has been added/removed. We don't need to do that here, because we fetch the most 269 * up-to-date account list. Additionally, we detect and do the right thing if all 270 * of the accounts have been removed. 271 */ 272 } 273 274 @Override 275 protected void onPause() { 276 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onPause"); 277 super.onPause(); 278 mUIController.onActivityPause(); 279 } 280 281 @Override 282 protected void onStop() { 283 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStop"); 284 super.onStop(); 285 mUIController.onActivityStop(); 286 } 287 288 @Override 289 protected void onDestroy() { 290 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onDestroy"); 291 mController.removeResultCallback(mControllerResult); 292 mTaskTracker.cancellAllInterrupt(); 293 mUIController.onActivityDestroy(); 294 super.onDestroy(); 295 } 296 297 @Override 298 public void onBackPressed() { 299 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 300 Log.d(Logging.LOG_TAG, this + " onBackPressed"); 301 } 302 if (!mUIController.onBackPressed(true)) { 303 // Not handled by UIController -- perform the default. i.e. close the app. 304 super.onBackPressed(); 305 } 306 } 307 308 @Override 309 public void onClick(View v) { 310 switch (v.getId()) { 311 case R.id.error_message: 312 dismissErrorMessage(); 313 break; 314 } 315 } 316 317 /** 318 * Force dismiss the error banner. 319 */ 320 private void dismissErrorMessage() { 321 mErrorBanner.dismiss(); 322 } 323 324 @Override 325 public boolean onCreateOptionsMenu(Menu menu) { 326 return mUIController.onCreateOptionsMenu(getMenuInflater(), menu); 327 } 328 329 @Override 330 public boolean onPrepareOptionsMenu(Menu menu) { 331 return mUIController.onPrepareOptionsMenu(getMenuInflater(), menu); 332 } 333 334 /** 335 * Called when the search key is pressd. 336 * 337 * Use the below command to emulate the key press on devices without the search key. 338 * adb shell input keyevent 84 339 */ 340 @Override 341 public boolean onSearchRequested() { 342 if (Email.DEBUG) { 343 Log.d(Logging.LOG_TAG, this + " onSearchRequested"); 344 } 345 mUIController.onSearchRequested(); 346 return true; // Event handled. 347 } 348 349 @Override 350 @SuppressWarnings("deprecation") 351 public boolean onOptionsItemSelected(MenuItem item) { 352 if (mUIController.onOptionsItemSelected(item)) { 353 return true; 354 } 355 return super.onOptionsItemSelected(item); 356 } 357 358 /** 359 * A {@link Controller.Result} to detect connection status. 360 */ 361 private class ControllerResult extends Controller.Result { 362 @Override 363 public void sendMailCallback( 364 MessagingException result, long accountId, long messageId, int progress) { 365 handleError(result, accountId, progress); 366 } 367 368 @Override 369 public void serviceCheckMailCallback( 370 MessagingException result, long accountId, long mailboxId, int progress, long tag) { 371 handleError(result, accountId, progress); 372 } 373 374 @Override 375 public void updateMailboxCallback(MessagingException result, long accountId, long mailboxId, 376 int progress, int numNewMessages, ArrayList<Long> addedMessages) { 377 handleError(result, accountId, progress); 378 } 379 380 @Override 381 public void updateMailboxListCallback( 382 MessagingException result, long accountId, int progress) { 383 handleError(result, accountId, progress); 384 } 385 386 @Override 387 public void loadAttachmentCallback(MessagingException result, long accountId, 388 long messageId, long attachmentId, int progress) { 389 handleError(result, accountId, progress); 390 } 391 392 @Override 393 public void loadMessageForViewCallback(MessagingException result, long accountId, 394 long messageId, int progress) { 395 handleError(result, accountId, progress); 396 } 397 398 private void handleError(final MessagingException result, final long accountId, 399 int progress) { 400 if (accountId == -1) { 401 return; 402 } 403 if (result == null) { 404 if (progress > 0) { 405 // Connection now working; clear the error message banner 406 if (mLastErrorAccountId == accountId) { 407 dismissErrorMessage(); 408 } 409 } 410 } else { 411 Account account = Account.restoreAccountWithId(EmailActivity.this, accountId); 412 if (account == null) return; 413 String message = 414 MessagingExceptionStrings.getErrorString(EmailActivity.this, result); 415 if (!TextUtils.isEmpty(account.mDisplayName)) { 416 // TODO Use properly designed layout. Don't just concatenate strings; 417 // which is generally poor for I18N. 418 message = message + " (" + account.mDisplayName + ")"; 419 } 420 if (mErrorBanner.show(message)) { 421 mLastErrorAccountId = accountId; 422 } 423 } 424 } 425 } 426 427 /** 428 * Handle a change to the system font size. This invalidates some static caches we have. 429 */ 430 private void onFontScaleChangeDetected() { 431 MessageListItem.resetDrawingCaches(); 432 } 433} 434