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