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