MessageViewFragmentBase.java revision 99930f81f1b7f1d5b769be958b3d1efe0aeef3fc
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 com.android.email.AttachmentInfo;
20import com.android.email.Controller;
21import com.android.email.ControllerResultUiThreadWrapper;
22import com.android.email.Email;
23import com.android.email.UiUtilities;
24import com.android.email.Preferences;
25import com.android.email.R;
26import com.android.email.Throttle;
27import com.android.email.mail.internet.EmailHtmlUtil;
28import com.android.email.service.AttachmentDownloadService;
29import com.android.emailcommon.Logging;
30import com.android.emailcommon.mail.Address;
31import com.android.emailcommon.mail.MessagingException;
32import com.android.emailcommon.provider.EmailContent.Attachment;
33import com.android.emailcommon.provider.EmailContent.Body;
34import com.android.emailcommon.provider.EmailContent.Mailbox;
35import com.android.emailcommon.provider.EmailContent.Message;
36import com.android.emailcommon.utility.AttachmentUtilities;
37import com.android.emailcommon.utility.EmailAsyncTask;
38import com.android.emailcommon.utility.Utility;
39
40import org.apache.commons.io.IOUtils;
41
42import android.app.Activity;
43import android.app.DownloadManager;
44import android.app.Fragment;
45import android.app.LoaderManager.LoaderCallbacks;
46import android.content.ActivityNotFoundException;
47import android.content.ContentResolver;
48import android.content.ContentUris;
49import android.content.Context;
50import android.content.Intent;
51import android.content.Loader;
52import android.content.pm.PackageManager;
53import android.content.res.Resources;
54import android.database.ContentObserver;
55import android.graphics.Bitmap;
56import android.graphics.BitmapFactory;
57import android.media.MediaScannerConnection;
58import android.net.Uri;
59import android.os.AsyncTask;
60import android.os.Bundle;
61import android.os.Environment;
62import android.os.Handler;
63import android.provider.ContactsContract;
64import android.provider.ContactsContract.QuickContact;
65import android.text.SpannableStringBuilder;
66import android.text.TextUtils;
67import android.text.format.DateUtils;
68import android.util.Log;
69import android.util.Patterns;
70import android.view.LayoutInflater;
71import android.view.View;
72import android.view.ViewGroup;
73import android.webkit.WebSettings;
74import android.webkit.WebView;
75import android.webkit.WebViewClient;
76import android.widget.Button;
77import android.widget.ImageView;
78import android.widget.LinearLayout;
79import android.widget.ProgressBar;
80import android.widget.TextView;
81
82import java.io.File;
83import java.io.FileOutputStream;
84import java.io.IOException;
85import java.io.InputStream;
86import java.io.OutputStream;
87import java.util.Formatter;
88import java.util.regex.Matcher;
89import java.util.regex.Pattern;
90
91// TODO Better handling of config changes.
92// - Retain the content; don't kick 3 async tasks every time
93
94/**
95 * Base class for {@link MessageViewFragment} and {@link MessageFileViewFragment}.
96 *
97 * See {@link MessageViewBase} for the class relation diagram.
98 */
99public abstract class MessageViewFragmentBase extends Fragment implements View.OnClickListener {
100    private static final String BUNDLE_KEY_CURRENT_TAB = "MessageViewFragmentBase.currentTab";
101    private static final String BUNDLE_KEY_PICTURE_LOADED = "MessageViewFragmentBase.pictureLoaded";
102    private static final int PHOTO_LOADER_ID = 1;
103    private Context mContext;
104
105    // Regex that matches start of img tag. '<(?i)img\s+'.
106    private static final Pattern IMG_TAG_START_REGEX = Pattern.compile("<(?i)img\\s+");
107    // Regex that matches Web URL protocol part as case insensitive.
108    private static final Pattern WEB_URL_PROTOCOL = Pattern.compile("(?i)http|https://");
109
110    private static int PREVIEW_ICON_WIDTH = 62;
111    private static int PREVIEW_ICON_HEIGHT = 62;
112
113    private TextView mSubjectView;
114    private TextView mFromNameView;
115    private TextView mFromAddressView;
116    private TextView mDateTimeView;
117    private TextView mAddressesView;
118    private WebView mMessageContentView;
119    private LinearLayout mAttachments;
120    private View mTabSection;
121    private ImageView mFromBadge;
122    private ImageView mSenderPresenceView;
123    private View mMainView;
124    private View mLoadingProgress;
125    private Button mShowDetailsButton;
126
127    private TextView mMessageTab;
128    private TextView mAttachmentTab;
129    private TextView mInviteTab;
130    // It is not really a tab, but looks like one of them.
131    private TextView mShowPicturesTab;
132
133    private View mAttachmentsScroll;
134    private View mInviteScroll;
135
136    private long mAccountId = -1;
137    private long mMessageId = -1;
138    private Message mMessage;
139
140    private LoadMessageTask mLoadMessageTask;
141    private ReloadMessageTask mReloadMessageTask;
142    private LoadBodyTask mLoadBodyTask;
143    private LoadAttachmentsTask mLoadAttachmentsTask;
144
145    private Controller mController;
146    private ControllerResultUiThreadWrapper<ControllerResults> mControllerCallback;
147
148    // contains the HTML body. Is used by LoadAttachmentTask to display inline images.
149    // is null most of the time, is used transiently to pass info to LoadAttachementTask
150    private String mHtmlTextRaw;
151
152    // contains the HTML content as set in WebView.
153    private String mHtmlTextWebView;
154
155    private boolean mResumed;
156    private boolean mLoadWhenResumed;
157
158    private boolean mIsMessageLoadedForTest;
159
160    private MessageObserver mMessageObserver;
161
162    private static final int CONTACT_STATUS_STATE_UNLOADED = 0;
163    private static final int CONTACT_STATUS_STATE_UNLOADED_TRIGGERED = 1;
164    private static final int CONTACT_STATUS_STATE_LOADED = 2;
165
166    private int mContactStatusState;
167    private Uri mQuickContactLookupUri;
168
169    /** Flag for {@link #mTabFlags}: Message has attachment(s) */
170    protected static final int TAB_FLAGS_HAS_ATTACHMENT = 1;
171
172    /**
173     * Flag for {@link #mTabFlags}: Message contains invite.  This flag is only set by
174     * {@link MessageViewFragment}.
175     */
176    protected static final int TAB_FLAGS_HAS_INVITE = 2;
177
178    /** Flag for {@link #mTabFlags}: Message contains pictures */
179    protected static final int TAB_FLAGS_HAS_PICTURES = 4;
180
181    /** Flag for {@link #mTabFlags}: "Show pictures" has already been pressed */
182    protected static final int TAB_FLAGS_PICTURE_LOADED = 8;
183
184    /**
185     * Flags to control the tabs.
186     * @see #updateTabs(int)
187     */
188    private int mTabFlags;
189
190    /** # of attachments in the current message */
191    private int mAttachmentCount;
192
193    // Use (random) large values, to avoid confusion with TAB_FLAGS_*
194    protected static final int TAB_MESSAGE = 101;
195    protected static final int TAB_INVITE = 102;
196    protected static final int TAB_ATTACHMENT = 103;
197    private static final int TAB_NONE = 0;
198
199    /** Current tab */
200    private int mCurrentTab = TAB_NONE;
201    /**
202     * Tab that was selected in the previous activity instance.
203     * Used to restore the current tab after screen rotation.
204     */
205    private int mRestoredTab = TAB_NONE;
206
207    private boolean mRestoredPictureLoaded;
208
209    private final EmailAsyncTask.Tracker mUpdatePreviewIconTaskTracker
210            = new EmailAsyncTask.Tracker();
211
212    /**
213     * Zoom scales for webview.  Values correspond to {@link Preferences#TEXT_ZOOM_TINY}..
214     * {@link Preferences#TEXT_ZOOM_HUGE}.
215     */
216    private static final float[] ZOOM_SCALE_ARRAY = new float[] {0.8f, 0.9f, 1.0f, 1.2f, 1.5f};
217
218    public interface Callback {
219        /** Called when the fragment is about to show up, or show a different message. */
220        public void onMessageViewShown(int mailboxType);
221
222        /** Called when the fragment is about to be destroyed. */
223        public void onMessageViewGone();
224
225        /**
226         * Called when a link in a message is clicked.
227         *
228         * @param url link url that's clicked.
229         * @return true if handled, false otherwise.
230         */
231        public boolean onUrlInMessageClicked(String url);
232
233        /**
234         * Called when the message specified doesn't exist, or is deleted/moved.
235         */
236        public void onMessageNotExists();
237
238        /** Called when it starts loading a message. */
239        public void onLoadMessageStarted();
240
241        /** Called when it successfully finishes loading a message. */
242        public void onLoadMessageFinished();
243
244        /** Called when an error occurred during loading a message. */
245        public void onLoadMessageError(String errorMessage);
246    }
247
248    public static class EmptyCallback implements Callback {
249        public static final Callback INSTANCE = new EmptyCallback();
250        @Override public void onMessageViewShown(int mailboxType) {}
251        @Override public void onMessageViewGone() {}
252        @Override public void onLoadMessageError(String errorMessage) {}
253        @Override public void onLoadMessageFinished() {}
254        @Override public void onLoadMessageStarted() {}
255        @Override public void onMessageNotExists() {}
256        @Override
257        public boolean onUrlInMessageClicked(String url) {
258            return false;
259        }
260    }
261
262    private Callback mCallback = EmptyCallback.INSTANCE;
263
264    @Override
265    public void onCreate(Bundle savedInstanceState) {
266        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
267            Log.d(Logging.LOG_TAG, "MessageViewFragment onCreate");
268        }
269        super.onCreate(savedInstanceState);
270
271        mContext = getActivity().getApplicationContext();
272
273        mControllerCallback = new ControllerResultUiThreadWrapper<ControllerResults>(
274                new Handler(), new ControllerResults());
275
276        mController = Controller.getInstance(mContext);
277        mMessageObserver = new MessageObserver(new Handler(), mContext);
278
279        if (savedInstanceState != null) {
280            restoreInstanceState(savedInstanceState);
281        }
282    }
283
284    @Override
285    public View onCreateView(
286            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
287        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
288            Log.d(Logging.LOG_TAG, "MessageViewFragment onCreateView");
289        }
290        final View view = inflater.inflate(R.layout.message_view_fragment, container, false);
291
292        mSubjectView = (TextView) view.findViewById(R.id.subject);
293        mFromNameView = (TextView) view.findViewById(R.id.from_name);
294        mFromAddressView = (TextView) view.findViewById(R.id.from_address);
295        mAddressesView = (TextView) view.findViewById(R.id.addresses);
296        mDateTimeView = (TextView) view.findViewById(R.id.datetime);
297        mMessageContentView = (WebView) view.findViewById(R.id.message_content);
298        mAttachments = (LinearLayout) view.findViewById(R.id.attachments);
299        mTabSection = view.findViewById(R.id.message_tabs_section);
300        mFromBadge = (ImageView) view.findViewById(R.id.badge);
301        mSenderPresenceView = (ImageView) view.findViewById(R.id.presence);
302        mMainView = view.findViewById(R.id.main_panel);
303        mLoadingProgress = view.findViewById(R.id.loading_progress);
304        mShowDetailsButton = (Button) view.findViewById(R.id.show_details);
305
306        mFromNameView.setOnClickListener(this);
307        mFromAddressView.setOnClickListener(this);
308        mFromBadge.setOnClickListener(this);
309        mSenderPresenceView.setOnClickListener(this);
310
311        mMessageTab = (TextView) view.findViewById(R.id.show_message);
312        mAttachmentTab = (TextView) view.findViewById(R.id.show_attachments);
313        mShowPicturesTab = (TextView) view.findViewById(R.id.show_pictures);
314        // Invite is only used in MessageViewFragment, but visibility is controlled here.
315        mInviteTab = (TextView) view.findViewById(R.id.show_invite);
316
317        mMessageTab.setOnClickListener(this);
318        mAttachmentTab.setOnClickListener(this);
319        mShowPicturesTab.setOnClickListener(this);
320        mInviteTab.setOnClickListener(this);
321        mShowDetailsButton.setOnClickListener(this);
322
323        mAttachmentsScroll = view.findViewById(R.id.attachments_scroll);
324        mInviteScroll = view.findViewById(R.id.invite_scroll);
325
326        WebSettings webSettings = mMessageContentView.getSettings();
327        boolean supportMultiTouch = mContext.getPackageManager()
328                .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH);
329        webSettings.setDisplayZoomControls(!supportMultiTouch);
330        webSettings.setSupportZoom(true);
331        webSettings.setBuiltInZoomControls(true);
332        mMessageContentView.setWebViewClient(new CustomWebViewClient());
333        return view;
334    }
335
336    @Override
337    public void onActivityCreated(Bundle savedInstanceState) {
338        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
339            Log.d(Logging.LOG_TAG, "MessageViewFragment onActivityCreated");
340        }
341        super.onActivityCreated(savedInstanceState);
342        mController.addResultCallback(mControllerCallback);
343    }
344
345    @Override
346    public void onStart() {
347        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
348            Log.d(Logging.LOG_TAG, "MessageViewFragment onStart");
349        }
350        super.onStart();
351    }
352
353    @Override
354    public void onResume() {
355        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
356            Log.d(Logging.LOG_TAG, "MessageViewFragment onResume");
357        }
358        super.onResume();
359
360        mResumed = true;
361        if (isMessageSpecified()) {
362            if (mLoadWhenResumed) {
363                // Load content which resets all view state; including WebView zoom/pan and
364                // the current tab.
365                loadMessageIfResumed();
366            } else {
367                // We've comes back from other (full-screen) activities. Content has already
368                // been loaded, so don't load it again. However, we need to update the
369                // attachment tab as system settings may have been updated that affect which
370                // options are available to the user.
371                updateAttachmentTab();
372            }
373        }
374    }
375
376    @Override
377    public void onPause() {
378        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
379            Log.d(Logging.LOG_TAG, "MessageViewFragment onPause");
380        }
381        mResumed = false;
382        super.onPause();
383    }
384
385    @Override
386    public void onStop() {
387        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
388            Log.d(Logging.LOG_TAG, "MessageViewFragment onStop");
389        }
390        super.onStop();
391    }
392
393    @Override
394    public void onDestroy() {
395        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
396            Log.d(Logging.LOG_TAG, "MessageViewFragment onDestroy");
397        }
398        mCallback.onMessageViewGone();
399        mController.removeResultCallback(mControllerCallback);
400        clearContent();
401        mMessageContentView.destroy();
402        mMessageContentView = null;
403        super.onDestroy();
404    }
405
406    @Override
407    public void onSaveInstanceState(Bundle outState) {
408        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
409            Log.d(Logging.LOG_TAG, "MessageViewFragment onSaveInstanceState");
410        }
411        super.onSaveInstanceState(outState);
412        outState.putInt(BUNDLE_KEY_CURRENT_TAB, mCurrentTab);
413        outState.putBoolean(BUNDLE_KEY_PICTURE_LOADED, (mTabFlags & TAB_FLAGS_PICTURE_LOADED) != 0);
414    }
415
416    private void restoreInstanceState(Bundle state) {
417        // At this point (in onCreate) no tabs are visible (because we don't know if the message has
418        // an attachment or invite before loading it).  We just remember the tab here.
419        // We'll make it current when the tab first becomes visible in updateTabs().
420        mRestoredTab = state.getInt(BUNDLE_KEY_CURRENT_TAB);
421        mRestoredPictureLoaded = state.getBoolean(BUNDLE_KEY_PICTURE_LOADED);
422    }
423
424    public void setCallback(Callback callback) {
425        mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
426    }
427
428    private void cancelAllTasks() {
429        mMessageObserver.unregister();
430        mUpdatePreviewIconTaskTracker.cancellAllInterrupt();
431        Utility.cancelTaskInterrupt(mLoadMessageTask);
432        mLoadMessageTask = null;
433        Utility.cancelTaskInterrupt(mReloadMessageTask);
434        mReloadMessageTask = null;
435        Utility.cancelTaskInterrupt(mLoadBodyTask);
436        mLoadBodyTask = null;
437        Utility.cancelTaskInterrupt(mLoadAttachmentsTask);
438        mLoadAttachmentsTask = null;
439    }
440
441    /**
442     * Subclass returns true if which message to open is already specified by the activity.
443     */
444    protected abstract boolean isMessageSpecified();
445
446    protected final Controller getController() {
447        return mController;
448    }
449
450    protected final Callback getCallback() {
451        return mCallback;
452    }
453
454    protected final Message getMessage() {
455        return mMessage;
456    }
457
458    protected final boolean isMessageOpen() {
459        return mMessage != null;
460    }
461
462    /**
463     * Returns the account id of the current message, or -1 if unknown (message not open yet, or
464     * viewing an EML message).
465     */
466    public long getAccountId() {
467        return mAccountId;
468    }
469
470    /**
471     * Clear all the content -- should be called when the fragment is hidden.
472     */
473    public void clearContent() {
474        cancelAllTasks();
475        resetView();
476    }
477
478    protected final void loadMessageIfResumed() {
479        if (!mResumed) {
480            mLoadWhenResumed = true;
481            return;
482        }
483        mLoadWhenResumed = false;
484        cancelAllTasks();
485        resetView();
486        mLoadMessageTask = new LoadMessageTask(true);
487        mLoadMessageTask.execute();
488    }
489
490    /**
491     * Show/hide the content.  We hide all the content (except for the bottom buttons) when loading,
492     * to avoid flicker.
493     */
494    private void showContent(boolean showContent, boolean showProgressWhenHidden) {
495        if (mLoadingProgress == null) {
496            // Phone UI doesn't have it yet.
497            // TODO Add loading_progress and main_panel to the phone layout too.
498        } else {
499            makeVisible(mMainView, showContent);
500            makeVisible(mLoadingProgress, !showContent && showProgressWhenHidden);
501        }
502    }
503
504    protected void resetView() {
505        showContent(false, false);
506        updateTabs(0);
507        setCurrentTab(TAB_MESSAGE);
508        if (mMessageContentView != null) {
509            blockNetworkLoads(true);
510            mMessageContentView.scrollTo(0, 0);
511            mMessageContentView.clearView();
512
513            // Dynamic configuration of WebView
514            final WebSettings settings = mMessageContentView.getSettings();
515            settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);
516            mMessageContentView.setInitialScale(getWebViewZoom());
517        }
518        mAttachmentsScroll.scrollTo(0, 0);
519        mInviteScroll.scrollTo(0, 0);
520        mAttachments.removeAllViews();
521        mAttachments.setVisibility(View.GONE);
522        initContactStatusViews();
523    }
524
525    /**
526     * Returns the zoom scale (in percent) which is a combination of the user setting
527     * (tiny, small, normal, large, huge) and the device density. The intention
528     * is for the text to be physically equal in size over different density
529     * screens.
530     */
531    private int getWebViewZoom() {
532        float density = mContext.getResources().getDisplayMetrics().density;
533        int zoom = Preferences.getPreferences(mContext).getTextZoom();
534        return (int) (ZOOM_SCALE_ARRAY[zoom] * density * 100);
535    }
536
537    private void initContactStatusViews() {
538        mContactStatusState = CONTACT_STATUS_STATE_UNLOADED;
539        mQuickContactLookupUri = null;
540        mSenderPresenceView.setImageResource(ContactStatusLoader.PRESENCE_UNKNOWN_RESOURCE_ID);
541        showDefaultQuickContactBadgeImage();
542    }
543
544    private void showDefaultQuickContactBadgeImage() {
545        mFromBadge.setImageResource(R.drawable.ic_contact_picture);
546    }
547
548    protected final void addTabFlags(int tabFlags) {
549        updateTabs(mTabFlags | tabFlags);
550    }
551
552    private final void clearTabFlags(int tabFlags) {
553        updateTabs(mTabFlags & ~tabFlags);
554    }
555
556    private void setAttachmentCount(int count) {
557        mAttachmentCount = count;
558        if (mAttachmentCount > 0) {
559            addTabFlags(TAB_FLAGS_HAS_ATTACHMENT);
560        } else {
561            clearTabFlags(TAB_FLAGS_HAS_ATTACHMENT);
562        }
563    }
564
565    private static void makeVisible(View v, boolean visible) {
566        final int visibility = visible ? View.VISIBLE : View.GONE;
567        if ((v != null) && (v.getVisibility() != visibility)) {
568            v.setVisibility(visibility);
569        }
570    }
571
572    private static boolean isVisible(View v) {
573        return (v != null) && (v.getVisibility() == View.VISIBLE);
574    }
575
576    /**
577     * Update the visual of the tabs.  (visibility, text, etc)
578     */
579    private void updateTabs(int tabFlags) {
580        mTabFlags = tabFlags;
581        boolean messageTabVisible = (tabFlags & (TAB_FLAGS_HAS_INVITE | TAB_FLAGS_HAS_ATTACHMENT))
582                != 0;
583        makeVisible(mMessageTab, messageTabVisible);
584        makeVisible(mInviteTab, (tabFlags & TAB_FLAGS_HAS_INVITE) != 0);
585        makeVisible(mAttachmentTab, (tabFlags & TAB_FLAGS_HAS_ATTACHMENT) != 0);
586
587        final boolean hasPictures = (tabFlags & TAB_FLAGS_HAS_PICTURES) != 0;
588        final boolean pictureLoaded = (tabFlags & TAB_FLAGS_PICTURE_LOADED) != 0;
589        makeVisible(mShowPicturesTab, hasPictures && !pictureLoaded);
590
591        mAttachmentTab.setText(mContext.getResources().getQuantityString(
592                R.plurals.message_view_show_attachments_action,
593                mAttachmentCount, mAttachmentCount));
594
595        // Hide the entire section if no tabs are visible.
596        makeVisible(mTabSection, isVisible(mMessageTab) || isVisible(mInviteTab)
597                || isVisible(mAttachmentTab) || isVisible(mShowPicturesTab));
598
599        // Restore previously selected tab after rotation
600        if (mRestoredTab != TAB_NONE && isVisible(getTabViewForFlag(mRestoredTab))) {
601            setCurrentTab(mRestoredTab);
602            mRestoredTab = TAB_NONE;
603        }
604    }
605
606    /**
607     * Set the current tab.
608     *
609     * @param tab any of {@link #TAB_MESSAGE}, {@link #TAB_ATTACHMENT} or {@link #TAB_INVITE}.
610     */
611    private void setCurrentTab(int tab) {
612        mCurrentTab = tab;
613
614        // Hide & unselect all tabs
615        makeVisible(getTabContentViewForFlag(TAB_MESSAGE), false);
616        makeVisible(getTabContentViewForFlag(TAB_ATTACHMENT), false);
617        makeVisible(getTabContentViewForFlag(TAB_INVITE), false);
618        getTabViewForFlag(TAB_MESSAGE).setSelected(false);
619        getTabViewForFlag(TAB_ATTACHMENT).setSelected(false);
620        getTabViewForFlag(TAB_INVITE).setSelected(false);
621
622        makeVisible(getTabContentViewForFlag(mCurrentTab), true);
623        getTabViewForFlag(mCurrentTab).setSelected(true);
624    }
625
626    private View getTabViewForFlag(int tabFlag) {
627        switch (tabFlag) {
628            case TAB_MESSAGE:
629                return mMessageTab;
630            case TAB_ATTACHMENT:
631                return mAttachmentTab;
632            case TAB_INVITE:
633                return mInviteTab;
634        }
635        throw new IllegalArgumentException();
636    }
637
638    private View getTabContentViewForFlag(int tabFlag) {
639        switch (tabFlag) {
640            case TAB_MESSAGE:
641                return mMessageContentView;
642            case TAB_ATTACHMENT:
643                return mAttachmentsScroll;
644            case TAB_INVITE:
645                return mInviteScroll;
646        }
647        throw new IllegalArgumentException();
648    }
649
650    private void blockNetworkLoads(boolean block) {
651        if (mMessageContentView != null) {
652            mMessageContentView.getSettings().setBlockNetworkLoads(block);
653        }
654    }
655
656    private void setMessageHtml(String html) {
657        if (html == null) {
658            html = "";
659        }
660        if (mMessageContentView != null) {
661            mMessageContentView.loadDataWithBaseURL("email://", html, "text/html", "utf-8", null);
662        }
663    }
664
665    /**
666     * Handle clicks on sender, which shows {@link QuickContact} or prompts to add
667     * the sender as a contact.
668     */
669    private void onClickSender() {
670        final Address senderEmail = Address.unpackFirst(mMessage.mFrom);
671        if (senderEmail == null) return;
672
673        if (mContactStatusState == CONTACT_STATUS_STATE_UNLOADED) {
674            // Status not loaded yet.
675            mContactStatusState = CONTACT_STATUS_STATE_UNLOADED_TRIGGERED;
676            return;
677        }
678        if (mContactStatusState == CONTACT_STATUS_STATE_UNLOADED_TRIGGERED) {
679            return; // Already clicked, and waiting for the data.
680        }
681
682        if (mQuickContactLookupUri != null) {
683            QuickContact.showQuickContact(mContext, mFromBadge, mQuickContactLookupUri,
684                        QuickContact.MODE_LARGE, null);
685        } else {
686            // No matching contact, ask user to create one
687            final Uri mailUri = Uri.fromParts("mailto", senderEmail.getAddress(), null);
688            final Intent intent = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT,
689                    mailUri);
690
691            // Pass along full E-mail string for possible create dialog
692            intent.putExtra(ContactsContract.Intents.EXTRA_CREATE_DESCRIPTION,
693                    senderEmail.toString());
694
695            // Only provide personal name hint if we have one
696            final String senderPersonal = senderEmail.getPersonal();
697            if (!TextUtils.isEmpty(senderPersonal)) {
698                intent.putExtra(ContactsContract.Intents.Insert.NAME, senderPersonal);
699            }
700            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
701
702            startActivity(intent);
703        }
704    }
705
706    private static class ContactStatusLoaderCallbacks
707            implements LoaderCallbacks<ContactStatusLoader.Result> {
708        private static final String BUNDLE_EMAIL_ADDRESS = "email";
709        private final MessageViewFragmentBase mFragment;
710
711        public ContactStatusLoaderCallbacks(MessageViewFragmentBase fragment) {
712            mFragment = fragment;
713        }
714
715        public static Bundle createArguments(String emailAddress) {
716            Bundle b = new Bundle();
717            b.putString(BUNDLE_EMAIL_ADDRESS, emailAddress);
718            return b;
719        }
720
721        @Override
722        public Loader<ContactStatusLoader.Result> onCreateLoader(int id, Bundle args) {
723            return new ContactStatusLoader(mFragment.mContext,
724                    args.getString(BUNDLE_EMAIL_ADDRESS));
725        }
726
727        @Override
728        public void onLoadFinished(Loader<ContactStatusLoader.Result> loader,
729                ContactStatusLoader.Result result) {
730            boolean triggered =
731                    (mFragment.mContactStatusState == CONTACT_STATUS_STATE_UNLOADED_TRIGGERED);
732            mFragment.mContactStatusState = CONTACT_STATUS_STATE_LOADED;
733            mFragment.mQuickContactLookupUri = result.mLookupUri;
734            mFragment.mSenderPresenceView.setImageResource(result.mPresenceResId);
735            if (result.mPhoto != null) { // photo will be null if unknown.
736                mFragment.mFromBadge.setImageBitmap(result.mPhoto);
737            }
738            if (triggered) {
739                mFragment.onClickSender();
740            }
741        }
742
743        @Override
744        public void onLoaderReset(Loader<ContactStatusLoader.Result> loader) {
745        }
746    }
747
748    private void onSaveAttachment(MessageViewAttachmentInfo info) {
749        if (!Utility.isExternalStorageMounted()) {
750            /*
751             * Abort early if there's no place to save the attachment. We don't want to spend
752             * the time downloading it and then abort.
753             */
754            Utility.showToast(getActivity(), R.string.message_view_status_attachment_not_saved);
755            return;
756        }
757        Attachment attachment = Attachment.restoreAttachmentWithId(mContext, info.mId);
758        Uri attachmentUri = AttachmentUtilities.getAttachmentUri(mAccountId, attachment.mId);
759
760        try {
761            File downloads = Environment.getExternalStoragePublicDirectory(
762                    Environment.DIRECTORY_DOWNLOADS);
763            downloads.mkdirs();
764            File file = Utility.createUniqueFile(downloads, attachment.mFileName);
765            Uri contentUri = AttachmentUtilities.resolveAttachmentIdToContentUri(
766                    mContext.getContentResolver(), attachmentUri);
767            InputStream in = mContext.getContentResolver().openInputStream(contentUri);
768            OutputStream out = new FileOutputStream(file);
769            IOUtils.copy(in, out);
770            out.flush();
771            out.close();
772            in.close();
773
774            Utility.showToast(getActivity(), String.format(
775                    mContext.getString(R.string.message_view_status_attachment_saved),
776                    file.getName()));
777
778            // Although the download manager can scan media files, scanning only happens after the
779            // user clicks on the item in the Downloads app. So, we run the attachment through
780            // the media scanner ourselves so it gets added to gallery / music immediately.
781            MediaScannerConnection.scanFile(mContext, new String[] {file.getAbsolutePath()},
782                    null, null);
783
784            DownloadManager dm =
785                    (DownloadManager) getActivity().getSystemService(Context.DOWNLOAD_SERVICE);
786            dm.addCompletedDownload(info.mName, info.mName,
787                    false /* do not use media scanner */,
788                    info.mContentType, file.getAbsolutePath(), info.mSize,
789                    true /* show notification */);
790        } catch (IOException ioe) {
791            Utility.showToast(getActivity(), R.string.message_view_status_attachment_not_saved);
792        }
793    }
794
795    private void onViewAttachment(MessageViewAttachmentInfo info) {
796        Intent intent = info.getAttachmentIntent(mContext, mAccountId);
797        try {
798            startActivity(intent);
799        } catch (ActivityNotFoundException e) {
800            Utility.showToast(getActivity(), R.string.message_view_display_attachment_toast);
801        }
802    }
803
804    private void onInfoAttachment(final MessageViewAttachmentInfo attachment) {
805        AttachmentInfoDialog dialog =
806            AttachmentInfoDialog.newInstance(getActivity(), attachment.mDenyFlags);
807        dialog.show(getActivity().getFragmentManager(), null);
808    }
809
810    private void onLoadAttachment(final MessageViewAttachmentInfo attachment) {
811        attachment.loadButton.setVisibility(View.GONE);
812        // If there's nothing in the download queue, we'll probably start right away so wait a
813        // second before showing the cancel button
814        if (AttachmentDownloadService.getQueueSize() == 0) {
815            // Set to invisible; if the button is still in this state one second from now, we'll
816            // assume the download won't start right away, and we make the cancel button visible
817            attachment.cancelButton.setVisibility(View.GONE);
818            // Create the timed task that will change the button state
819            new AsyncTask<Void, Void, Void>() {
820                @Override
821                protected Void doInBackground(Void... params) {
822                    try {
823                        Thread.sleep(1000L);
824                    } catch (InterruptedException e) { }
825                    return null;
826                }
827                @Override
828                protected void onPostExecute(Void result) {
829                    // If the timeout completes and the attachment has not loaded, show cancel
830                    if (!attachment.loaded) {
831                        attachment.cancelButton.setVisibility(View.VISIBLE);
832                    }
833                }
834            }.execute();
835        } else {
836            attachment.cancelButton.setVisibility(View.VISIBLE);
837        }
838        attachment.showProgressIndeterminate();
839        mController.loadAttachment(attachment.mId, mMessageId, mAccountId);
840    }
841
842    private void onCancelAttachment(MessageViewAttachmentInfo attachment) {
843        // Don't change button states if we couldn't cancel the download
844        if (AttachmentDownloadService.cancelQueuedAttachment(attachment.mId)) {
845            attachment.loadButton.setVisibility(View.VISIBLE);
846            attachment.cancelButton.setVisibility(View.GONE);
847            attachment.hideProgress();
848        }
849    }
850
851    /**
852     * Called by ControllerResults. Show the "View" and "Save" buttons; hide "Load" and "Stop"
853     *
854     * @param attachmentId the attachment that was just downloaded
855     */
856    private void doFinishLoadAttachment(long attachmentId) {
857        MessageViewAttachmentInfo info = findAttachmentInfo(attachmentId);
858        if (info != null) {
859            info.loaded = true;
860            updateAttachmentButtons(info);
861        }
862    }
863
864    private void onShowPicturesInHtml() {
865        if (mMessageContentView != null) {
866            blockNetworkLoads(false);
867            setMessageHtml(mHtmlTextWebView);
868            addTabFlags(TAB_FLAGS_PICTURE_LOADED);
869        }
870    }
871
872    private void onShowDetails() {
873        if (mMessage == null) {
874            return; // shouldn't happen
875        }
876        String subject = mMessage.mSubject;
877        String date = formatDate(mMessage.mTimeStamp, true);
878
879        final String SEPARATOR = "\n";
880        String from = Address.toString(Address.unpack(mMessage.mFrom), SEPARATOR);
881        String to = Address.toString(Address.unpack(mMessage.mTo), SEPARATOR);
882        String cc = Address.toString(Address.unpack(mMessage.mCc), SEPARATOR);
883        String bcc = Address.toString(Address.unpack(mMessage.mBcc), SEPARATOR);
884        MessageViewMessageDetailsDialog dialog = MessageViewMessageDetailsDialog.newInstance(
885                getActivity(), subject, date, from, to, cc, bcc);
886        dialog.show(getActivity().getFragmentManager(), null);
887    }
888
889    @Override
890    public void onClick(View view) {
891        if (!isMessageOpen()) {
892            return; // Ignore.
893        }
894        switch (view.getId()) {
895            case R.id.from_name:
896            case R.id.from_address:
897            case R.id.badge:
898            case R.id.presence:
899                onClickSender();
900                break;
901            case R.id.load:
902                onLoadAttachment((MessageViewAttachmentInfo) view.getTag());
903                break;
904            case R.id.info:
905                onInfoAttachment((MessageViewAttachmentInfo) view.getTag());
906                break;
907            case R.id.save:
908                onSaveAttachment((MessageViewAttachmentInfo) view.getTag());
909                break;
910            case R.id.open:
911                onViewAttachment((MessageViewAttachmentInfo) view.getTag());
912                break;
913            case R.id.cancel:
914                onCancelAttachment((MessageViewAttachmentInfo) view.getTag());
915                break;
916            case R.id.show_message:
917                setCurrentTab(TAB_MESSAGE);
918                break;
919            case R.id.show_invite:
920                setCurrentTab(TAB_INVITE);
921                break;
922            case R.id.show_attachments:
923                setCurrentTab(TAB_ATTACHMENT);
924                break;
925            case R.id.show_pictures:
926                onShowPicturesInHtml();
927                break;
928            case R.id.show_details:
929                onShowDetails();
930                break;
931        }
932    }
933
934    /**
935     * Start loading contact photo and presence.
936     */
937    private void queryContactStatus() {
938        initContactStatusViews(); // Initialize the state, just in case.
939
940        // Find the sender email address, and start presence check.
941        if (mMessage != null) {
942            Address sender = Address.unpackFirst(mMessage.mFrom);
943            if (sender != null) {
944                String email = sender.getAddress();
945                if (email != null) {
946                    getLoaderManager().restartLoader(PHOTO_LOADER_ID,
947                            ContactStatusLoaderCallbacks.createArguments(email),
948                            new ContactStatusLoaderCallbacks(this));
949                }
950            }
951        }
952    }
953
954    /**
955     * Called by {@link LoadMessageTask} and {@link ReloadMessageTask} to load a message in a
956     * subclass specific way.
957     *
958     * NOTE This method is called on a worker thread!  Implementations must properly synchronize
959     * when accessing members.  This method may be called after or even at the same time as
960     * {@link #clearContent()}.
961     *
962     * @param activity the parent activity.  Subclass use it as a context, and to show a toast.
963     */
964    protected abstract Message openMessageSync(Activity activity);
965
966    /**
967     * Async task for loading a single message outside of the UI thread
968     */
969    private class LoadMessageTask extends AsyncTask<Void, Void, Message> {
970
971        private final boolean mOkToFetch;
972        private int mMailboxType;
973
974        /**
975         * Special constructor to cache some local info
976         */
977        public LoadMessageTask(boolean okToFetch) {
978            mOkToFetch = okToFetch;
979        }
980
981        @Override
982        protected Message doInBackground(Void... params) {
983            Activity activity = getActivity();
984            Message message = null;
985            if (activity != null) {
986                message = openMessageSync(activity);
987            }
988            if (message != null) {
989                mMailboxType = Mailbox.getMailboxType(mContext, message.mMailboxKey);
990                if (mMailboxType == -1) {
991                    message = null; // mailbox removed??
992                }
993            }
994            return message;
995        }
996
997        @Override
998        protected void onPostExecute(Message message) {
999            if (isCancelled()) {
1000                return;
1001            }
1002            if (message == null) {
1003                resetView();
1004                mCallback.onMessageNotExists();
1005                return;
1006            }
1007            mMessageId = message.mId;
1008
1009            reloadUiFromMessage(message, mOkToFetch);
1010            queryContactStatus();
1011            onMessageShown(mMessageId, mMailboxType);
1012        }
1013    }
1014
1015    /**
1016     * Kicked by {@link MessageObserver}.  Reload the message and update the views.
1017     */
1018    private class ReloadMessageTask extends AsyncTask<Void, Void, Message> {
1019        @Override
1020        protected Message doInBackground(Void... params) {
1021            if (!isMessageSpecified()) { // just in case
1022                return null;
1023            }
1024            Activity activity = getActivity();
1025            if (activity == null) {
1026                return null;
1027            } else {
1028                return openMessageSync(activity);
1029            }
1030        }
1031
1032        @Override
1033        protected void onPostExecute(Message message) {
1034            if (isCancelled()) {
1035                return;
1036            }
1037            if (message == null || message.mMailboxKey != mMessage.mMailboxKey) {
1038                // Message deleted or moved.
1039                mCallback.onMessageNotExists();
1040                return;
1041            }
1042            mMessage = message;
1043            updateHeaderView(mMessage);
1044        }
1045    }
1046
1047    /**
1048     * Called when a message is shown to the user.
1049     */
1050    protected void onMessageShown(long messageId, int mailboxType) {
1051        mCallback.onMessageViewShown(mailboxType);
1052    }
1053
1054    /**
1055     * Called when the message body is loaded.
1056     */
1057    protected void onPostLoadBody() {
1058    }
1059
1060    /**
1061     * Async task for loading a single message body outside of the UI thread
1062     */
1063    private class LoadBodyTask extends AsyncTask<Void, Void, String[]> {
1064
1065        private long mId;
1066        private boolean mErrorLoadingMessageBody;
1067
1068        /**
1069         * Special constructor to cache some local info
1070         */
1071        public LoadBodyTask(long messageId) {
1072            mId = messageId;
1073        }
1074
1075        @Override
1076        protected String[] doInBackground(Void... params) {
1077            try {
1078                String text = null;
1079                String html = Body.restoreBodyHtmlWithMessageId(mContext, mId);
1080                if (html == null) {
1081                    text = Body.restoreBodyTextWithMessageId(mContext, mId);
1082                }
1083                return new String[] { text, html };
1084            } catch (RuntimeException re) {
1085                // This catches SQLiteException as well as other RTE's we've seen from the
1086                // database calls, such as IllegalStateException
1087                Log.d(Logging.LOG_TAG, "Exception while loading message body", re);
1088                mErrorLoadingMessageBody = true;
1089                return null;
1090            }
1091        }
1092
1093        @Override
1094        protected void onPostExecute(String[] results) {
1095            if (results == null || isCancelled()) {
1096                if (mErrorLoadingMessageBody) {
1097                    Utility.showToast(getActivity(), R.string.error_loading_message_body);
1098                }
1099                resetView();
1100                return;
1101            }
1102            reloadUiFromBody(results[0], results[1]);    // text, html
1103            onPostLoadBody();
1104        }
1105    }
1106
1107    /**
1108     * Async task for loading attachments
1109     *
1110     * Note:  This really should only be called when the message load is complete - or, we should
1111     * leave open a listener so the attachments can fill in as they are discovered.  In either case,
1112     * this implementation is incomplete, as it will fail to refresh properly if the message is
1113     * partially loaded at this time.
1114     */
1115    private class LoadAttachmentsTask extends AsyncTask<Long, Void, Attachment[]> {
1116        @Override
1117        protected Attachment[] doInBackground(Long... messageIds) {
1118            return Attachment.restoreAttachmentsWithMessageId(mContext, messageIds[0]);
1119        }
1120
1121        @Override
1122        protected void onPostExecute(Attachment[] attachments) {
1123            try {
1124                if (isCancelled() || attachments == null) {
1125                    return;
1126                }
1127                boolean htmlChanged = false;
1128                int numDisplayedAttachments = 0;
1129                for (Attachment attachment : attachments) {
1130                    if (mHtmlTextRaw != null && attachment.mContentId != null
1131                            && attachment.mContentUri != null) {
1132                        // for html body, replace CID for inline images
1133                        // Regexp which matches ' src="cid:contentId"'.
1134                        String contentIdRe =
1135                            "\\s+(?i)src=\"cid(?-i):\\Q" + attachment.mContentId + "\\E\"";
1136                        String srcContentUri = " src=\"" + attachment.mContentUri + "\"";
1137                        mHtmlTextRaw = mHtmlTextRaw.replaceAll(contentIdRe, srcContentUri);
1138                        htmlChanged = true;
1139                    } else {
1140                        addAttachment(attachment);
1141                        numDisplayedAttachments++;
1142                    }
1143                }
1144                setAttachmentCount(numDisplayedAttachments);
1145                mHtmlTextWebView = mHtmlTextRaw;
1146                mHtmlTextRaw = null;
1147                if (htmlChanged) {
1148                    setMessageHtml(mHtmlTextWebView);
1149                }
1150            } finally {
1151                showContent(true, false);
1152            }
1153        }
1154    }
1155
1156    private static Bitmap getPreviewIcon(Context context, AttachmentInfo attachment) {
1157        try {
1158            return BitmapFactory.decodeStream(
1159                    context.getContentResolver().openInputStream(
1160                            AttachmentUtilities.getAttachmentThumbnailUri(
1161                                    attachment.mAccountKey, attachment.mId,
1162                                    PREVIEW_ICON_WIDTH,
1163                                    PREVIEW_ICON_HEIGHT)));
1164        } catch (Exception e) {
1165            Log.d(Logging.LOG_TAG, "Attachment preview failed with exception " + e.getMessage());
1166            return null;
1167        }
1168    }
1169
1170    /**
1171     * Subclass of AttachmentInfo which includes our views and buttons related to attachment
1172     * handling, as well as our determination of suitability for viewing (based on availability of
1173     * a viewer app) and saving (based upon the presence of external storage)
1174     */
1175    private static class MessageViewAttachmentInfo extends AttachmentInfo {
1176        private Button openButton;
1177        private Button saveButton;
1178        private Button loadButton;
1179        private Button infoButton;
1180        private Button cancelButton;
1181        private ImageView iconView;
1182
1183        // Don't touch it directly from the outer class.
1184        private ProgressBar mProgressView;
1185        private boolean loaded;
1186
1187        private MessageViewAttachmentInfo(Context context, Attachment attachment,
1188                ProgressBar progressView) {
1189            super(context, attachment);
1190            mProgressView = progressView;
1191        }
1192
1193        /**
1194         * Create a new attachment info based upon an existing attachment info. Display
1195         * related fields (such as views and buttons) are copied from old to new.
1196         */
1197        private MessageViewAttachmentInfo(Context context, MessageViewAttachmentInfo oldInfo) {
1198            super(context, oldInfo);
1199            openButton = oldInfo.openButton;
1200            saveButton = oldInfo.saveButton;
1201            loadButton = oldInfo.loadButton;
1202            infoButton = oldInfo.infoButton;
1203            cancelButton = oldInfo.cancelButton;
1204            iconView = oldInfo.iconView;
1205            mProgressView = oldInfo.mProgressView;
1206            loaded = oldInfo.loaded;
1207        }
1208
1209        public void hideProgress() {
1210            // Don't use GONE, which'll break the layout.
1211            if (mProgressView.getVisibility() != View.INVISIBLE) {
1212                mProgressView.setVisibility(View.INVISIBLE);
1213            }
1214        }
1215
1216        public void showProgress(int progress) {
1217            if (mProgressView.getVisibility() != View.VISIBLE) {
1218                mProgressView.setVisibility(View.VISIBLE);
1219            }
1220            if (mProgressView.isIndeterminate()) {
1221                mProgressView.setIndeterminate(false);
1222            }
1223            mProgressView.setProgress(progress);
1224        }
1225
1226        public void showProgressIndeterminate() {
1227            if (mProgressView.getVisibility() != View.VISIBLE) {
1228                mProgressView.setVisibility(View.VISIBLE);
1229            }
1230            if (!mProgressView.isIndeterminate()) {
1231                mProgressView.setIndeterminate(true);
1232            }
1233        }
1234    }
1235
1236    /**
1237     * Updates all current attachments on the attachment tab.
1238     */
1239    private void updateAttachmentTab() {
1240        for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
1241            View view = mAttachments.getChildAt(i);
1242            MessageViewAttachmentInfo oldInfo = (MessageViewAttachmentInfo)view.getTag();
1243            MessageViewAttachmentInfo newInfo =
1244                    new MessageViewAttachmentInfo(getActivity(), oldInfo);
1245            updateAttachmentButtons(newInfo);
1246            view.setTag(newInfo);
1247        }
1248    }
1249
1250    /**
1251     * Updates the attachment buttons. Adjusts the visibility of the buttons as well
1252     * as updating any tag information associated with the buttons.
1253     */
1254    private void updateAttachmentButtons(MessageViewAttachmentInfo attachmentInfo) {
1255        ImageView attachmentIcon = attachmentInfo.iconView;
1256        Button openButton = attachmentInfo.openButton;
1257        Button saveButton = attachmentInfo.saveButton;
1258        Button loadButton = attachmentInfo.loadButton;
1259        Button infoButton = attachmentInfo.infoButton;
1260        Button cancelButton = attachmentInfo.cancelButton;
1261
1262        if (!attachmentInfo.mAllowView) {
1263            openButton.setVisibility(View.GONE);
1264        }
1265        if (!attachmentInfo.mAllowSave) {
1266            saveButton.setVisibility(View.GONE);
1267        }
1268
1269        if (!attachmentInfo.mAllowView && !attachmentInfo.mAllowSave) {
1270            // This attachment may never be viewed or saved, so block everything
1271            attachmentInfo.hideProgress();
1272            openButton.setVisibility(View.GONE);
1273            saveButton.setVisibility(View.GONE);
1274            loadButton.setVisibility(View.GONE);
1275            cancelButton.setVisibility(View.GONE);
1276            infoButton.setVisibility(View.VISIBLE);
1277        } else if (attachmentInfo.loaded) {
1278            // If the attachment is loaded, show 100% progress
1279            // Note that for POP3 messages, the user will only see "Open" and "Save",
1280            // because the entire message is loaded before being shown.
1281            // Hide "Load" and "Info", show "View" and "Save"
1282            attachmentInfo.showProgress(100);
1283            if (attachmentInfo.mAllowSave) {
1284                saveButton.setVisibility(View.VISIBLE);
1285            }
1286            if (attachmentInfo.mAllowView) {
1287                // Set the attachment action button text accordingly
1288                if (attachmentInfo.mContentType.startsWith("audio/") ||
1289                        attachmentInfo.mContentType.startsWith("video/")) {
1290                    openButton.setText(R.string.message_view_attachment_play_action);
1291                } else if (attachmentInfo.mAllowInstall) {
1292                    openButton.setText(R.string.message_view_attachment_install_action);
1293                } else {
1294                    openButton.setText(R.string.message_view_attachment_view_action);
1295                }
1296                openButton.setVisibility(View.VISIBLE);
1297            }
1298            if (attachmentInfo.mDenyFlags == AttachmentInfo.ALLOW) {
1299                infoButton.setVisibility(View.GONE);
1300            } else {
1301                infoButton.setVisibility(View.VISIBLE);
1302            }
1303            loadButton.setVisibility(View.GONE);
1304            cancelButton.setVisibility(View.GONE);
1305
1306            updatePreviewIcon(attachmentInfo);
1307        } else {
1308            // The attachment is not loaded, so present UI to start downloading it
1309
1310            // Show "Load"; hide "View", "Save" and "Info"
1311            saveButton.setVisibility(View.GONE);
1312            openButton.setVisibility(View.GONE);
1313            infoButton.setVisibility(View.GONE);
1314
1315            // If the attachment is queued, show the indeterminate progress bar.  From this point,.
1316            // any progress changes will cause this to be replaced by the normal progress bar
1317            if (AttachmentDownloadService.isAttachmentQueued(attachmentInfo.mId)) {
1318                attachmentInfo.showProgressIndeterminate();
1319                loadButton.setVisibility(View.GONE);
1320                cancelButton.setVisibility(View.VISIBLE);
1321            } else {
1322                loadButton.setVisibility(View.VISIBLE);
1323                cancelButton.setVisibility(View.GONE);
1324            }
1325        }
1326        openButton.setTag(attachmentInfo);
1327        saveButton.setTag(attachmentInfo);
1328        loadButton.setTag(attachmentInfo);
1329        infoButton.setTag(attachmentInfo);
1330        cancelButton.setTag(attachmentInfo);
1331    }
1332
1333    /**
1334     * Copy data from a cursor-refreshed attachment into the UI.  Called from UI thread.
1335     *
1336     * @param attachment A single attachment loaded from the provider
1337     */
1338    private void addAttachment(Attachment attachment) {
1339        LayoutInflater inflater = getActivity().getLayoutInflater();
1340        View view = inflater.inflate(R.layout.message_view_attachment, null);
1341
1342        TextView attachmentName = (TextView)view.findViewById(R.id.attachment_name);
1343        TextView attachmentInfoView = (TextView)view.findViewById(R.id.attachment_info);
1344        ImageView attachmentIcon = (ImageView)view.findViewById(R.id.attachment_icon);
1345        Button openButton = (Button)view.findViewById(R.id.open);
1346        Button saveButton = (Button)view.findViewById(R.id.save);
1347        Button loadButton = (Button)view.findViewById(R.id.load);
1348        Button infoButton = (Button)view.findViewById(R.id.info);
1349        Button cancelButton = (Button)view.findViewById(R.id.cancel);
1350        ProgressBar attachmentProgress = (ProgressBar)view.findViewById(R.id.progress);
1351
1352        MessageViewAttachmentInfo attachmentInfo = new MessageViewAttachmentInfo(
1353                mContext, attachment, attachmentProgress);
1354
1355        // Check whether the attachment already exists
1356        if (Utility.attachmentExists(mContext, attachment)) {
1357            attachmentInfo.loaded = true;
1358        }
1359
1360        attachmentInfo.openButton = openButton;
1361        attachmentInfo.saveButton = saveButton;
1362        attachmentInfo.loadButton = loadButton;
1363        attachmentInfo.infoButton = infoButton;
1364        attachmentInfo.cancelButton = cancelButton;
1365        attachmentInfo.iconView = attachmentIcon;
1366
1367        updateAttachmentButtons(attachmentInfo);
1368
1369        view.setTag(attachmentInfo);
1370        openButton.setOnClickListener(this);
1371        saveButton.setOnClickListener(this);
1372        loadButton.setOnClickListener(this);
1373        infoButton.setOnClickListener(this);
1374        cancelButton.setOnClickListener(this);
1375
1376        attachmentName.setText(attachmentInfo.mName);
1377        attachmentInfoView.setText(UiUtilities.formatSize(mContext, attachmentInfo.mSize));
1378
1379        mAttachments.addView(view);
1380        mAttachments.setVisibility(View.VISIBLE);
1381    }
1382
1383    private MessageViewAttachmentInfo findAttachmentInfoFromView(long attachmentId) {
1384        for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
1385            MessageViewAttachmentInfo attachmentInfo =
1386                    (MessageViewAttachmentInfo) mAttachments.getChildAt(i).getTag();
1387            if (attachmentInfo.mId == attachmentId) {
1388                return attachmentInfo;
1389            }
1390        }
1391        return null;
1392    }
1393
1394    /**
1395     * Reload the UI from a provider cursor.  {@link LoadMessageTask#onPostExecute} calls it.
1396     *
1397     * Update the header views, and start loading the body.
1398     *
1399     * @param message A copy of the message loaded from the database
1400     * @param okToFetch If true, and message is not fully loaded, it's OK to fetch from
1401     * the network.  Use false to prevent looping here.
1402     */
1403    protected void reloadUiFromMessage(Message message, boolean okToFetch) {
1404        mMessage = message;
1405        mAccountId = message.mAccountKey;
1406
1407        mMessageObserver.register(ContentUris.withAppendedId(Message.CONTENT_URI, mMessage.mId));
1408
1409        updateHeaderView(mMessage);
1410
1411        // Handle partially-loaded email, as follows:
1412        // 1. Check value of message.mFlagLoaded
1413        // 2. If != LOADED, ask controller to load it
1414        // 3. Controller callback (after loaded) should trigger LoadBodyTask & LoadAttachmentsTask
1415        // 4. Else start the loader tasks right away (message already loaded)
1416        if (okToFetch && message.mFlagLoaded != Message.FLAG_LOADED_COMPLETE) {
1417            mControllerCallback.getWrappee().setWaitForLoadMessageId(message.mId);
1418            mController.loadMessageForView(message.mId);
1419        } else {
1420            mControllerCallback.getWrappee().setWaitForLoadMessageId(-1);
1421            // Ask for body
1422            mLoadBodyTask = new LoadBodyTask(message.mId);
1423            mLoadBodyTask.execute();
1424        }
1425    }
1426
1427    protected void updateHeaderView(Message message) {
1428        mSubjectView.setText(message.mSubject);
1429        final Address from = Address.unpackFirst(message.mFrom);
1430
1431        // Set sender address/display name
1432        // Note we set " " for empty field, so TextView's won't get squashed.
1433        // Otherwise their height will be 0, which breaks the layout.
1434        if (from != null) {
1435            final String fromFriendly = from.toFriendly();
1436            final String fromAddress = from.getAddress();
1437            mFromNameView.setText(fromFriendly);
1438            mFromAddressView.setText(fromFriendly.equals(fromAddress) ? " " : fromAddress);
1439        } else {
1440            mFromNameView.setText(" ");
1441            mFromAddressView.setText(" ");
1442        }
1443        mDateTimeView.setText(formatDate(message.mTimeStamp, false));
1444
1445        // To/Cc/Bcc
1446        final Resources res = mContext.getResources();
1447        final SpannableStringBuilder ssb = new SpannableStringBuilder();
1448        final String friendlyTo = Address.toFriendly(Address.unpack(message.mTo));
1449        final String friendlyCc = Address.toFriendly(Address.unpack(message.mCc));
1450        final String friendlyBcc = Address.toFriendly(Address.unpack(message.mBcc));
1451
1452        if (!TextUtils.isEmpty(friendlyTo)) {
1453            Utility.appendBold(ssb, res.getString(R.string.message_view_to_label));
1454            ssb.append(" ");
1455            ssb.append(friendlyTo);
1456        }
1457        if (!TextUtils.isEmpty(friendlyCc)) {
1458            ssb.append("  ");
1459            Utility.appendBold(ssb, res.getString(R.string.message_view_cc_label));
1460            ssb.append(" ");
1461            ssb.append(friendlyCc);
1462        }
1463        if (!TextUtils.isEmpty(friendlyBcc)) {
1464            ssb.append("  ");
1465            Utility.appendBold(ssb, res.getString(R.string.message_view_bcc_label));
1466            ssb.append(" ");
1467            ssb.append(friendlyBcc);
1468        }
1469        mAddressesView.setText(ssb);
1470    }
1471
1472    private String formatDate(long millis, boolean withYear) {
1473        StringBuilder sb = new StringBuilder();
1474        Formatter formatter = new Formatter(sb);
1475        DateUtils.formatDateRange(mContext, formatter, millis, millis,
1476                DateUtils.FORMAT_SHOW_DATE
1477                | DateUtils.FORMAT_ABBREV_ALL
1478                | DateUtils.FORMAT_SHOW_TIME
1479                | (withYear ? DateUtils.FORMAT_SHOW_YEAR : DateUtils.FORMAT_NO_YEAR));
1480        return sb.toString();
1481    }
1482
1483    /**
1484     * Reload the body from the provider cursor.  This must only be called from the UI thread.
1485     *
1486     * @param bodyText text part
1487     * @param bodyHtml html part
1488     *
1489     * TODO deal with html vs text and many other issues <- WHAT DOES IT MEAN??
1490     */
1491    private void reloadUiFromBody(String bodyText, String bodyHtml) {
1492        String text = null;
1493        mHtmlTextRaw = null;
1494        boolean hasImages = false;
1495
1496        if (bodyHtml == null) {
1497            text = bodyText;
1498            /*
1499             * Convert the plain text to HTML
1500             */
1501            StringBuffer sb = new StringBuffer("<html><body>");
1502            if (text != null) {
1503                // Escape any inadvertent HTML in the text message
1504                text = EmailHtmlUtil.escapeCharacterToDisplay(text);
1505                // Find any embedded URL's and linkify
1506                Matcher m = Patterns.WEB_URL.matcher(text);
1507                while (m.find()) {
1508                    int start = m.start();
1509                    /*
1510                     * WEB_URL_PATTERN may match domain part of email address. To detect
1511                     * this false match, the character just before the matched string
1512                     * should not be '@'.
1513                     */
1514                    if (start == 0 || text.charAt(start - 1) != '@') {
1515                        String url = m.group();
1516                        Matcher proto = WEB_URL_PROTOCOL.matcher(url);
1517                        String link;
1518                        if (proto.find()) {
1519                            // This is work around to force URL protocol part be lower case,
1520                            // because WebView could follow only lower case protocol link.
1521                            link = proto.group().toLowerCase() + url.substring(proto.end());
1522                        } else {
1523                            // Patterns.WEB_URL matches URL without protocol part,
1524                            // so added default protocol to link.
1525                            link = "http://" + url;
1526                        }
1527                        String href = String.format("<a href=\"%s\">%s</a>", link, url);
1528                        m.appendReplacement(sb, href);
1529                    }
1530                    else {
1531                        m.appendReplacement(sb, "$0");
1532                    }
1533                }
1534                m.appendTail(sb);
1535            }
1536            sb.append("</body></html>");
1537            text = sb.toString();
1538        } else {
1539            text = bodyHtml;
1540            mHtmlTextRaw = bodyHtml;
1541            hasImages = IMG_TAG_START_REGEX.matcher(text).find();
1542        }
1543
1544        // TODO this is not really accurate.
1545        // - Images aren't the only network resources.  (e.g. CSS)
1546        // - If images are attached to the email and small enough, we download them at once,
1547        //   and won't need network access when they're shown.
1548        if (hasImages) {
1549            if (mRestoredPictureLoaded) {
1550                blockNetworkLoads(false);
1551                addTabFlags(TAB_FLAGS_PICTURE_LOADED); // Set for next onSaveInstanceState
1552
1553                // Make sure to reset the flag -- otherwise this will keep taking effect even after
1554                // moving to another message.
1555                mRestoredPictureLoaded = false;
1556            } else {
1557                addTabFlags(TAB_FLAGS_HAS_PICTURES);
1558            }
1559        }
1560        setMessageHtml(text);
1561
1562        // Ask for attachments after body
1563        mLoadAttachmentsTask = new LoadAttachmentsTask();
1564        mLoadAttachmentsTask.execute(mMessage.mId);
1565
1566        mIsMessageLoadedForTest = true;
1567    }
1568
1569    /**
1570     * Overrides for WebView behaviors.
1571     */
1572    private class CustomWebViewClient extends WebViewClient {
1573        @Override
1574        public boolean shouldOverrideUrlLoading(WebView view, String url) {
1575            return mCallback.onUrlInMessageClicked(url);
1576        }
1577    }
1578
1579    private View findAttachmentView(long attachmentId) {
1580        for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
1581            View view = mAttachments.getChildAt(i);
1582            MessageViewAttachmentInfo attachment = (MessageViewAttachmentInfo) view.getTag();
1583            if (attachment.mId == attachmentId) {
1584                return view;
1585            }
1586        }
1587        return null;
1588    }
1589
1590    private MessageViewAttachmentInfo findAttachmentInfo(long attachmentId) {
1591        View view = findAttachmentView(attachmentId);
1592        if (view != null) {
1593            return (MessageViewAttachmentInfo)view.getTag();
1594        }
1595        return null;
1596    }
1597
1598    /**
1599     * Controller results listener.  We wrap it with {@link ControllerResultUiThreadWrapper},
1600     * so all methods are called on the UI thread.
1601     */
1602    private class ControllerResults extends Controller.Result {
1603        private long mWaitForLoadMessageId;
1604
1605        public void setWaitForLoadMessageId(long messageId) {
1606            mWaitForLoadMessageId = messageId;
1607        }
1608
1609        @Override
1610        public void loadMessageForViewCallback(MessagingException result, long accountId,
1611                long messageId, int progress) {
1612            if (messageId != mWaitForLoadMessageId) {
1613                // We are not waiting for this message to load, so exit quickly
1614                return;
1615            }
1616            if (result == null) {
1617                switch (progress) {
1618                    case 0:
1619                        mCallback.onLoadMessageStarted();
1620                        // Loading from network -- show the progress icon.
1621                        showContent(false, true);
1622                        break;
1623                    case 100:
1624                        mWaitForLoadMessageId = -1;
1625                        mCallback.onLoadMessageFinished();
1626                        // reload UI and reload everything else too
1627                        // pass false to LoadMessageTask to prevent looping here
1628                        cancelAllTasks();
1629                        mLoadMessageTask = new LoadMessageTask(false);
1630                        mLoadMessageTask.execute();
1631                        break;
1632                    default:
1633                        // do nothing - we don't have a progress bar at this time
1634                        break;
1635                }
1636            } else {
1637                mWaitForLoadMessageId = -1;
1638                String error = mContext.getString(R.string.status_network_error);
1639                mCallback.onLoadMessageError(error);
1640                resetView();
1641            }
1642        }
1643
1644        @Override
1645        public void loadAttachmentCallback(MessagingException result, long accountId,
1646                long messageId, long attachmentId, int progress) {
1647            if (messageId == mMessageId) {
1648                if (result == null) {
1649                    showAttachmentProgress(attachmentId, progress);
1650                    switch (progress) {
1651                        case 100:
1652                            final MessageViewAttachmentInfo attachmentInfo =
1653                                    findAttachmentInfoFromView(attachmentId);
1654                            if (attachmentInfo != null) {
1655                                updatePreviewIcon(attachmentInfo);
1656                            }
1657                            doFinishLoadAttachment(attachmentId);
1658                            break;
1659                        default:
1660                            // do nothing - we don't have a progress bar at this time
1661                            break;
1662                    }
1663                } else {
1664                    MessageViewAttachmentInfo attachment = findAttachmentInfo(attachmentId);
1665                    if (attachment == null) {
1666                        // Called before LoadAttachmentsTask finishes.
1667                        // (Possible if you quickly close & re-open a message)
1668                        return;
1669                    }
1670                    attachment.cancelButton.setVisibility(View.GONE);
1671                    attachment.loadButton.setVisibility(View.VISIBLE);
1672                    attachment.hideProgress();
1673
1674                    final String error;
1675                    if (result.getCause() instanceof IOException) {
1676                        error = mContext.getString(R.string.status_network_error);
1677                    } else {
1678                        error = mContext.getString(
1679                                R.string.message_view_load_attachment_failed_toast,
1680                                attachment.mName);
1681                    }
1682                    mCallback.onLoadMessageError(error);
1683                }
1684            }
1685        }
1686
1687        private void showAttachmentProgress(long attachmentId, int progress) {
1688            MessageViewAttachmentInfo attachment = findAttachmentInfo(attachmentId);
1689            if (attachment != null) {
1690                if (progress == 0) {
1691                    attachment.cancelButton.setVisibility(View.GONE);
1692                }
1693                attachment.showProgress(progress);
1694            }
1695        }
1696    }
1697
1698    /**
1699     * Class to detect update on the current message (e.g. toggle star).  When it gets content
1700     * change notifications, it kicks {@link ReloadMessageTask}.
1701     *
1702     * TODO Use the new Throttle class.
1703     */
1704    private class MessageObserver extends ContentObserver implements Runnable {
1705        private final Throttle mThrottle;
1706        private final ContentResolver mContentResolver;
1707
1708        private boolean mRegistered;
1709
1710        public MessageObserver(Handler handler, Context context) {
1711            super(handler);
1712            mContentResolver = context.getContentResolver();
1713            mThrottle = new Throttle("MessageObserver", this, handler);
1714        }
1715
1716        public void unregister() {
1717            if (!mRegistered) {
1718                return;
1719            }
1720            mThrottle.cancelScheduledCallback();
1721            mContentResolver.unregisterContentObserver(this);
1722            mRegistered = false;
1723        }
1724
1725        public void register(Uri notifyUri) {
1726            unregister();
1727            mContentResolver.registerContentObserver(notifyUri, true, this);
1728            mRegistered = true;
1729        }
1730
1731        @Override
1732        public boolean deliverSelfNotifications() {
1733            return true;
1734        }
1735
1736        @Override
1737        public void onChange(boolean selfChange) {
1738            mThrottle.onEvent();
1739        }
1740
1741        /**
1742         * This method is delay-called by {@link Throttle} on the UI thread.  Need to make
1743         * sure if the fragment is still valid.  (i.e. don't reload if clearContent() has been
1744         * called.)
1745         */
1746        @Override
1747        public void run() {
1748            if (!isMessageSpecified()) {
1749                return;
1750            }
1751            Utility.cancelTaskInterrupt(mReloadMessageTask);
1752            mReloadMessageTask = new ReloadMessageTask();
1753            mReloadMessageTask.execute();
1754        }
1755    }
1756
1757    private void updatePreviewIcon(MessageViewAttachmentInfo attachmentInfo) {
1758        new UpdatePreviewIconTask(attachmentInfo).execute();
1759    }
1760
1761    private class UpdatePreviewIconTask extends EmailAsyncTask<Void, Void, Bitmap> {
1762        @SuppressWarnings("hiding")
1763        private final Context mContext;
1764        private final MessageViewAttachmentInfo mAttachmentInfo;
1765
1766        public UpdatePreviewIconTask(MessageViewAttachmentInfo attachmentInfo) {
1767            super(mUpdatePreviewIconTaskTracker);
1768            mContext = getActivity();
1769            mAttachmentInfo = attachmentInfo;
1770        }
1771
1772        @Override
1773        protected Bitmap doInBackground(Void... params) {
1774            return getPreviewIcon(mContext, mAttachmentInfo);
1775        }
1776
1777        @Override
1778        protected void onPostExecute(Bitmap result) {
1779            if (result == null) {
1780                return;
1781            }
1782            mAttachmentInfo.iconView.setImageBitmap(result);
1783        }
1784    }
1785
1786    public boolean isMessageLoadedForTest() {
1787        return mIsMessageLoadedForTest;
1788    }
1789
1790    public void clearIsMessageLoadedForTest() {
1791        mIsMessageLoadedForTest = true;
1792    }
1793}
1794