MessageViewFragmentBase.java revision 28d6e09575cc5b9091c5d29990792db3465cccf0
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.Controller;
20import com.android.email.ControllerResultUiThreadWrapper;
21import com.android.email.Email;
22import com.android.email.Preferences;
23import com.android.email.R;
24import com.android.email.Throttle;
25import com.android.email.Utility;
26import com.android.email.mail.Address;
27import com.android.email.mail.MessagingException;
28import com.android.email.mail.internet.EmailHtmlUtil;
29import com.android.email.mail.internet.MimeUtility;
30import com.android.email.provider.AttachmentProvider;
31import com.android.email.provider.EmailContent.Attachment;
32import com.android.email.provider.EmailContent.Body;
33import com.android.email.provider.EmailContent.Mailbox;
34import com.android.email.provider.EmailContent.Message;
35import com.android.email.service.AttachmentDownloadService;
36
37import org.apache.commons.io.IOUtils;
38
39import android.app.Activity;
40import android.app.Fragment;
41import android.app.LoaderManager.LoaderCallbacks;
42import android.content.ActivityNotFoundException;
43import android.content.ContentResolver;
44import android.content.ContentUris;
45import android.content.Context;
46import android.content.Intent;
47import android.content.Loader;
48import android.database.ContentObserver;
49import android.graphics.Bitmap;
50import android.graphics.BitmapFactory;
51import android.graphics.drawable.ColorDrawable;
52import android.graphics.drawable.Drawable;
53import android.net.Uri;
54import android.os.AsyncTask;
55import android.os.Bundle;
56import android.os.Environment;
57import android.os.Handler;
58import android.provider.ContactsContract;
59import android.provider.ContactsContract.QuickContact;
60import android.text.TextUtils;
61import android.util.Log;
62import android.util.Patterns;
63import android.view.LayoutInflater;
64import android.view.View;
65import android.view.ViewGroup;
66import android.view.animation.Animation;
67import android.view.animation.AnimationUtils;
68import android.webkit.WebSettings;
69import android.webkit.WebView;
70import android.webkit.WebViewClient;
71import android.widget.Button;
72import android.widget.ImageView;
73import android.widget.LinearLayout;
74import android.widget.ProgressBar;
75import android.widget.TextView;
76
77import java.io.File;
78import java.io.FileOutputStream;
79import java.io.IOException;
80import java.io.InputStream;
81import java.io.OutputStream;
82import java.util.Date;
83import java.util.regex.Matcher;
84import java.util.regex.Pattern;
85
86// TODO Better handling of config changes.
87// - Restore "Show pictures" state, scroll position and current tab
88// - Retain the content; don't kick 3 async tasks every time
89
90/**
91 * Base class for {@link MessageViewFragment} and {@link MessageFileViewFragment}.
92 *
93 * See {@link MessageViewBase} for the class relation diagram.
94 */
95public abstract class MessageViewFragmentBase extends Fragment implements View.OnClickListener {
96    private static final int PHOTO_LOADER_ID = 1;
97    private Context mContext;
98
99    // Regex that matches start of img tag. '<(?i)img\s+'.
100    private static final Pattern IMG_TAG_START_REGEX = Pattern.compile("<(?i)img\\s+");
101    // Regex that matches Web URL protocol part as case insensitive.
102    private static final Pattern WEB_URL_PROTOCOL = Pattern.compile("(?i)http|https://");
103
104    private static int PREVIEW_ICON_WIDTH = 62;
105    private static int PREVIEW_ICON_HEIGHT = 62;
106
107    private TextView mSubjectView;
108    private TextView mFromNameView;
109    private TextView mFromAddressView;
110    private TextView mDateTimeView;
111    private TextView mToView;
112    private TextView mCcView;
113    private View mCcContainerView;
114    private TextView mBccView;
115    private View mBccContainerView;
116    private WebView mMessageContentView;
117    private LinearLayout mAttachments;
118    private View mTabSection;
119    private ImageView mFromBadge;
120    private ImageView mSenderPresenceView;
121    private View mMainView;
122    private View mLoadingProgress;
123
124    private TextView mMessageTab;
125    private TextView mAttachmentTab;
126    private TextView mInviteTab;
127    // It is not really a tab, but looks like one of them.
128    private TextView mShowPicturesTab;
129
130    private View mAttachmentsScroll;
131    private View mInviteScroll;
132
133    private Animation mFadeInAnimation;
134    private Animation mFadeOutAnimation;
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 java.text.DateFormat mDateFormat;
146    private java.text.DateFormat mTimeFormat;
147
148    private Controller mController;
149    private ControllerResultUiThreadWrapper<ControllerResults> mControllerCallback;
150
151    // contains the HTML body. Is used by LoadAttachmentTask to display inline images.
152    // is null most of the time, is used transiently to pass info to LoadAttachementTask
153    private String mHtmlTextRaw;
154
155    // contains the HTML content as set in WebView.
156    private String mHtmlTextWebView;
157
158    private boolean mResumed;
159    private boolean mLoadWhenResumed;
160
161    private boolean mIsMessageLoadedForTest;
162
163    private MessageObserver mMessageObserver;
164
165    private static final int CONTACT_STATUS_STATE_UNLOADED = 0;
166    private static final int CONTACT_STATUS_STATE_UNLOADED_TRIGGERED = 1;
167    private static final int CONTACT_STATUS_STATE_LOADED = 2;
168
169    private int mContactStatusState;
170    private Uri mQuickContactLookupUri;
171
172    /** Flag for {@link #mTabFlags}: Message has attachment(s) */
173    protected static final int TAB_FLAGS_HAS_ATTACHMENT = 1;
174
175    /**
176     * Flag for {@link #mTabFlags}: Message contains invite.  This flag is only set by
177     * {@link MessageViewFragment}.
178     */
179    protected static final int TAB_FLAGS_HAS_INVITE = 2;
180
181    /** Flag for {@link #mTabFlags}: Message contains pictures */
182    protected static final int TAB_FLAGS_HAS_PICTURES = 4;
183
184    /** Flag for {@link #mTabFlags}: "Show pictures" has already been pressed */
185    protected static final int TAB_FLAGS_PICTURE_LOADED = 8;
186
187    /**
188     * Flags to control the tabs.
189     * @see #updateTabFlags(int)
190     */
191    private int mTabFlags;
192
193    /** # of attachments in the current message */
194    private int mAttachmentCount;
195
196    // Use (random) large values, to avoid confusion with TAB_FLAGS_*
197    protected static final int TAB_MESSAGE = 101;
198    protected static final int TAB_INVITE = 102;
199    protected static final int TAB_ATTACHMENT = 103;
200
201    /**
202     * Currently visible tab.  Any of {@link #TAB_MESSAGE}, {@link #TAB_INVITE} or
203     * {@link #TAB_ATTACHMENT}.
204     *
205     * Note we don't retain this value through configuration changes, as restoring the current tab
206     * would be clumsy with the current implementation where we load Message/Body/Attachments
207     * separately.  (e.g. # of attachments can't be obtained quickly enough to update the UI
208     * after screen rotation.)
209     */
210    private int mCurrentTab;
211
212    /**
213     * Encapsulates known information about a single attachment.
214     */
215    private static class AttachmentInfo {
216        public String name;
217        public String contentType;
218        public long size;
219        public long attachmentId;
220        public Button viewButton;
221        public Button saveButton;
222        public Button loadButton;
223        public Button cancelButton;
224        public ImageView iconView;
225        public ProgressBar progressView;
226    }
227
228    public interface Callback {
229        /** Called when the fragment is about to show up, or show a different message. */
230        public void onMessageViewShown(int mailboxType);
231
232        /** Called when the fragment is about to be destroyed. */
233        public void onMessageViewGone();
234
235        /**
236         * Called when a link in a message is clicked.
237         *
238         * @param url link url that's clicked.
239         * @return true if handled, false otherwise.
240         */
241        public boolean onUrlInMessageClicked(String url);
242
243        /**
244         * Called when the message specified doesn't exist, or is deleted/moved.
245         */
246        public void onMessageNotExists();
247
248        /** Called when it starts loading a message. */
249        public void onLoadMessageStarted();
250
251        /** Called when it successfully finishes loading a message. */
252        public void onLoadMessageFinished();
253
254        /** Called when an error occurred during loading a message. */
255        public void onLoadMessageError(String errorMessage);
256    }
257
258    public static class EmptyCallback implements Callback {
259        public static final Callback INSTANCE = new EmptyCallback();
260        @Override public void onMessageViewShown(int mailboxType) {}
261        @Override public void onMessageViewGone() {}
262        @Override public void onLoadMessageError(String errorMessage) {}
263        @Override public void onLoadMessageFinished() {}
264        @Override public void onLoadMessageStarted() {}
265        @Override public void onMessageNotExists() {}
266        @Override
267        public boolean onUrlInMessageClicked(String url) {
268            return false;
269        }
270    }
271
272    private Callback mCallback = EmptyCallback.INSTANCE;
273
274    @Override
275    public void onCreate(Bundle savedInstanceState) {
276        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
277            Log.d(Email.LOG_TAG, "MessageViewFragment onCreate");
278        }
279        super.onCreate(savedInstanceState);
280
281        mContext = getActivity().getApplicationContext();
282
283        mControllerCallback = new ControllerResultUiThreadWrapper<ControllerResults>(
284                new Handler(), new ControllerResults());
285
286        mDateFormat = android.text.format.DateFormat.getDateFormat(mContext); // short format
287        mTimeFormat = android.text.format.DateFormat.getTimeFormat(mContext); // 12/24 date format
288
289        mController = Controller.getInstance(mContext);
290        mMessageObserver = new MessageObserver(new Handler(), mContext);
291
292        mFadeInAnimation = AnimationUtils.loadAnimation(mContext, android.R.anim.fade_in);
293        mFadeOutAnimation = AnimationUtils.loadAnimation(mContext, android.R.anim.fade_out);
294    }
295
296    @Override
297    public View onCreateView(
298            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
299        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
300            Log.d(Email.LOG_TAG, "MessageViewFragment onCreateView");
301        }
302        final View view = inflater.inflate(R.layout.message_view_fragment, container, false);
303
304        mSubjectView = (TextView) view.findViewById(R.id.subject);
305        mFromNameView = (TextView) view.findViewById(R.id.from_name);
306        mFromAddressView = (TextView) view.findViewById(R.id.from_address);
307        mToView = (TextView) view.findViewById(R.id.to);
308        mCcView = (TextView) view.findViewById(R.id.cc);
309        mCcContainerView = view.findViewById(R.id.cc_container);
310        mBccView = (TextView) view.findViewById(R.id.bcc);
311        mBccContainerView = view.findViewById(R.id.bcc_container);
312        mDateTimeView = (TextView) view.findViewById(R.id.datetime);
313        mMessageContentView = (WebView) view.findViewById(R.id.message_content);
314        mAttachments = (LinearLayout) view.findViewById(R.id.attachments);
315        mTabSection = view.findViewById(R.id.message_tabs_section);
316        mFromBadge = (ImageView) view.findViewById(R.id.badge);
317        mSenderPresenceView = (ImageView) view.findViewById(R.id.presence);
318        mMainView = view.findViewById(R.id.main_panel);
319        mLoadingProgress = view.findViewById(R.id.loading_progress);
320
321        mFromNameView.setOnClickListener(this);
322        mFromAddressView.setOnClickListener(this);
323        mFromBadge.setOnClickListener(this);
324        mSenderPresenceView.setOnClickListener(this);
325
326        mMessageTab = (TextView) view.findViewById(R.id.show_message);
327        mAttachmentTab = (TextView) view.findViewById(R.id.show_attachments);
328        mShowPicturesTab = (TextView) view.findViewById(R.id.show_pictures);
329        // Invite is only used in MessageViewFragment, but visibility is controlled here.
330        mInviteTab = (TextView) view.findViewById(R.id.show_invite);
331
332        mMessageTab.setOnClickListener(this);
333        mAttachmentTab.setOnClickListener(this);
334        mShowPicturesTab.setOnClickListener(this);
335        mInviteTab.setOnClickListener(this);
336
337        mAttachmentsScroll = view.findViewById(R.id.attachments_scroll);
338        mInviteScroll = view.findViewById(R.id.invite_scroll);
339
340        mMessageContentView.setVerticalScrollBarEnabled(false);
341        WebSettings webSettings = mMessageContentView.getSettings();
342        webSettings.setBlockNetworkLoads(true);
343        webSettings.setSupportZoom(true);
344        webSettings.setBuiltInZoomControls(true);
345        mMessageContentView.setWebViewClient(new CustomWebViewClient());
346        return view;
347    }
348
349    @Override
350    public void onActivityCreated(Bundle savedInstanceState) {
351        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
352            Log.d(Email.LOG_TAG, "MessageViewFragment onActivityCreated");
353        }
354        super.onActivityCreated(savedInstanceState);
355        mController.addResultCallback(mControllerCallback);
356    }
357
358    @Override
359    public void onStart() {
360        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
361            Log.d(Email.LOG_TAG, "MessageViewFragment onStart");
362        }
363        super.onStart();
364    }
365
366    @Override
367    public void onResume() {
368        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
369            Log.d(Email.LOG_TAG, "MessageViewFragment onResume");
370        }
371        super.onResume();
372
373        mResumed = true;
374        if (isMessageSpecified()) {
375            if (mLoadWhenResumed) {
376                loadMessageIfResumed();
377            } else {
378                // This means, the user comes back from other (full-screen) activities.
379                // In this case we've already loaded the content, so don't load it again,
380                // which results in resetting all view state, including WebView zoom/pan
381                // and the current tab.
382            }
383        }
384    }
385
386    @Override
387    public void onPause() {
388        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
389            Log.d(Email.LOG_TAG, "MessageViewFragment onPause");
390        }
391        mResumed = false;
392        super.onPause();
393    }
394
395    @Override
396    public void onStop() {
397        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
398            Log.d(Email.LOG_TAG, "MessageViewFragment onStop");
399        }
400        super.onStop();
401    }
402
403    @Override
404    public void onDestroy() {
405        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
406            Log.d(Email.LOG_TAG, "MessageViewFragment onDestroy");
407        }
408        mCallback.onMessageViewGone();
409        mController.removeResultCallback(mControllerCallback);
410        clearContent();
411        mMessageContentView.destroy();
412        mMessageContentView = null;
413        super.onDestroy();
414    }
415
416    @Override
417    public void onSaveInstanceState(Bundle outState) {
418        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
419            Log.d(Email.LOG_TAG, "MessageViewFragment onSaveInstanceState");
420        }
421        super.onSaveInstanceState(outState);
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        Utility.cancelTaskInterrupt(mLoadMessageTask);
431        mLoadMessageTask = null;
432        Utility.cancelTaskInterrupt(mReloadMessageTask);
433        mReloadMessageTask = null;
434        Utility.cancelTaskInterrupt(mLoadBodyTask);
435        mLoadBodyTask = null;
436        Utility.cancelTaskInterrupt(mLoadAttachmentsTask);
437        mLoadAttachmentsTask = null;
438    }
439
440    /**
441     * Subclass returns true if which message to open is already specified by the activity.
442     */
443    protected abstract boolean isMessageSpecified();
444
445    protected final Controller getController() {
446        return mController;
447    }
448
449    protected final Callback getCallback() {
450        return mCallback;
451    }
452
453    protected final Message getMessage() {
454        return mMessage;
455    }
456
457    protected final boolean isMessageOpen() {
458        return mMessage != null;
459    }
460
461    /**
462     * Returns the account id of the current message, or -1 if unknown (message not open yet, or
463     * viewing an EML message).
464     */
465    public long getAccountId() {
466        return mAccountId;
467    }
468
469    /**
470     * Clear all the content -- should be called when the fragment is hidden.
471     */
472    public void clearContent() {
473        cancelAllTasks();
474        resetView();
475    }
476
477    protected final void loadMessageIfResumed() {
478        if (!mResumed) {
479            mLoadWhenResumed = true;
480            return;
481        }
482        mLoadWhenResumed = false;
483        cancelAllTasks();
484        resetView();
485        mLoadMessageTask = new LoadMessageTask(true);
486        mLoadMessageTask.execute();
487    }
488
489    /**
490     * Show/hide the content.  We hide all the content (except for the bottom buttons) when loading,
491     * to avoid flicker.
492     */
493    private void showContent(boolean show) {
494        if (mLoadingProgress == null) {
495            // Phone UI doesn't have it yet.
496            // TODO Add loading_progress and main_panel to the phone layout too.
497        } else {
498            mMainView.clearAnimation();
499            mLoadingProgress.clearAnimation();
500            makeVisible(mMainView, show);
501            makeVisible(mLoadingProgress, !show);
502            if (show) {
503                // When showing, fade it in.  I'll look much smoother.
504                mMainView.startAnimation(mFadeInAnimation);
505                mLoadingProgress.startAnimation(mFadeOutAnimation);
506            } else {
507                // When hiding, don't fade it out, to hide flicker.
508            }
509        }
510    }
511
512    protected void resetView() {
513        showContent(false);
514        setCurrentTab(TAB_MESSAGE);
515        updateTabFlags(0);
516        if (mMessageContentView != null) {
517            mMessageContentView.getSettings().setBlockNetworkLoads(true);
518            mMessageContentView.scrollTo(0, 0);
519            mMessageContentView.loadUrl("file:///android_asset/empty.html");
520
521            // Dynamic configuration of WebView
522            WebSettings.TextSize textZoom;
523            switch (Preferences.getPreferences(mContext).getTextZoom()) {
524                case Preferences.TEXT_ZOOM_TINY:    textZoom = WebSettings.TextSize.SMALLEST; break;
525                case Preferences.TEXT_ZOOM_SMALL:   textZoom = WebSettings.TextSize.SMALLER; break;
526                case Preferences.TEXT_ZOOM_NORMAL:  textZoom = WebSettings.TextSize.NORMAL; break;
527                case Preferences.TEXT_ZOOM_LARGE:   textZoom = WebSettings.TextSize.LARGER; break;
528                case Preferences.TEXT_ZOOM_HUGE:    textZoom = WebSettings.TextSize.LARGEST; break;
529                default:                            textZoom = WebSettings.TextSize.NORMAL; break;
530            }
531            mMessageContentView.getSettings().setTextSize(textZoom);
532        }
533        mAttachmentsScroll.scrollTo(0, 0);
534        mInviteScroll.scrollTo(0, 0);
535        mAttachments.removeAllViews();
536        mAttachments.setVisibility(View.GONE);
537        initContactStatusViews();
538    }
539
540    private void initContactStatusViews() {
541        mContactStatusState = CONTACT_STATUS_STATE_UNLOADED;
542        mQuickContactLookupUri = null;
543        mSenderPresenceView.setImageResource(ContactStatusLoader.PRESENCE_UNKNOWN_RESOURCE_ID);
544        showDefaultQuickContactBadgeImage();
545    }
546
547    private void showDefaultQuickContactBadgeImage() {
548        mFromBadge.setImageResource(R.drawable.ic_contact_picture);
549    }
550
551    protected final void addTabFlags(int tabFlags) {
552        updateTabFlags(mTabFlags | tabFlags);
553    }
554
555    private final void clearTabFlags(int tabFlags) {
556        updateTabFlags(mTabFlags & ~tabFlags);
557    }
558
559    private void setAttachmentCount(int count) {
560        mAttachmentCount = count;
561        if (mAttachmentCount > 0) {
562            addTabFlags(TAB_FLAGS_HAS_ATTACHMENT);
563        } else {
564            clearTabFlags(TAB_FLAGS_HAS_ATTACHMENT);
565        }
566    }
567
568    private static void makeVisible(View v, boolean visible) {
569        v.setVisibility(visible ? View.VISIBLE : View.GONE);
570    }
571
572    /**
573     * Update the visual of the tabs.  (visibility, text, etc)
574     */
575    private void updateTabFlags(int tabFlags) {
576        mTabFlags = tabFlags;
577        mTabSection.setVisibility(tabFlags == 0 ? View.GONE : View.VISIBLE);
578        if (tabFlags == 0) {
579            return;
580        }
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        makeVisible(mShowPicturesTab, (tabFlags & TAB_FLAGS_HAS_PICTURES) != 0);
587        mShowPicturesTab.setEnabled((tabFlags & TAB_FLAGS_PICTURE_LOADED) == 0);
588
589        mAttachmentTab.setText(mContext.getResources().getQuantityString(
590                R.plurals.message_view_show_attachments_action,
591                mAttachmentCount, mAttachmentCount));
592    }
593
594    /**
595     * Set the current tab.
596     *
597     * @param tab any of {@link #TAB_MESSAGE}, {@link #TAB_ATTACHMENT} or {@link #TAB_INVITE}.
598     */
599    private void setCurrentTab(int tab) {
600        mCurrentTab = tab;
601        makeVisible(mMessageContentView, tab == TAB_MESSAGE);
602        mMessageTab.setSelected(tab == TAB_MESSAGE);
603
604        makeVisible(mAttachmentsScroll, tab == TAB_ATTACHMENT);
605        mAttachmentTab.setSelected(tab == TAB_ATTACHMENT);
606
607        makeVisible(mInviteScroll, tab == TAB_INVITE);
608        mInviteTab.setSelected(tab == TAB_INVITE);
609    }
610
611    /**
612     * Handle clicks on sender, which shows {@link QuickContact} or prompts to add
613     * the sender as a contact.
614     */
615    private void onClickSender() {
616        final Address senderEmail = Address.unpackFirst(mMessage.mFrom);
617        if (senderEmail == null) return;
618
619        if (mContactStatusState == CONTACT_STATUS_STATE_UNLOADED) {
620            // Status not loaded yet.
621            mContactStatusState = CONTACT_STATUS_STATE_UNLOADED_TRIGGERED;
622            return;
623        }
624        if (mContactStatusState == CONTACT_STATUS_STATE_UNLOADED_TRIGGERED) {
625            return; // Already clicked, and waiting for the data.
626        }
627
628        if (mQuickContactLookupUri != null) {
629            QuickContact.showQuickContact(mContext, mFromBadge, mQuickContactLookupUri,
630                        QuickContact.MODE_LARGE, null);
631        } else {
632            // No matching contact, ask user to create one
633            final Uri mailUri = Uri.fromParts("mailto", senderEmail.getAddress(), null);
634            final Intent intent = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT,
635                    mailUri);
636
637            // Pass along full E-mail string for possible create dialog
638            intent.putExtra(ContactsContract.Intents.EXTRA_CREATE_DESCRIPTION,
639                    senderEmail.toString());
640
641            // Only provide personal name hint if we have one
642            final String senderPersonal = senderEmail.getPersonal();
643            if (!TextUtils.isEmpty(senderPersonal)) {
644                intent.putExtra(ContactsContract.Intents.Insert.NAME, senderPersonal);
645            }
646            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
647
648            startActivity(intent);
649        }
650    }
651
652    private static class ContactStatusLoaderCallbacks
653            implements LoaderCallbacks<ContactStatusLoader.Result> {
654        private static final String BUNDLE_EMAIL_ADDRESS = "email";
655        private final MessageViewFragmentBase mFragment;
656
657        public ContactStatusLoaderCallbacks(MessageViewFragmentBase fragment) {
658            mFragment = fragment;
659        }
660
661        public static Bundle createArguments(String emailAddress) {
662            Bundle b = new Bundle();
663            b.putString(BUNDLE_EMAIL_ADDRESS, emailAddress);
664            return b;
665        }
666
667        @Override
668        public Loader<ContactStatusLoader.Result> onCreateLoader(int id, Bundle args) {
669            return new ContactStatusLoader(mFragment.mContext,
670                    args.getString(BUNDLE_EMAIL_ADDRESS));
671        }
672
673        @Override
674        public void onLoadFinished(Loader<ContactStatusLoader.Result> loader,
675                ContactStatusLoader.Result result) {
676            boolean triggered =
677                    (mFragment.mContactStatusState == CONTACT_STATUS_STATE_UNLOADED_TRIGGERED);
678            mFragment.mContactStatusState = CONTACT_STATUS_STATE_LOADED;
679            mFragment.mQuickContactLookupUri = result.mLookupUri;
680            mFragment.mSenderPresenceView.setImageResource(result.mPresenceResId);
681            if (result.mPhoto != null) { // photo will be null if unknown.
682                mFragment.mFromBadge.setImageBitmap(result.mPhoto);
683            }
684            if (triggered) {
685                mFragment.onClickSender();
686            }
687        }
688
689        @Override
690        public void onLoaderReset(Loader<ContactStatusLoader.Result> loader) {
691        }
692    }
693
694    private void onSaveAttachment(AttachmentInfo info) {
695        if (!Utility.isExternalStorageMounted()) {
696            /*
697             * Abort early if there's no place to save the attachment. We don't want to spend
698             * the time downloading it and then abort.
699             */
700            Utility.showToast(getActivity(), R.string.message_view_status_attachment_not_saved);
701            return;
702        }
703        Attachment attachment = Attachment.restoreAttachmentWithId(mContext, info.attachmentId);
704        Uri attachmentUri = AttachmentProvider.getAttachmentUri(mAccountId, attachment.mId);
705
706        try {
707            File file = Utility.createUniqueFile(Environment.getExternalStorageDirectory(),
708                    attachment.mFileName);
709            Uri contentUri = AttachmentProvider.resolveAttachmentIdToContentUri(
710                    mContext.getContentResolver(), attachmentUri);
711            InputStream in = mContext.getContentResolver().openInputStream(contentUri);
712            OutputStream out = new FileOutputStream(file);
713            IOUtils.copy(in, out);
714            out.flush();
715            out.close();
716            in.close();
717
718            Utility.showToast(getActivity(), String.format(
719                    mContext.getString(R.string.message_view_status_attachment_saved),
720                    file.getName()));
721            MediaOpener.scanAndOpen(getActivity(), file);
722        } catch (IOException ioe) {
723            Utility.showToast(getActivity(), R.string.message_view_status_attachment_not_saved);
724        }
725    }
726
727    private void onViewAttachment(AttachmentInfo info) {
728        Uri attachmentUri = AttachmentProvider.getAttachmentUri(mAccountId, info.attachmentId);
729        Uri contentUri = AttachmentProvider.resolveAttachmentIdToContentUri(
730                mContext.getContentResolver(), attachmentUri);
731        try {
732            Intent intent = new Intent(Intent.ACTION_VIEW);
733            intent.setData(contentUri);
734            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
735                            | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
736            startActivity(intent);
737        } catch (ActivityNotFoundException e) {
738            Utility.showToast(getActivity(), R.string.message_view_display_attachment_toast);
739            // TODO: Add a proper warning message (and lots of upstream cleanup to prevent
740            // it from happening) in the next release.
741        }
742    }
743
744    private void onLoadAttachment(final AttachmentInfo attachment) {
745        attachment.loadButton.setVisibility(View.GONE);
746        // If there's nothing in the download queue, we'll probably start right away so wait a
747        // second before showing the cancel button
748        if (AttachmentDownloadService.getQueueSize() == 0) {
749            // Set to invisible; if the button is still in this state one second from now, we'll
750            // assume the download won't start right away, and we make the cancel button visible
751            attachment.cancelButton.setVisibility(View.INVISIBLE);
752            // Create the timed task that will change the button state
753            new AsyncTask<Void, Void, Void>() {
754                @Override
755                protected Void doInBackground(Void... params) {
756                    try {
757                        Thread.sleep(1000L);
758                    } catch (InterruptedException e) { }
759                    return null;
760                }
761                @Override
762                protected void onPostExecute(Void result) {
763                    if (attachment.cancelButton.getVisibility() == View.INVISIBLE) {
764                        attachment.cancelButton.setVisibility(View.VISIBLE);
765                    }
766                }
767            }.execute();
768        } else {
769            attachment.cancelButton.setVisibility(View.VISIBLE);
770        }
771        ProgressBar bar = attachment.progressView;
772        bar.setVisibility(View.VISIBLE);
773        bar.setIndeterminate(true);
774        mController.loadAttachment(attachment.attachmentId, mMessageId, mAccountId);
775    }
776
777    private void onCancelAttachment(AttachmentInfo attachment) {
778        // Don't change button states if we couldn't cancel the download
779        if (AttachmentDownloadService.cancelQueuedAttachment(attachment.attachmentId)) {
780            attachment.loadButton.setVisibility(View.VISIBLE);
781            attachment.cancelButton.setVisibility(View.GONE);
782            ProgressBar bar = attachment.progressView;
783            bar.setVisibility(View.GONE);
784        }
785    }
786
787    /**
788     * Called by ControllerResults. Show the "View" and "Save" buttons; hide "Load"
789     *
790     * @param attachmentId the attachment that was just downloaded
791     */
792    private void doFinishLoadAttachment(long attachmentId) {
793        AttachmentInfo info = findAttachmentInfo(attachmentId);
794        if (info != null) {
795            info.loadButton.setVisibility(View.INVISIBLE);
796            info.loadButton.setVisibility(View.GONE);
797            if (!TextUtils.isEmpty(info.name)) {
798                info.saveButton.setVisibility(View.VISIBLE);
799            }
800            info.viewButton.setVisibility(View.VISIBLE);
801        }
802    }
803
804    private void onShowPicturesInHtml() {
805        if (mMessageContentView != null) {
806            mMessageContentView.getSettings().setBlockNetworkLoads(false);
807            if (mHtmlTextWebView != null) {
808                mMessageContentView.loadDataWithBaseURL("email://", mHtmlTextWebView,
809                                                        "text/html", "utf-8", null);
810            }
811            addTabFlags(TAB_FLAGS_PICTURE_LOADED);
812        }
813    }
814
815    @Override
816    public void onClick(View view) {
817        if (!isMessageOpen()) {
818            return; // Ignore.
819        }
820        switch (view.getId()) {
821            case R.id.from_name:
822            case R.id.from_address:
823            case R.id.badge:
824            case R.id.presence:
825                onClickSender();
826                break;
827            case R.id.load:
828                onLoadAttachment((AttachmentInfo) view.getTag());
829                break;
830            case R.id.save:
831                onSaveAttachment((AttachmentInfo) view.getTag());
832                break;
833            case R.id.view:
834                onViewAttachment((AttachmentInfo) view.getTag());
835                break;
836            case R.id.cancel:
837                onCancelAttachment((AttachmentInfo) view.getTag());
838                break;
839            case R.id.show_message:
840                setCurrentTab(TAB_MESSAGE);
841                break;
842            case R.id.show_invite:
843                setCurrentTab(TAB_INVITE);
844                break;
845            case R.id.show_attachments:
846                setCurrentTab(TAB_ATTACHMENT);
847                break;
848            case R.id.show_pictures:
849                onShowPicturesInHtml();
850                break;
851        }
852    }
853
854    /**
855     * Start loading contact photo and presence.
856     */
857    private void queryContactStatus() {
858        initContactStatusViews(); // Initialize the state, just in case.
859
860        // Find the sender email address, and start presence check.
861        if (mMessage != null) {
862            Address sender = Address.unpackFirst(mMessage.mFrom);
863            if (sender != null) {
864                String email = sender.getAddress();
865                if (email != null) {
866                    getLoaderManager().restartLoader(PHOTO_LOADER_ID,
867                            ContactStatusLoaderCallbacks.createArguments(email),
868                            new ContactStatusLoaderCallbacks(this));
869                }
870            }
871        }
872    }
873
874    /**
875     * Called by {@link LoadMessageTask} and {@link ReloadMessageTask} to load a message in a
876     * subclass specific way.
877     *
878     * NOTE This method is called on a worker thread!  Implementations must properly synchronize
879     * when accessing members.  This method may be called after or even at the same time as
880     * {@link #clearContent()}.
881     *
882     * @param activity the parent activity.  Subclass use it as a context, and to show a toast.
883     */
884    protected abstract Message openMessageSync(Activity activity);
885
886    /**
887     * Async task for loading a single message outside of the UI thread
888     */
889    private class LoadMessageTask extends AsyncTask<Void, Void, Message> {
890
891        private final boolean mOkToFetch;
892        private int mMailboxType;
893
894        /**
895         * Special constructor to cache some local info
896         */
897        public LoadMessageTask(boolean okToFetch) {
898            mOkToFetch = okToFetch;
899        }
900
901        @Override
902        protected Message doInBackground(Void... params) {
903            Activity activity = getActivity();
904            Message message = null;
905            if (activity != null) {
906                message = openMessageSync(activity);
907            }
908            if (message != null) {
909                mMailboxType = Mailbox.getMailboxType(mContext, message.mMailboxKey);
910                if (mMailboxType == -1) {
911                    message = null; // mailbox removed??
912                }
913            }
914            return message;
915        }
916
917        @Override
918        protected void onPostExecute(Message message) {
919            if (isCancelled()) {
920                return;
921            }
922            if (message == null) {
923                mCallback.onMessageNotExists();
924                return;
925            }
926            mMessageId = message.mId;
927
928            reloadUiFromMessage(message, mOkToFetch);
929            queryContactStatus();
930            onMessageShown(mMessageId, mMailboxType);
931        }
932    }
933
934    /**
935     * Kicked by {@link MessageObserver}.  Reload the message and update the views.
936     */
937    private class ReloadMessageTask extends AsyncTask<Void, Void, Message> {
938        @Override
939        protected Message doInBackground(Void... params) {
940            if (!isMessageSpecified()) { // just in case
941                return null;
942            }
943            Activity activity = getActivity();
944            if (activity == null) {
945                return null;
946            } else {
947                return openMessageSync(activity);
948            }
949        }
950
951        @Override
952        protected void onPostExecute(Message message) {
953            if (isCancelled()) {
954                return;
955            }
956            if (message == null || message.mMailboxKey != mMessage.mMailboxKey) {
957                // Message deleted or moved.
958                mCallback.onMessageNotExists();
959                return;
960            }
961            mMessage = message;
962            updateHeaderView(mMessage);
963        }
964    }
965
966    /**
967     * Called when a message is shown to the user.
968     */
969    protected void onMessageShown(long messageId, int mailboxType) {
970        mCallback.onMessageViewShown(mailboxType);
971    }
972
973    /**
974     * Called when the message body is loaded.
975     */
976    protected void onPostLoadBody() {
977    }
978
979    /**
980     * Async task for loading a single message body outside of the UI thread
981     */
982    private class LoadBodyTask extends AsyncTask<Void, Void, String[]> {
983
984        private long mId;
985        private boolean mErrorLoadingMessageBody;
986
987        /**
988         * Special constructor to cache some local info
989         */
990        public LoadBodyTask(long messageId) {
991            mId = messageId;
992        }
993
994        @Override
995        protected String[] doInBackground(Void... params) {
996            try {
997                String text = null;
998                String html = Body.restoreBodyHtmlWithMessageId(mContext, mId);
999                if (html == null) {
1000                    text = Body.restoreBodyTextWithMessageId(mContext, mId);
1001                }
1002                return new String[] { text, html };
1003            } catch (RuntimeException re) {
1004                // This catches SQLiteException as well as other RTE's we've seen from the
1005                // database calls, such as IllegalStateException
1006                Log.d(Email.LOG_TAG, "Exception while loading message body: " + re.toString());
1007                mErrorLoadingMessageBody = true;
1008                return null;
1009            }
1010        }
1011
1012        @Override
1013        protected void onPostExecute(String[] results) {
1014            if (results == null || isCancelled()) {
1015                if (mErrorLoadingMessageBody) {
1016                    Utility.showToast(getActivity(), R.string.error_loading_message_body);
1017                }
1018                return;
1019            }
1020            reloadUiFromBody(results[0], results[1]);    // text, html
1021            onPostLoadBody();
1022        }
1023    }
1024
1025    /**
1026     * Async task for loading attachments
1027     *
1028     * Note:  This really should only be called when the message load is complete - or, we should
1029     * leave open a listener so the attachments can fill in as they are discovered.  In either case,
1030     * this implementation is incomplete, as it will fail to refresh properly if the message is
1031     * partially loaded at this time.
1032     */
1033    private class LoadAttachmentsTask extends AsyncTask<Long, Void, Attachment[]> {
1034        @Override
1035        protected Attachment[] doInBackground(Long... messageIds) {
1036            return Attachment.restoreAttachmentsWithMessageId(mContext, messageIds[0]);
1037        }
1038
1039        @Override
1040        protected void onPostExecute(Attachment[] attachments) {
1041            if (isCancelled() || attachments == null) {
1042                return;
1043            }
1044            boolean htmlChanged = false;
1045            int numDisplayedAttachments = 0;
1046            for (Attachment attachment : attachments) {
1047                if (mHtmlTextRaw != null && attachment.mContentId != null
1048                        && attachment.mContentUri != null) {
1049                    // for html body, replace CID for inline images
1050                    // Regexp which matches ' src="cid:contentId"'.
1051                    String contentIdRe =
1052                        "\\s+(?i)src=\"cid(?-i):\\Q" + attachment.mContentId + "\\E\"";
1053                    String srcContentUri = " src=\"" + attachment.mContentUri + "\"";
1054                    mHtmlTextRaw = mHtmlTextRaw.replaceAll(contentIdRe, srcContentUri);
1055                    htmlChanged = true;
1056                } else {
1057                    addAttachment(attachment);
1058                    numDisplayedAttachments++;
1059                }
1060            }
1061            setAttachmentCount(numDisplayedAttachments);
1062            mHtmlTextWebView = mHtmlTextRaw;
1063            mHtmlTextRaw = null;
1064            if (htmlChanged && mMessageContentView != null) {
1065                mMessageContentView.loadDataWithBaseURL("email://", mHtmlTextWebView,
1066                                                        "text/html", "utf-8", null);
1067            }
1068        }
1069    }
1070
1071    private Bitmap getPreviewIcon(AttachmentInfo attachment) {
1072        try {
1073            return BitmapFactory.decodeStream(
1074                    mContext.getContentResolver().openInputStream(
1075                            AttachmentProvider.getAttachmentThumbnailUri(
1076                                    mAccountId, attachment.attachmentId,
1077                                    PREVIEW_ICON_WIDTH,
1078                                    PREVIEW_ICON_HEIGHT)));
1079        } catch (Exception e) {
1080            Log.d(Email.LOG_TAG, "Attachment preview failed with exception " + e.getMessage());
1081            return null;
1082        }
1083    }
1084
1085    private void updateAttachmentThumbnail(long attachmentId) {
1086        for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
1087            AttachmentInfo attachment = (AttachmentInfo) mAttachments.getChildAt(i).getTag();
1088            if (attachment.attachmentId == attachmentId) {
1089                Bitmap previewIcon = getPreviewIcon(attachment);
1090                if (previewIcon != null) {
1091                    attachment.iconView.setImageBitmap(previewIcon);
1092                }
1093                return;
1094            }
1095        }
1096    }
1097
1098    /**
1099     * Copy data from a cursor-refreshed attachment into the UI.  Called from UI thread.
1100     *
1101     * @param attachment A single attachment loaded from the provider
1102     */
1103    private void addAttachment(Attachment attachment) {
1104        AttachmentInfo attachmentInfo = new AttachmentInfo();
1105        attachmentInfo.size = attachment.mSize;
1106        attachmentInfo.contentType =
1107                AttachmentProvider.inferMimeType(attachment.mFileName, attachment.mMimeType);
1108        attachmentInfo.name = attachment.mFileName;
1109        attachmentInfo.attachmentId = attachment.mId;
1110
1111        LayoutInflater inflater = getActivity().getLayoutInflater();
1112        View view = inflater.inflate(R.layout.message_view_attachment, null);
1113
1114        TextView attachmentName = (TextView)view.findViewById(R.id.attachment_name);
1115        TextView attachmentInfoView = (TextView)view.findViewById(R.id.attachment_info);
1116        ImageView attachmentIcon = (ImageView)view.findViewById(R.id.attachment_icon);
1117        Button attachmentView = (Button)view.findViewById(R.id.view);
1118        Button attachmentSave = (Button)view.findViewById(R.id.save);
1119        Button attachmentLoad = (Button)view.findViewById(R.id.load);
1120        Button attachmentCancel = (Button)view.findViewById(R.id.cancel);
1121        ProgressBar attachmentProgress = (ProgressBar)view.findViewById(R.id.progress);
1122
1123        // TODO: Remove this test (acceptable types = everything; unacceptable = nothing)
1124        if ((!MimeUtility.mimeTypeMatches(attachmentInfo.contentType,
1125                Email.ACCEPTABLE_ATTACHMENT_VIEW_TYPES))
1126                || (MimeUtility.mimeTypeMatches(attachmentInfo.contentType,
1127                        Email.UNACCEPTABLE_ATTACHMENT_VIEW_TYPES))) {
1128            attachmentView.setVisibility(View.GONE);
1129        }
1130
1131        if (attachmentInfo.size > Email.MAX_ATTACHMENT_DOWNLOAD_SIZE) {
1132            attachmentView.setVisibility(View.GONE);
1133            attachmentSave.setVisibility(View.GONE);
1134        }
1135
1136        attachmentInfo.viewButton = attachmentView;
1137        attachmentInfo.saveButton = attachmentSave;
1138        attachmentInfo.loadButton = attachmentLoad;
1139        attachmentInfo.cancelButton = attachmentCancel;
1140        attachmentInfo.iconView = attachmentIcon;
1141        attachmentInfo.progressView = attachmentProgress;
1142
1143        // If the attachment is loaded, show 100% progress
1144        // Note that for POP3 messages, the user will only see "Open" and "Save" since the entire
1145        // message is loaded before being shown.
1146        if (Utility.attachmentExists(mContext, attachment)) {
1147            // Hide "Load", show "View" and "Save"
1148            attachmentProgress.setVisibility(View.VISIBLE);
1149            attachmentProgress.setProgress(100);
1150            attachmentSave.setVisibility(View.VISIBLE);
1151            attachmentView.setVisibility(View.VISIBLE);
1152            attachmentLoad.setVisibility(View.INVISIBLE);
1153            attachmentCancel.setVisibility(View.GONE);
1154        } else {
1155            // Show "Load"; hide "View" and "Save"
1156            attachmentSave.setVisibility(View.INVISIBLE);
1157            attachmentView.setVisibility(View.INVISIBLE);
1158            // If the attachment is queued, show the indeterminate progress bar.  From this point,.
1159            // any progress changes will cause this to be replaced by the normal progress bar
1160            if (AttachmentDownloadService.isAttachmentQueued(attachment.mId)){
1161                attachmentProgress.setVisibility(View.VISIBLE);
1162                attachmentProgress.setIndeterminate(true);
1163                attachmentLoad.setVisibility(View.GONE);
1164                attachmentCancel.setVisibility(View.VISIBLE);
1165            } else {
1166                attachmentLoad.setVisibility(View.VISIBLE);
1167                attachmentCancel.setVisibility(View.GONE);
1168            }
1169        }
1170
1171        // Don't enable the "save" button if we've got no place to save the file
1172        if (!Utility.isExternalStorageMounted()) {
1173            attachmentSave.setEnabled(false);
1174        }
1175
1176        view.setTag(attachmentInfo);
1177        attachmentView.setOnClickListener(this);
1178        attachmentView.setTag(attachmentInfo);
1179        attachmentSave.setOnClickListener(this);
1180        attachmentSave.setTag(attachmentInfo);
1181        attachmentLoad.setOnClickListener(this);
1182        attachmentLoad.setTag(attachmentInfo);
1183        attachmentCancel.setOnClickListener(this);
1184        attachmentCancel.setTag(attachmentInfo);
1185
1186        attachmentName.setText(attachmentInfo.name);
1187        attachmentInfoView.setText(Utility.formatSize(mContext, attachmentInfo.size));
1188
1189        Bitmap previewIcon = getPreviewIcon(attachmentInfo);
1190        if (previewIcon != null) {
1191            attachmentIcon.setImageBitmap(previewIcon);
1192        }
1193
1194        mAttachments.addView(view);
1195        mAttachments.setVisibility(View.VISIBLE);
1196    }
1197
1198    /**
1199     * Reload the UI from a provider cursor.  {@link LoadMessageTask#onPostExecute} calls it.
1200     *
1201     * Update the header views, and start loading the body.
1202     *
1203     * @param message A copy of the message loaded from the database
1204     * @param okToFetch If true, and message is not fully loaded, it's OK to fetch from
1205     * the network.  Use false to prevent looping here.
1206     */
1207    protected void reloadUiFromMessage(Message message, boolean okToFetch) {
1208        mMessage = message;
1209        mAccountId = message.mAccountKey;
1210
1211        mMessageObserver.register(ContentUris.withAppendedId(Message.CONTENT_URI, mMessage.mId));
1212
1213        updateHeaderView(mMessage);
1214
1215        // Handle partially-loaded email, as follows:
1216        // 1. Check value of message.mFlagLoaded
1217        // 2. If != LOADED, ask controller to load it
1218        // 3. Controller callback (after loaded) should trigger LoadBodyTask & LoadAttachmentsTask
1219        // 4. Else start the loader tasks right away (message already loaded)
1220        if (okToFetch && message.mFlagLoaded != Message.FLAG_LOADED_COMPLETE) {
1221            mControllerCallback.getWrappee().setWaitForLoadMessageId(message.mId);
1222            mController.loadMessageForView(message.mId);
1223        } else {
1224            mControllerCallback.getWrappee().setWaitForLoadMessageId(-1);
1225            // Ask for body
1226            mLoadBodyTask = new LoadBodyTask(message.mId);
1227            mLoadBodyTask.execute();
1228        }
1229    }
1230
1231    protected void updateHeaderView(Message message) {
1232        mSubjectView.setText(message.mSubject);
1233        final Address from = Address.unpackFirst(message.mFrom);
1234
1235        // Set sender address/display name
1236        // Note we set " " for empty field, so TextView's won't get squashed.
1237        // Otherwise their height will be 0, which breaks the layout.
1238        if (from != null) {
1239            final String fromFriendly = from.toFriendly();
1240            final String fromAddress = from.getAddress();
1241            mFromNameView.setText(fromFriendly);
1242            mFromAddressView.setText(fromFriendly.equals(fromAddress) ? " " : fromAddress);
1243        } else {
1244            mFromNameView.setText(" ");
1245            mFromAddressView.setText(" ");
1246        }
1247        Date date = new Date(message.mTimeStamp);
1248        // STOPSHIP Use the same format as MessageListItem uses
1249        mDateTimeView.setText(mTimeFormat.format(date));
1250        mToView.setText(Address.toFriendly(Address.unpack(message.mTo)));
1251        String friendlyCc = Address.toFriendly(Address.unpack(message.mCc));
1252        mCcView.setText(friendlyCc);
1253        mCcContainerView.setVisibility((friendlyCc != null) ? View.VISIBLE : View.GONE);
1254        String friendlyBcc = Address.toFriendly(Address.unpack(message.mBcc));
1255        mBccView.setText(friendlyBcc);
1256        mBccContainerView.setVisibility((friendlyBcc != null) ? View.VISIBLE : View.GONE);
1257    }
1258
1259    /**
1260     * Reload the body from the provider cursor.  This must only be called from the UI thread.
1261     *
1262     * @param bodyText text part
1263     * @param bodyHtml html part
1264     *
1265     * TODO deal with html vs text and many other issues <- WHAT DOES IT MEAN??
1266     */
1267    private void reloadUiFromBody(String bodyText, String bodyHtml) {
1268        String text = null;
1269        mHtmlTextRaw = null;
1270        boolean hasImages = false;
1271
1272        if (bodyHtml == null) {
1273            text = bodyText;
1274            /*
1275             * Convert the plain text to HTML
1276             */
1277            StringBuffer sb = new StringBuffer("<html><body>");
1278            if (text != null) {
1279                // Escape any inadvertent HTML in the text message
1280                text = EmailHtmlUtil.escapeCharacterToDisplay(text);
1281                // Find any embedded URL's and linkify
1282                Matcher m = Patterns.WEB_URL.matcher(text);
1283                while (m.find()) {
1284                    int start = m.start();
1285                    /*
1286                     * WEB_URL_PATTERN may match domain part of email address. To detect
1287                     * this false match, the character just before the matched string
1288                     * should not be '@'.
1289                     */
1290                    if (start == 0 || text.charAt(start - 1) != '@') {
1291                        String url = m.group();
1292                        Matcher proto = WEB_URL_PROTOCOL.matcher(url);
1293                        String link;
1294                        if (proto.find()) {
1295                            // This is work around to force URL protocol part be lower case,
1296                            // because WebView could follow only lower case protocol link.
1297                            link = proto.group().toLowerCase() + url.substring(proto.end());
1298                        } else {
1299                            // Patterns.WEB_URL matches URL without protocol part,
1300                            // so added default protocol to link.
1301                            link = "http://" + url;
1302                        }
1303                        String href = String.format("<a href=\"%s\">%s</a>", link, url);
1304                        m.appendReplacement(sb, href);
1305                    }
1306                    else {
1307                        m.appendReplacement(sb, "$0");
1308                    }
1309                }
1310                m.appendTail(sb);
1311            }
1312            sb.append("</body></html>");
1313            text = sb.toString();
1314        } else {
1315            text = bodyHtml;
1316            mHtmlTextRaw = bodyHtml;
1317            hasImages = IMG_TAG_START_REGEX.matcher(text).find();
1318        }
1319
1320        // TODO this is not really accurate.
1321        // - Images aren't the only network resources.  (e.g. CSS)
1322        // - If images are attached to the email and small enough, we download them at once,
1323        //   and won't need network access when they're shown.
1324        if (hasImages) {
1325            addTabFlags(TAB_FLAGS_HAS_PICTURES);
1326        }
1327        if (mMessageContentView != null) {
1328            mMessageContentView.loadDataWithBaseURL("email://", text, "text/html", "utf-8", null);
1329        }
1330
1331        // Ask for attachments after body
1332        mLoadAttachmentsTask = new LoadAttachmentsTask();
1333        mLoadAttachmentsTask.execute(mMessage.mId);
1334
1335        mIsMessageLoadedForTest = true;
1336        showContent(true);
1337    }
1338
1339    /**
1340     * Overrides for WebView behaviors.
1341     */
1342    private class CustomWebViewClient extends WebViewClient {
1343        @Override
1344        public boolean shouldOverrideUrlLoading(WebView view, String url) {
1345            return mCallback.onUrlInMessageClicked(url);
1346        }
1347    }
1348
1349    private View findAttachmentView(long attachmentId) {
1350        for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
1351            View view = mAttachments.getChildAt(i);
1352            AttachmentInfo attachment = (AttachmentInfo) view.getTag();
1353            if (attachment.attachmentId == attachmentId) {
1354                return view;
1355            }
1356        }
1357        return null;
1358    }
1359
1360    private AttachmentInfo findAttachmentInfo(long attachmentId) {
1361        View view = findAttachmentView(attachmentId);
1362        if (view != null) {
1363            return (AttachmentInfo)view.getTag();
1364        }
1365        return null;
1366    }
1367
1368    /**
1369     * Controller results listener.  We wrap it with {@link ControllerResultUiThreadWrapper},
1370     * so all methods are called on the UI thread.
1371     */
1372    private class ControllerResults extends Controller.Result {
1373        private long mWaitForLoadMessageId;
1374
1375        public void setWaitForLoadMessageId(long messageId) {
1376            mWaitForLoadMessageId = messageId;
1377        }
1378
1379        @Override
1380        public void loadMessageForViewCallback(MessagingException result, long accountId,
1381                long messageId, int progress) {
1382            if (messageId != mWaitForLoadMessageId) {
1383                // We are not waiting for this message to load, so exit quickly
1384                return;
1385            }
1386            if (result == null) {
1387                switch (progress) {
1388                    case 0:
1389                        mCallback.onLoadMessageStarted();
1390                        loadBodyContent("file:///android_asset/loading.html");
1391                        break;
1392                    case 100:
1393                        mWaitForLoadMessageId = -1;
1394                        mCallback.onLoadMessageFinished();
1395                        // reload UI and reload everything else too
1396                        // pass false to LoadMessageTask to prevent looping here
1397                        cancelAllTasks();
1398                        mLoadMessageTask = new LoadMessageTask(false);
1399                        mLoadMessageTask.execute();
1400                        break;
1401                    default:
1402                        // do nothing - we don't have a progress bar at this time
1403                        break;
1404                }
1405            } else {
1406                mWaitForLoadMessageId = -1;
1407                String error = mContext.getString(R.string.status_network_error);
1408                mCallback.onLoadMessageError(error);
1409                loadBodyContent("file:///android_asset/empty.html");
1410            }
1411        }
1412
1413        private void loadBodyContent(String uri) {
1414            if (mMessageContentView != null) {
1415                mMessageContentView.loadUrl(uri);
1416            }
1417        }
1418
1419        @Override
1420        public void loadAttachmentCallback(MessagingException result, long accountId,
1421                long messageId, long attachmentId, int progress) {
1422            if (messageId == mMessageId) {
1423                if (result == null) {
1424                    showAttachmentProgress(attachmentId, progress);
1425                    switch (progress) {
1426                        case 100:
1427                            updateAttachmentThumbnail(attachmentId);
1428                            doFinishLoadAttachment(attachmentId);
1429                            break;
1430                        default:
1431                            // do nothing - we don't have a progress bar at this time
1432                            break;
1433                    }
1434                } else {
1435                    AttachmentInfo attachment = findAttachmentInfo(attachmentId);
1436                    attachment.cancelButton.setVisibility(View.GONE);
1437                    attachment.loadButton.setVisibility(View.VISIBLE);
1438                    attachment.progressView.setVisibility(View.INVISIBLE);
1439
1440                    final String error;
1441                    if (result.getCause() instanceof IOException) {
1442                        error = mContext.getString(R.string.status_network_error);
1443                    } else {
1444                        error = mContext.getString(
1445                                R.string.message_view_load_attachment_failed_toast,
1446                                attachment.name);
1447                    }
1448                    mCallback.onLoadMessageError(error);
1449                }
1450            }
1451        }
1452
1453        private void showAttachmentProgress(long attachmentId, int progress) {
1454            AttachmentInfo attachment = findAttachmentInfo(attachmentId);
1455            if (attachment != null) {
1456                ProgressBar bar = attachment.progressView;
1457                if (progress == 0) {
1458                    // When the download starts, we can get rid of the indeterminate bar
1459                    bar.setVisibility(View.VISIBLE);
1460                    bar.setIndeterminate(false);
1461                    // And we're not implementing stop of in-progress downloads
1462                    attachment.cancelButton.setVisibility(View.GONE);
1463                }
1464                bar.setProgress(progress);
1465            }
1466        }
1467    }
1468
1469    /**
1470     * Class to detect update on the current message (e.g. toggle star).  When it gets content
1471     * change notifications, it kicks {@link ReloadMessageTask}.
1472     *
1473     * TODO Use the new Throttle class.
1474     */
1475    private class MessageObserver extends ContentObserver implements Runnable {
1476        private final Throttle mThrottle;
1477        private final ContentResolver mContentResolver;
1478
1479        private boolean mRegistered;
1480
1481        public MessageObserver(Handler handler, Context context) {
1482            super(handler);
1483            mContentResolver = context.getContentResolver();
1484            mThrottle = new Throttle("MessageObserver", this, handler);
1485        }
1486
1487        public void unregister() {
1488            if (!mRegistered) {
1489                return;
1490            }
1491            mThrottle.cancelScheduledCallback();
1492            mContentResolver.unregisterContentObserver(this);
1493            mRegistered = false;
1494        }
1495
1496        public void register(Uri notifyUri) {
1497            unregister();
1498            mContentResolver.registerContentObserver(notifyUri, true, this);
1499            mRegistered = true;
1500        }
1501
1502        @Override
1503        public boolean deliverSelfNotifications() {
1504            return true;
1505        }
1506
1507        @Override
1508        public void onChange(boolean selfChange) {
1509            mThrottle.onEvent();
1510        }
1511
1512        /**
1513         * This method is delay-called by {@link Throttle} on the UI thread.  Need to make
1514         * sure if the fragment is still valid.  (i.e. don't reload if clearContent() has been
1515         * called.)
1516         */
1517        @Override
1518        public void run() {
1519            if (!isMessageSpecified()) {
1520                return;
1521            }
1522            Utility.cancelTaskInterrupt(mReloadMessageTask);
1523            mReloadMessageTask = new ReloadMessageTask();
1524            mReloadMessageTask.execute();
1525        }
1526    }
1527
1528    public boolean isMessageLoadedForTest() {
1529        return mIsMessageLoadedForTest;
1530    }
1531
1532    public void clearIsMessageLoadedForTest() {
1533        mIsMessageLoadedForTest = true;
1534    }
1535}
1536