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