1/******************************************************************************* 2 * Copyright (C) 2012 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 *******************************************************************************/ 17 18package com.android.mail.ui; 19 20import android.app.Dialog; 21import android.app.LoaderManager; 22import android.content.ContentResolver; 23import android.content.Context; 24import android.content.Intent; 25import android.content.res.Configuration; 26import android.nfc.NdefMessage; 27import android.nfc.NdefRecord; 28import android.nfc.NfcAdapter; 29import android.nfc.NfcEvent; 30import android.os.Bundle; 31import android.support.v7.app.ActionBar; 32import android.support.v7.widget.Toolbar; 33import android.view.DragEvent; 34import android.view.KeyEvent; 35import android.view.Menu; 36import android.view.MenuItem; 37import android.view.MotionEvent; 38import android.view.accessibility.AccessibilityManager; 39 40import com.android.bitmap.BitmapCache; 41import com.android.bitmap.UnrefedBitmapCache; 42import com.android.mail.R; 43import com.android.mail.analytics.AnalyticsTimer; 44import com.android.mail.bitmap.ContactResolver; 45import com.android.mail.compose.ComposeActivity; 46import com.android.mail.providers.Account; 47import com.android.mail.providers.Folder; 48import com.android.mail.utils.StorageLowState; 49import com.android.mail.utils.Utils; 50 51import java.io.UnsupportedEncodingException; 52import java.net.URLEncoder; 53 54/** 55 * This is the root activity container that holds the left navigation fragment 56 * (usually a list of folders), and the main content fragment (either a 57 * conversation list or a conversation view). 58 */ 59public class MailActivity extends AbstractMailActivity implements ControllableActivity { 60 61 /** 339KB cache fits 10 bitmaps at 33856 bytes each. */ 62 private static final int SENDERS_IMAGES_CACHE_TARGET_SIZE_BYTES = 1024 * 339; 63 private static final float SENDERS_IMAGES_PREVIEWS_CACHE_NON_POOLED_FRACTION = 0f; 64 /** Each string has upper estimate of 50 bytes, so this cache would be 5KB. */ 65 private static final int SENDERS_IMAGES_PREVIEWS_CACHE_NULL_CAPACITY = 100; 66 67 /** 68 * The activity controller to which we delegate most Activity lifecycle events. 69 */ 70 private ActivityController mController; 71 72 private ViewMode mViewMode; 73 74 private ToastBarOperation mPendingToastOp; 75 private boolean mAccessibilityEnabled; 76 private AccessibilityManager mAccessibilityManager; 77 78 protected ConversationListHelper mConversationListHelper; 79 80 /** 81 * The account name currently in use. Used to construct the NFC mailto: message. This needs 82 * to be static since the {@link ComposeActivity} needs to statically change the account name 83 * and have the NFC message changed accordingly. 84 */ 85 protected static String sAccountName = null; 86 87 private BitmapCache mSendersImageCache; 88 89 /** 90 * Create an NFC message (in the NDEF: Nfc Data Exchange Format) to instruct the recepient to 91 * send an email to the current account. 92 */ 93 private static class NdefMessageMaker implements NfcAdapter.CreateNdefMessageCallback { 94 @Override 95 public NdefMessage createNdefMessage(NfcEvent event) { 96 if (sAccountName == null) { 97 return null; 98 } 99 return getMailtoNdef(sAccountName); 100 } 101 102 /** 103 * Returns an NDEF message with a single mailto URI record 104 * for the given email address. 105 */ 106 private static NdefMessage getMailtoNdef(String account) { 107 byte[] accountBytes; 108 try { 109 accountBytes = URLEncoder.encode(account, "UTF-8").getBytes("UTF-8"); 110 } catch (UnsupportedEncodingException e) { 111 accountBytes = account.getBytes(); 112 } 113 byte prefix = 0x06; // mailto: 114 byte[] recordBytes = new byte[accountBytes.length + 1]; 115 recordBytes[0] = prefix; 116 System.arraycopy(accountBytes, 0, recordBytes, 1, accountBytes.length); 117 NdefRecord mailto = new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_URI, 118 new byte[0], recordBytes); 119 return new NdefMessage(new NdefRecord[] { mailto }); 120 } 121 } 122 123 private final NdefMessageMaker mNdefHandler = new NdefMessageMaker(); 124 125 public MailActivity() { 126 super(); 127 mConversationListHelper = new ConversationListHelper(); 128 } 129 130 @Override 131 public boolean dispatchTouchEvent(MotionEvent ev) { 132 mController.onTouchEvent(ev); 133 return super.dispatchTouchEvent(ev); 134 } 135 136 /** 137 * Default implementation returns a null view mode. 138 */ 139 @Override 140 public ViewMode getViewMode() { 141 return mViewMode; 142 } 143 144 @Override 145 public void onActivityResult(int requestCode, int resultCode, Intent data) { 146 mController.onActivityResult(requestCode, resultCode, data); 147 } 148 149 @Override 150 public void onBackPressed() { 151 if (!mController.onBackPressed()) { 152 super.onBackPressed(); 153 } 154 } 155 156 @Override 157 public void onCreate(Bundle savedState) { 158 super.onCreate(savedState); 159 // Log the start time if this is launched from the launcher with no saved states 160 Intent i = getIntent(); 161 if (i != null && i.getCategories() != null && 162 i.getCategories().contains(Intent.CATEGORY_LAUNCHER)) { 163 AnalyticsTimer.getInstance().trackStart(AnalyticsTimer.COLD_START_LAUNCHER); 164 } 165 166 resetSenderImageCache(); 167 mViewMode = new ViewMode(); 168 final boolean tabletUi = Utils.useTabletUI(this.getResources()); 169 mController = ControllerFactory.forActivity(this, mViewMode, tabletUi); 170 171 setContentView(mController.getContentViewResource()); 172 173 final Toolbar toolbar = (Toolbar) findViewById(R.id.mail_toolbar); 174 // Toolbar is currently only used on phone layout, so this is expected to be null 175 // on tablets 176 if (toolbar != null) { 177 setSupportActionBar(toolbar); 178 toolbar.setNavigationOnClickListener(mController.getNavigationViewClickListener()); 179 } 180 181 final ActionBar actionBar = getSupportActionBar(); 182 if (actionBar != null) { 183 // Hide the app icon. 184 actionBar.setIcon(android.R.color.transparent); 185 actionBar.setDisplayUseLogoEnabled(false); 186 } 187 188 // Must be done after setting up action bar 189 mController.onCreate(savedState); 190 191 mAccessibilityManager = 192 (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE); 193 mAccessibilityEnabled = mAccessibilityManager.isEnabled(); 194 final NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); 195 if (nfcAdapter != null) { 196 nfcAdapter.setNdefPushMessageCallback(mNdefHandler, this); 197 } 198 } 199 200 @Override 201 protected void onPostCreate(Bundle savedInstanceState) { 202 super.onPostCreate(savedInstanceState); 203 204 mController.onPostCreate(savedInstanceState); 205 } 206 207 @Override 208 public void onConfigurationChanged(Configuration newConfig) { 209 super.onConfigurationChanged(newConfig); 210 mController.onConfigurationChanged(newConfig); 211 } 212 213 @Override 214 protected void onRestart() { 215 super.onRestart(); 216 mController.onRestart(); 217 } 218 219 /** 220 * Constructs and sets the default NFC message. This message instructs the receiver to send 221 * email to the account provided as the argument. This message is to be shared with 222 * "zero-clicks" using NFC. The message will be available as long as the current activity is in 223 * the foreground. 224 * 225 * @param account The email address to send mail to. 226 */ 227 public static void setNfcMessage(String account) { 228 sAccountName = account; 229 } 230 231 @Override 232 protected void onRestoreInstanceState(Bundle savedInstanceState) { 233 super.onRestoreInstanceState(savedInstanceState); 234 mController.onRestoreInstanceState(savedInstanceState); 235 } 236 237 @Override 238 public Dialog onCreateDialog(int id, Bundle bundle) { 239 final Dialog dialog = mController.onCreateDialog(id, bundle); 240 return dialog == null ? super.onCreateDialog(id, bundle) : dialog; 241 } 242 243 @Override 244 public boolean onCreateOptionsMenu(Menu menu) { 245 return mController.onCreateOptionsMenu(menu) || super.onCreateOptionsMenu(menu); 246 } 247 248 @Override 249 public boolean onKeyDown(int keyCode, KeyEvent event) { 250 return mController.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event); 251 } 252 253 @Override 254 public boolean onOptionsItemSelected(MenuItem item) { 255 return mController.onOptionsItemSelected(item) || super.onOptionsItemSelected(item); 256 } 257 258 @Override 259 public void onPause() { 260 super.onPause(); 261 mController.onPause(); 262 } 263 264 @Override 265 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) { 266 super.onPrepareDialog(id, dialog, bundle); 267 mController.onPrepareDialog(id, dialog, bundle); 268 } 269 270 @Override 271 public boolean onPrepareOptionsMenu(Menu menu) { 272 mController.onPrepareOptionsMenu(menu); 273 return super.onPrepareOptionsMenu(menu); 274 } 275 276 @Override 277 public void onResume() { 278 super.onResume(); 279 mController.onResume(); 280 final boolean enabled = mAccessibilityManager.isEnabled(); 281 if (enabled != mAccessibilityEnabled) { 282 onAccessibilityStateChanged(enabled); 283 } 284 // App has resumed, re-check the top-level storage situation. 285 StorageLowState.checkStorageLowMode(this); 286 } 287 288 @Override 289 public void onSaveInstanceState(Bundle outState) { 290 super.onSaveInstanceState(outState); 291 mController.onSaveInstanceState(outState); 292 } 293 294 @Override 295 protected void onStart() { 296 super.onStart(); 297 mController.onStart(); 298 } 299 300 @Override 301 public boolean onSearchRequested() { 302 mController.startSearch(); 303 return true; 304 } 305 306 @Override 307 protected void onStop() { 308 super.onStop(); 309 mController.onStop(); 310 } 311 312 @Override 313 protected void onDestroy() { 314 super.onDestroy(); 315 mController.onDestroy(); 316 } 317 318 @Override 319 public void onWindowFocusChanged(boolean hasFocus) { 320 super.onWindowFocusChanged(hasFocus); 321 mController.onWindowFocusChanged(hasFocus); 322 } 323 324 @Override 325 public String toString() { 326 final StringBuilder sb = new StringBuilder(super.toString()); 327 sb.append("{ViewMode="); 328 sb.append(mViewMode); 329 sb.append(" controller="); 330 sb.append(mController); 331 sb.append("}"); 332 return sb.toString(); 333 } 334 335 @Override 336 public ConversationListCallbacks getListHandler() { 337 return mController; 338 } 339 340 @Override 341 public FolderChangeListener getFolderChangeListener() { 342 return mController; 343 } 344 345 @Override 346 public FolderSelector getFolderSelector() { 347 return mController; 348 } 349 350 @Override 351 public FolderController getFolderController() { 352 return mController; 353 } 354 355 @Override 356 public ConversationSelectionSet getSelectedSet() { 357 return mController.getSelectedSet(); 358 } 359 360 @Override 361 public boolean supportsDrag(DragEvent event, Folder folder) { 362 return mController.supportsDrag(event, folder); 363 } 364 365 @Override 366 public void handleDrop(DragEvent event, Folder folder) { 367 mController.handleDrop(event, folder); 368 } 369 370 @Override 371 public void onUndoAvailable(ToastBarOperation undoOp) { 372 mController.onUndoAvailable(undoOp); 373 } 374 375 @Override 376 public Folder getHierarchyFolder() { 377 return mController.getHierarchyFolder(); 378 } 379 380 @Override 381 public ConversationUpdater getConversationUpdater() { 382 return mController; 383 } 384 385 @Override 386 public ErrorListener getErrorListener() { 387 return mController; 388 } 389 390 @Override 391 public void setPendingToastOperation(ToastBarOperation op) { 392 mPendingToastOp = op; 393 } 394 395 @Override 396 public ToastBarOperation getPendingToastOperation() { 397 return mPendingToastOp; 398 } 399 400 @Override 401 public void onAnimationEnd(AnimatedAdapter animatedAdapter) { 402 mController.onAnimationEnd(animatedAdapter); 403 } 404 405 @Override 406 public AccountController getAccountController() { 407 return mController; 408 } 409 410 @Override 411 public RecentFolderController getRecentFolderController() { 412 return mController; 413 } 414 415 @Override 416 public DrawerController getDrawerController() { 417 return mController.getDrawerController(); 418 } 419 420 @Override 421 public KeyboardNavigationController getKeyboardNavigationController() { 422 return mController; 423 } 424 425 @Override 426 public void onFooterViewErrorActionClick(Folder folder, int errorStatus) { 427 mController.onFooterViewErrorActionClick(folder, errorStatus); 428 } 429 430 @Override 431 public void onFooterViewLoadMoreClick(Folder folder) { 432 mController.onFooterViewLoadMoreClick(folder); 433 } 434 435 @Override 436 public void startDragMode() { 437 mController.startDragMode(); 438 } 439 440 @Override 441 public void stopDragMode() { 442 mController.stopDragMode(); 443 } 444 445 @Override 446 public boolean isAccessibilityEnabled() { 447 return mAccessibilityEnabled; 448 } 449 450 public void onAccessibilityStateChanged(boolean enabled) { 451 mAccessibilityEnabled = enabled; 452 mController.onAccessibilityStateChanged(); 453 } 454 455 @Override 456 public final ConversationListHelper getConversationListHelper() { 457 return mConversationListHelper; 458 } 459 460 @Override 461 public FragmentLauncher getFragmentLauncher() { 462 return mController; 463 } 464 465 @Override 466 public ContactLoaderCallbacks getContactLoaderCallbacks() { 467 return new ContactLoaderCallbacks(getActivityContext()); 468 } 469 470 @Override 471 public ContactResolver getContactResolver(ContentResolver resolver, BitmapCache bitmapCache) { 472 return new ContactResolver(resolver, bitmapCache); 473 } 474 475 @Override 476 public BitmapCache getSenderImageCache() { 477 return mSendersImageCache; 478 } 479 480 @Override 481 public void resetSenderImageCache() { 482 mSendersImageCache = createNewSenderImageCache(); 483 } 484 485 private BitmapCache createNewSenderImageCache() { 486 return new UnrefedBitmapCache(Utils.isLowRamDevice(this) ? 487 0 : SENDERS_IMAGES_CACHE_TARGET_SIZE_BYTES, 488 SENDERS_IMAGES_PREVIEWS_CACHE_NON_POOLED_FRACTION, 489 SENDERS_IMAGES_PREVIEWS_CACHE_NULL_CAPACITY); 490 } 491 492 @Override 493 public void showHelp(Account account, int viewMode) { 494 int helpContext = ViewMode.isConversationMode(viewMode) 495 ? R.string.conversation_view_help_context 496 : R.string.conversation_list_help_context; 497 Utils.showHelp(this, account, getString(helpContext)); 498 } 499 500 /** 501 * Returns the loader callback that can create a 502 * {@link AbstractActivityController#LOADER_WELCOME_TOUR_ACCOUNTS} followed by a 503 * {@link AbstractActivityController#LOADER_WELCOME_TOUR} which determines whether the welcome 504 * tour should be displayed. 505 * 506 * The base implementation returns {@code null} and subclasses should return an actual 507 * implementation if they want to be invoked at appropriate time. 508 */ 509 public LoaderManager.LoaderCallbacks<?> getWelcomeCallbacks() { 510 return null; 511 } 512 513 /** 514 * Returns whether the latest version of the welcome tour was shown on this device. 515 * <p> 516 * The base implementation returns {@code true} and applications that implement a welcome tour 517 * should override this method in order to optimize 518 * {@link AbstractActivityController#perhapsStartWelcomeTour()}. 519 * 520 * @return Whether the latest version of the welcome tour was shown. 521 */ 522 public boolean wasLatestWelcomeTourShownOnDeviceForAllAccounts() { 523 return true; 524 } 525} 526