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