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