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