| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.email.activity; |
| |
| import android.app.Activity; |
| import android.app.DownloadManager; |
| import android.app.Fragment; |
| import android.app.LoaderManager.LoaderCallbacks; |
| import android.content.ActivityNotFoundException; |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.Loader; |
| import android.content.pm.PackageManager; |
| import android.content.res.Resources; |
| import android.database.ContentObserver; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.media.MediaScannerConnection; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.Environment; |
| import android.os.Handler; |
| import android.provider.ContactsContract; |
| import android.provider.ContactsContract.QuickContact; |
| import android.text.SpannableStringBuilder; |
| import android.text.TextUtils; |
| import android.text.format.DateUtils; |
| import android.util.Log; |
| import android.util.Patterns; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.webkit.WebSettings; |
| import android.webkit.WebView; |
| import android.webkit.WebViewClient; |
| import android.widget.Button; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| import android.widget.ProgressBar; |
| import android.widget.TextView; |
| |
| import com.android.email.AttachmentInfo; |
| import com.android.email.Controller; |
| import com.android.email.ControllerResultUiThreadWrapper; |
| import com.android.email.Email; |
| import com.android.email.Preferences; |
| import com.android.email.R; |
| import com.android.email.Throttle; |
| import com.android.email.mail.internet.EmailHtmlUtil; |
| import com.android.email.service.AttachmentDownloadService; |
| import com.android.emailcommon.Logging; |
| import com.android.emailcommon.mail.Address; |
| import com.android.emailcommon.mail.MessagingException; |
| import com.android.emailcommon.provider.Account; |
| import com.android.emailcommon.provider.EmailContent.Attachment; |
| import com.android.emailcommon.provider.EmailContent.Body; |
| import com.android.emailcommon.provider.EmailContent.Message; |
| import com.android.emailcommon.provider.Mailbox; |
| import com.android.emailcommon.utility.AttachmentUtilities; |
| import com.android.emailcommon.utility.EmailAsyncTask; |
| import com.android.emailcommon.utility.Utility; |
| import com.google.common.collect.Maps; |
| |
| import org.apache.commons.io.IOUtils; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.util.Formatter; |
| import java.util.Map; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| // TODO Better handling of config changes. |
| // - Retain the content; don't kick 3 async tasks every time |
| |
| /** |
| * Base class for {@link MessageViewFragment} and {@link MessageFileViewFragment}. |
| */ |
| public abstract class MessageViewFragmentBase extends Fragment implements View.OnClickListener { |
| private static final String BUNDLE_KEY_CURRENT_TAB = "MessageViewFragmentBase.currentTab"; |
| private static final String BUNDLE_KEY_PICTURE_LOADED = "MessageViewFragmentBase.pictureLoaded"; |
| private static final int PHOTO_LOADER_ID = 1; |
| protected Context mContext; |
| |
| // Regex that matches start of img tag. '<(?i)img\s+'. |
| private static final Pattern IMG_TAG_START_REGEX = Pattern.compile("<(?i)img\\s+"); |
| // Regex that matches Web URL protocol part as case insensitive. |
| private static final Pattern WEB_URL_PROTOCOL = Pattern.compile("(?i)http|https://"); |
| |
| private static int PREVIEW_ICON_WIDTH = 62; |
| private static int PREVIEW_ICON_HEIGHT = 62; |
| |
| // The different levels of zoom: read from the Preferences. |
| private static String[] sZoomSizes = null; |
| |
| private TextView mSubjectView; |
| private TextView mFromNameView; |
| private TextView mFromAddressView; |
| private TextView mDateTimeView; |
| private TextView mAddressesView; |
| private WebView mMessageContentView; |
| private LinearLayout mAttachments; |
| private View mTabSection; |
| private ImageView mFromBadge; |
| private ImageView mSenderPresenceView; |
| private View mMainView; |
| private View mLoadingProgress; |
| private View mDetailsCollapsed; |
| private View mDetailsExpanded; |
| private boolean mDetailsFilled; |
| |
| private TextView mMessageTab; |
| private TextView mAttachmentTab; |
| private TextView mInviteTab; |
| // It is not really a tab, but looks like one of them. |
| private TextView mShowPicturesTab; |
| private View mAlwaysShowPicturesButton; |
| |
| private View mAttachmentsScroll; |
| private View mInviteScroll; |
| |
| private long mAccountId = Account.NO_ACCOUNT; |
| private long mMessageId = Message.NO_MESSAGE; |
| private Message mMessage; |
| |
| private Controller mController; |
| private ControllerResultUiThreadWrapper<ControllerResults> mControllerCallback; |
| |
| // contains the HTML body. Is used by LoadAttachmentTask to display inline images. |
| // is null most of the time, is used transiently to pass info to LoadAttachementTask |
| private String mHtmlTextRaw; |
| |
| // contains the HTML content as set in WebView. |
| private String mHtmlTextWebView; |
| |
| private boolean mIsMessageLoadedForTest; |
| |
| private MessageObserver mMessageObserver; |
| |
| private static final int CONTACT_STATUS_STATE_UNLOADED = 0; |
| private static final int CONTACT_STATUS_STATE_UNLOADED_TRIGGERED = 1; |
| private static final int CONTACT_STATUS_STATE_LOADED = 2; |
| |
| private int mContactStatusState; |
| private Uri mQuickContactLookupUri; |
| |
| /** Flag for {@link #mTabFlags}: Message has attachment(s) */ |
| protected static final int TAB_FLAGS_HAS_ATTACHMENT = 1; |
| |
| /** |
| * Flag for {@link #mTabFlags}: Message contains invite. This flag is only set by |
| * {@link MessageViewFragment}. |
| */ |
| protected static final int TAB_FLAGS_HAS_INVITE = 2; |
| |
| /** Flag for {@link #mTabFlags}: Message contains pictures */ |
| protected static final int TAB_FLAGS_HAS_PICTURES = 4; |
| |
| /** Flag for {@link #mTabFlags}: "Show pictures" has already been pressed */ |
| protected static final int TAB_FLAGS_PICTURE_LOADED = 8; |
| |
| /** |
| * Flags to control the tabs. |
| * @see #updateTabs(int) |
| */ |
| private int mTabFlags; |
| |
| /** # of attachments in the current message */ |
| private int mAttachmentCount; |
| |
| // Use (random) large values, to avoid confusion with TAB_FLAGS_* |
| protected static final int TAB_MESSAGE = 101; |
| protected static final int TAB_INVITE = 102; |
| protected static final int TAB_ATTACHMENT = 103; |
| private static final int TAB_NONE = 0; |
| |
| /** Current tab */ |
| private int mCurrentTab = TAB_NONE; |
| /** |
| * Tab that was selected in the previous activity instance. |
| * Used to restore the current tab after screen rotation. |
| */ |
| private int mRestoredTab = TAB_NONE; |
| |
| private boolean mRestoredPictureLoaded; |
| |
| private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker(); |
| |
| public interface Callback { |
| /** |
| * Called when a link in a message is clicked. |
| * |
| * @param url link url that's clicked. |
| * @return true if handled, false otherwise. |
| */ |
| public boolean onUrlInMessageClicked(String url); |
| |
| /** |
| * Called when the message specified doesn't exist, or is deleted/moved. |
| */ |
| public void onMessageNotExists(); |
| |
| /** Called when it starts loading a message. */ |
| public void onLoadMessageStarted(); |
| |
| /** Called when it successfully finishes loading a message. */ |
| public void onLoadMessageFinished(); |
| |
| /** Called when an error occurred during loading a message. */ |
| public void onLoadMessageError(String errorMessage); |
| } |
| |
| public static class EmptyCallback implements Callback { |
| public static final Callback INSTANCE = new EmptyCallback(); |
| @Override public void onLoadMessageError(String errorMessage) {} |
| @Override public void onLoadMessageFinished() {} |
| @Override public void onLoadMessageStarted() {} |
| @Override public void onMessageNotExists() {} |
| @Override |
| public boolean onUrlInMessageClicked(String url) { |
| return false; |
| } |
| } |
| |
| private Callback mCallback = EmptyCallback.INSTANCE; |
| |
| @Override |
| public void onAttach(Activity activity) { |
| if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { |
| Log.d(Logging.LOG_TAG, this + " onAttach"); |
| } |
| super.onAttach(activity); |
| } |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { |
| Log.d(Logging.LOG_TAG, this + " onCreate"); |
| } |
| super.onCreate(savedInstanceState); |
| |
| mContext = getActivity().getApplicationContext(); |
| |
| // Initialize components, but don't "start" them. Registering the controller callbacks |
| // and starting MessageObserver, should be done in onActivityCreated or later and be stopped |
| // in onDestroyView to prevent from getting callbacks when the fragment is in the back |
| // stack, but they'll start again when it's back from the back stack. |
| mController = Controller.getInstance(mContext); |
| mControllerCallback = new ControllerResultUiThreadWrapper<ControllerResults>( |
| new Handler(), new ControllerResults()); |
| mMessageObserver = new MessageObserver(new Handler(), mContext); |
| |
| if (savedInstanceState != null) { |
| restoreInstanceState(savedInstanceState); |
| } |
| } |
| |
| @Override |
| public View onCreateView( |
| LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { |
| if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { |
| Log.d(Logging.LOG_TAG, this + " onCreateView"); |
| } |
| final View view = inflater.inflate(R.layout.message_view_fragment, container, false); |
| |
| cleanupDetachedViews(); |
| |
| mSubjectView = (TextView) UiUtilities.getView(view, R.id.subject); |
| mFromNameView = (TextView) UiUtilities.getView(view, R.id.from_name); |
| mFromAddressView = (TextView) UiUtilities.getView(view, R.id.from_address); |
| mAddressesView = (TextView) UiUtilities.getView(view, R.id.addresses); |
| mDateTimeView = (TextView) UiUtilities.getView(view, R.id.datetime); |
| mMessageContentView = (WebView) UiUtilities.getView(view, R.id.message_content); |
| mAttachments = (LinearLayout) UiUtilities.getView(view, R.id.attachments); |
| mTabSection = UiUtilities.getView(view, R.id.message_tabs_section); |
| mFromBadge = (ImageView) UiUtilities.getView(view, R.id.badge); |
| mSenderPresenceView = (ImageView) UiUtilities.getView(view, R.id.presence); |
| mMainView = UiUtilities.getView(view, R.id.main_panel); |
| mLoadingProgress = UiUtilities.getView(view, R.id.loading_progress); |
| mDetailsCollapsed = UiUtilities.getView(view, R.id.sub_header_contents_collapsed); |
| mDetailsExpanded = UiUtilities.getView(view, R.id.sub_header_contents_expanded); |
| |
| mFromNameView.setOnClickListener(this); |
| mFromAddressView.setOnClickListener(this); |
| mFromBadge.setOnClickListener(this); |
| mSenderPresenceView.setOnClickListener(this); |
| |
| mMessageTab = UiUtilities.getView(view, R.id.show_message); |
| mAttachmentTab = UiUtilities.getView(view, R.id.show_attachments); |
| mShowPicturesTab = UiUtilities.getView(view, R.id.show_pictures); |
| mAlwaysShowPicturesButton = UiUtilities.getView(view, R.id.always_show_pictures_button); |
| // Invite is only used in MessageViewFragment, but visibility is controlled here. |
| mInviteTab = UiUtilities.getView(view, R.id.show_invite); |
| |
| mMessageTab.setOnClickListener(this); |
| mAttachmentTab.setOnClickListener(this); |
| mShowPicturesTab.setOnClickListener(this); |
| mAlwaysShowPicturesButton.setOnClickListener(this); |
| mInviteTab.setOnClickListener(this); |
| mDetailsCollapsed.setOnClickListener(this); |
| mDetailsExpanded.setOnClickListener(this); |
| |
| mAttachmentsScroll = UiUtilities.getView(view, R.id.attachments_scroll); |
| mInviteScroll = UiUtilities.getView(view, R.id.invite_scroll); |
| |
| WebSettings webSettings = mMessageContentView.getSettings(); |
| boolean supportMultiTouch = mContext.getPackageManager() |
| .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH); |
| webSettings.setDisplayZoomControls(!supportMultiTouch); |
| webSettings.setSupportZoom(true); |
| webSettings.setBuiltInZoomControls(true); |
| mMessageContentView.setWebViewClient(new CustomWebViewClient()); |
| return view; |
| } |
| |
| @Override |
| public void onActivityCreated(Bundle savedInstanceState) { |
| if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { |
| Log.d(Logging.LOG_TAG, this + " onActivityCreated"); |
| } |
| super.onActivityCreated(savedInstanceState); |
| mController.addResultCallback(mControllerCallback); |
| |
| resetView(); |
| new LoadMessageTask(true).executeParallel(); |
| |
| UiUtilities.installFragment(this); |
| } |
| |
| @Override |
| public void onStart() { |
| if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { |
| Log.d(Logging.LOG_TAG, this + " onStart"); |
| } |
| super.onStart(); |
| } |
| |
| @Override |
| public void onResume() { |
| if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { |
| Log.d(Logging.LOG_TAG, this + " onResume"); |
| } |
| super.onResume(); |
| |
| // We might have comes back from other full-screen activities. If so, we need to update |
| // the attachment tab as system settings may have been updated that affect which |
| // options are available to the user. |
| updateAttachmentTab(); |
| } |
| |
| @Override |
| public void onPause() { |
| if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { |
| Log.d(Logging.LOG_TAG, this + " onPause"); |
| } |
| super.onPause(); |
| } |
| |
| @Override |
| public void onStop() { |
| if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { |
| Log.d(Logging.LOG_TAG, this + " onStop"); |
| } |
| super.onStop(); |
| } |
| |
| @Override |
| public void onDestroyView() { |
| if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { |
| Log.d(Logging.LOG_TAG, this + " onDestroyView"); |
| } |
| UiUtilities.uninstallFragment(this); |
| mController.removeResultCallback(mControllerCallback); |
| cancelAllTasks(); |
| |
| // We should clean up the Webview here, but it can't release resources until it is |
| // actually removed from the view tree. |
| |
| super.onDestroyView(); |
| } |
| |
| private void cleanupDetachedViews() { |
| // WebView cleanup must be done after it leaves the rendering tree, according to |
| // its contract |
| if (mMessageContentView != null) { |
| mMessageContentView.destroy(); |
| mMessageContentView = null; |
| } |
| } |
| |
| @Override |
| public void onDestroy() { |
| if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { |
| Log.d(Logging.LOG_TAG, this + " onDestroy"); |
| } |
| |
| cleanupDetachedViews(); |
| super.onDestroy(); |
| } |
| |
| @Override |
| public void onDetach() { |
| if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { |
| Log.d(Logging.LOG_TAG, this + " onDetach"); |
| } |
| super.onDetach(); |
| } |
| |
| @Override |
| public void onSaveInstanceState(Bundle outState) { |
| if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { |
| Log.d(Logging.LOG_TAG, this + " onSaveInstanceState"); |
| } |
| super.onSaveInstanceState(outState); |
| outState.putInt(BUNDLE_KEY_CURRENT_TAB, mCurrentTab); |
| outState.putBoolean(BUNDLE_KEY_PICTURE_LOADED, (mTabFlags & TAB_FLAGS_PICTURE_LOADED) != 0); |
| } |
| |
| private void restoreInstanceState(Bundle state) { |
| if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { |
| Log.d(Logging.LOG_TAG, this + " restoreInstanceState"); |
| } |
| // At this point (in onCreate) no tabs are visible (because we don't know if the message has |
| // an attachment or invite before loading it). We just remember the tab here. |
| // We'll make it current when the tab first becomes visible in updateTabs(). |
| mRestoredTab = state.getInt(BUNDLE_KEY_CURRENT_TAB); |
| mRestoredPictureLoaded = state.getBoolean(BUNDLE_KEY_PICTURE_LOADED); |
| } |
| |
| public void setCallback(Callback callback) { |
| mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback; |
| } |
| |
| private void cancelAllTasks() { |
| mMessageObserver.unregister(); |
| mTaskTracker.cancellAllInterrupt(); |
| } |
| |
| protected final Controller getController() { |
| return mController; |
| } |
| |
| protected final Callback getCallback() { |
| return mCallback; |
| } |
| |
| public final Message getMessage() { |
| return mMessage; |
| } |
| |
| protected final boolean isMessageOpen() { |
| return mMessage != null; |
| } |
| |
| /** |
| * Returns the account id of the current message, or -1 if unknown (message not open yet, or |
| * viewing an EML message). |
| */ |
| public long getAccountId() { |
| return mAccountId; |
| } |
| |
| /** |
| * Show/hide the content. We hide all the content (except for the bottom buttons) when loading, |
| * to avoid flicker. |
| */ |
| private void showContent(boolean showContent, boolean showProgressWhenHidden) { |
| makeVisible(mMainView, showContent); |
| makeVisible(mLoadingProgress, !showContent && showProgressWhenHidden); |
| } |
| |
| // TODO: clean this up - most of this is not needed since the WebView and Fragment is not |
| // reused for multiple messages. |
| protected void resetView() { |
| showContent(false, false); |
| updateTabs(0); |
| setCurrentTab(TAB_MESSAGE); |
| if (mMessageContentView != null) { |
| blockNetworkLoads(true); |
| mMessageContentView.scrollTo(0, 0); |
| |
| // Dynamic configuration of WebView |
| final WebSettings settings = mMessageContentView.getSettings(); |
| settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL); |
| mMessageContentView.setInitialScale(getWebViewZoom()); |
| } |
| mAttachmentsScroll.scrollTo(0, 0); |
| mInviteScroll.scrollTo(0, 0); |
| mAttachments.removeAllViews(); |
| mAttachments.setVisibility(View.GONE); |
| initContactStatusViews(); |
| } |
| |
| /** |
| * Returns the zoom scale (in percent) which is a combination of the user setting |
| * (tiny, small, normal, large, huge) and the device density. The intention |
| * is for the text to be physically equal in size over different density |
| * screens. |
| */ |
| private int getWebViewZoom() { |
| float density = mContext.getResources().getDisplayMetrics().density; |
| int zoom = Preferences.getPreferences(mContext).getTextZoom(); |
| if (sZoomSizes == null) { |
| sZoomSizes = mContext.getResources() |
| .getStringArray(R.array.general_preference_text_zoom_size); |
| } |
| return (int)(Float.valueOf(sZoomSizes[zoom]) * density * 100); |
| } |
| |
| private void initContactStatusViews() { |
| mContactStatusState = CONTACT_STATUS_STATE_UNLOADED; |
| mQuickContactLookupUri = null; |
| showDefaultQuickContactBadgeImage(); |
| } |
| |
| private void showDefaultQuickContactBadgeImage() { |
| mFromBadge.setImageResource(R.drawable.ic_contact_picture); |
| } |
| |
| protected final void addTabFlags(int tabFlags) { |
| updateTabs(mTabFlags | tabFlags); |
| } |
| |
| private final void clearTabFlags(int tabFlags) { |
| updateTabs(mTabFlags & ~tabFlags); |
| } |
| |
| private void setAttachmentCount(int count) { |
| mAttachmentCount = count; |
| if (mAttachmentCount > 0) { |
| addTabFlags(TAB_FLAGS_HAS_ATTACHMENT); |
| } else { |
| clearTabFlags(TAB_FLAGS_HAS_ATTACHMENT); |
| } |
| } |
| |
| private static void makeVisible(View v, boolean visible) { |
| final int visibility = visible ? View.VISIBLE : View.GONE; |
| if ((v != null) && (v.getVisibility() != visibility)) { |
| v.setVisibility(visibility); |
| } |
| } |
| |
| private static boolean isVisible(View v) { |
| return (v != null) && (v.getVisibility() == View.VISIBLE); |
| } |
| |
| /** |
| * Update the visual of the tabs. (visibility, text, etc) |
| */ |
| private void updateTabs(int tabFlags) { |
| mTabFlags = tabFlags; |
| |
| if (getView() == null) { |
| return; |
| } |
| |
| boolean messageTabVisible = (tabFlags & (TAB_FLAGS_HAS_INVITE | TAB_FLAGS_HAS_ATTACHMENT)) |
| != 0; |
| makeVisible(mMessageTab, messageTabVisible); |
| makeVisible(mInviteTab, (tabFlags & TAB_FLAGS_HAS_INVITE) != 0); |
| makeVisible(mAttachmentTab, (tabFlags & TAB_FLAGS_HAS_ATTACHMENT) != 0); |
| |
| final boolean hasPictures = (tabFlags & TAB_FLAGS_HAS_PICTURES) != 0; |
| final boolean pictureLoaded = (tabFlags & TAB_FLAGS_PICTURE_LOADED) != 0; |
| makeVisible(mShowPicturesTab, hasPictures && !pictureLoaded); |
| |
| mAttachmentTab.setText(mContext.getResources().getQuantityString( |
| R.plurals.message_view_show_attachments_action, |
| mAttachmentCount, mAttachmentCount)); |
| |
| // Hide the entire section if no tabs are visible. |
| makeVisible(mTabSection, isVisible(mMessageTab) || isVisible(mInviteTab) |
| || isVisible(mAttachmentTab) || isVisible(mShowPicturesTab) |
| || isVisible(mAlwaysShowPicturesButton)); |
| |
| // Restore previously selected tab after rotation |
| if (mRestoredTab != TAB_NONE && isVisible(getTabViewForFlag(mRestoredTab))) { |
| setCurrentTab(mRestoredTab); |
| mRestoredTab = TAB_NONE; |
| } |
| } |
| |
| /** |
| * Set the current tab. |
| * |
| * @param tab any of {@link #TAB_MESSAGE}, {@link #TAB_ATTACHMENT} or {@link #TAB_INVITE}. |
| */ |
| private void setCurrentTab(int tab) { |
| mCurrentTab = tab; |
| |
| // Hide & unselect all tabs |
| makeVisible(getTabContentViewForFlag(TAB_MESSAGE), false); |
| makeVisible(getTabContentViewForFlag(TAB_ATTACHMENT), false); |
| makeVisible(getTabContentViewForFlag(TAB_INVITE), false); |
| getTabViewForFlag(TAB_MESSAGE).setSelected(false); |
| getTabViewForFlag(TAB_ATTACHMENT).setSelected(false); |
| getTabViewForFlag(TAB_INVITE).setSelected(false); |
| |
| makeVisible(getTabContentViewForFlag(mCurrentTab), true); |
| getTabViewForFlag(mCurrentTab).setSelected(true); |
| } |
| |
| private View getTabViewForFlag(int tabFlag) { |
| switch (tabFlag) { |
| case TAB_MESSAGE: |
| return mMessageTab; |
| case TAB_ATTACHMENT: |
| return mAttachmentTab; |
| case TAB_INVITE: |
| return mInviteTab; |
| } |
| throw new IllegalArgumentException(); |
| } |
| |
| private View getTabContentViewForFlag(int tabFlag) { |
| switch (tabFlag) { |
| case TAB_MESSAGE: |
| return mMessageContentView; |
| case TAB_ATTACHMENT: |
| return mAttachmentsScroll; |
| case TAB_INVITE: |
| return mInviteScroll; |
| } |
| throw new IllegalArgumentException(); |
| } |
| |
| private void blockNetworkLoads(boolean block) { |
| if (mMessageContentView != null) { |
| mMessageContentView.getSettings().setBlockNetworkLoads(block); |
| } |
| } |
| |
| private void setMessageHtml(String html) { |
| if (html == null) { |
| html = ""; |
| } |
| if (mMessageContentView != null) { |
| mMessageContentView.loadDataWithBaseURL("email://", html, "text/html", "utf-8", null); |
| } |
| } |
| |
| /** |
| * Handle clicks on sender, which shows {@link QuickContact} or prompts to add |
| * the sender as a contact. |
| */ |
| private void onClickSender() { |
| if (!isMessageOpen()) return; |
| final Address senderEmail = Address.unpackFirst(mMessage.mFrom); |
| if (senderEmail == null) return; |
| |
| if (mContactStatusState == CONTACT_STATUS_STATE_UNLOADED) { |
| // Status not loaded yet. |
| mContactStatusState = CONTACT_STATUS_STATE_UNLOADED_TRIGGERED; |
| return; |
| } |
| if (mContactStatusState == CONTACT_STATUS_STATE_UNLOADED_TRIGGERED) { |
| return; // Already clicked, and waiting for the data. |
| } |
| |
| if (mQuickContactLookupUri != null) { |
| QuickContact.showQuickContact(mContext, mFromBadge, mQuickContactLookupUri, |
| QuickContact.MODE_MEDIUM, null); |
| } else { |
| // No matching contact, ask user to create one |
| final Uri mailUri = Uri.fromParts("mailto", senderEmail.getAddress(), null); |
| final Intent intent = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT, |
| mailUri); |
| |
| // Only provide personal name hint if we have one |
| final String senderPersonal = senderEmail.getPersonal(); |
| if (!TextUtils.isEmpty(senderPersonal)) { |
| intent.putExtra(ContactsContract.Intents.Insert.NAME, senderPersonal); |
| } |
| intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); |
| |
| startActivity(intent); |
| } |
| } |
| |
| private static class ContactStatusLoaderCallbacks |
| implements LoaderCallbacks<ContactStatusLoader.Result> { |
| private static final String BUNDLE_EMAIL_ADDRESS = "email"; |
| private final MessageViewFragmentBase mFragment; |
| |
| public ContactStatusLoaderCallbacks(MessageViewFragmentBase fragment) { |
| mFragment = fragment; |
| } |
| |
| public static Bundle createArguments(String emailAddress) { |
| Bundle b = new Bundle(); |
| b.putString(BUNDLE_EMAIL_ADDRESS, emailAddress); |
| return b; |
| } |
| |
| @Override |
| public Loader<ContactStatusLoader.Result> onCreateLoader(int id, Bundle args) { |
| return new ContactStatusLoader(mFragment.mContext, |
| args.getString(BUNDLE_EMAIL_ADDRESS)); |
| } |
| |
| @Override |
| public void onLoadFinished(Loader<ContactStatusLoader.Result> loader, |
| ContactStatusLoader.Result result) { |
| boolean triggered = |
| (mFragment.mContactStatusState == CONTACT_STATUS_STATE_UNLOADED_TRIGGERED); |
| mFragment.mContactStatusState = CONTACT_STATUS_STATE_LOADED; |
| mFragment.mQuickContactLookupUri = result.mLookupUri; |
| |
| if (result.isUnknown()) { |
| mFragment.mSenderPresenceView.setVisibility(View.GONE); |
| } else { |
| mFragment.mSenderPresenceView.setVisibility(View.VISIBLE); |
| mFragment.mSenderPresenceView.setImageResource(result.mPresenceResId); |
| } |
| if (result.mPhoto != null) { // photo will be null if unknown. |
| mFragment.mFromBadge.setImageBitmap(result.mPhoto); |
| } |
| if (triggered) { |
| mFragment.onClickSender(); |
| } |
| } |
| |
| @Override |
| public void onLoaderReset(Loader<ContactStatusLoader.Result> loader) { |
| } |
| } |
| |
| private void onSaveAttachment(MessageViewAttachmentInfo info) { |
| if (!Utility.isExternalStorageMounted()) { |
| /* |
| * Abort early if there's no place to save the attachment. We don't want to spend |
| * the time downloading it and then abort. |
| */ |
| Utility.showToast(getActivity(), R.string.message_view_status_attachment_not_saved); |
| return; |
| } |
| |
| if (info.isFileSaved()) { |
| // Nothing to do - we have the file saved. |
| return; |
| } |
| |
| File savedFile = performAttachmentSave(info); |
| if (savedFile != null) { |
| Utility.showToast(getActivity(), String.format( |
| mContext.getString(R.string.message_view_status_attachment_saved), |
| savedFile.getName())); |
| } else { |
| Utility.showToast(getActivity(), R.string.message_view_status_attachment_not_saved); |
| } |
| } |
| |
| private File performAttachmentSave(MessageViewAttachmentInfo info) { |
| Attachment attachment = Attachment.restoreAttachmentWithId(mContext, info.mId); |
| Uri attachmentUri = AttachmentUtilities.getAttachmentUri(mAccountId, attachment.mId); |
| |
| try { |
| File downloads = Environment.getExternalStoragePublicDirectory( |
| Environment.DIRECTORY_DOWNLOADS); |
| downloads.mkdirs(); |
| File file = Utility.createUniqueFile(downloads, attachment.mFileName); |
| Uri contentUri = AttachmentUtilities.resolveAttachmentIdToContentUri( |
| mContext.getContentResolver(), attachmentUri); |
| InputStream in = mContext.getContentResolver().openInputStream(contentUri); |
| OutputStream out = new FileOutputStream(file); |
| IOUtils.copy(in, out); |
| out.flush(); |
| out.close(); |
| in.close(); |
| |
| String absolutePath = file.getAbsolutePath(); |
| |
| // Although the download manager can scan media files, scanning only happens after the |
| // user clicks on the item in the Downloads app. So, we run the attachment through |
| // the media scanner ourselves so it gets added to gallery / music immediately. |
| MediaScannerConnection.scanFile(mContext, new String[] {absolutePath}, |
| null, null); |
| |
| DownloadManager dm = |
| (DownloadManager) getActivity().getSystemService(Context.DOWNLOAD_SERVICE); |
| dm.addCompletedDownload(info.mName, info.mName, |
| false /* do not use media scanner */, |
| info.mContentType, absolutePath, info.mSize, |
| true /* show notification */); |
| |
| // Cache the stored file information. |
| info.setSavedPath(absolutePath); |
| |
| // Update our buttons. |
| updateAttachmentButtons(info); |
| |
| return file; |
| |
| } catch (IOException ioe) { |
| // Ignore. Callers will handle it from the return code. |
| } |
| |
| return null; |
| } |
| |
| private void onOpenAttachment(MessageViewAttachmentInfo info) { |
| if (info.mAllowInstall) { |
| // The package installer is unable to install files from a content URI; it must be |
| // given a file path. Therefore, we need to save it first in order to proceed |
| if (!info.mAllowSave || !Utility.isExternalStorageMounted()) { |
| Utility.showToast(getActivity(), R.string.message_view_status_attachment_not_saved); |
| return; |
| } |
| |
| if (!info.isFileSaved()) { |
| if (performAttachmentSave(info) == null) { |
| // Saving failed for some reason - bail. |
| Utility.showToast( |
| getActivity(), R.string.message_view_status_attachment_not_saved); |
| return; |
| } |
| } |
| } |
| try { |
| Intent intent = info.getAttachmentIntent(mContext, mAccountId); |
| startActivity(intent); |
| } catch (ActivityNotFoundException e) { |
| Utility.showToast(getActivity(), R.string.message_view_display_attachment_toast); |
| } |
| } |
| |
| private void onInfoAttachment(final MessageViewAttachmentInfo attachment) { |
| AttachmentInfoDialog dialog = |
| AttachmentInfoDialog.newInstance(getActivity(), attachment.mDenyFlags); |
| dialog.show(getActivity().getFragmentManager(), null); |
| } |
| |
| private void onLoadAttachment(final MessageViewAttachmentInfo attachment) { |
| attachment.loadButton.setVisibility(View.GONE); |
| // If there's nothing in the download queue, we'll probably start right away so wait a |
| // second before showing the cancel button |
| if (AttachmentDownloadService.getQueueSize() == 0) { |
| // Set to invisible; if the button is still in this state one second from now, we'll |
| // assume the download won't start right away, and we make the cancel button visible |
| attachment.cancelButton.setVisibility(View.GONE); |
| // Create the timed task that will change the button state |
| new EmailAsyncTask<Void, Void, Void>(mTaskTracker) { |
| @Override |
| protected Void doInBackground(Void... params) { |
| try { |
| Thread.sleep(1000L); |
| } catch (InterruptedException e) { } |
| return null; |
| } |
| @Override |
| protected void onSuccess(Void result) { |
| // If the timeout completes and the attachment has not loaded, show cancel |
| if (!attachment.loaded) { |
| attachment.cancelButton.setVisibility(View.VISIBLE); |
| } |
| } |
| }.executeParallel(); |
| } else { |
| attachment.cancelButton.setVisibility(View.VISIBLE); |
| } |
| attachment.showProgressIndeterminate(); |
| mController.loadAttachment(attachment.mId, mMessageId, mAccountId); |
| } |
| |
| private void onCancelAttachment(MessageViewAttachmentInfo attachment) { |
| // Don't change button states if we couldn't cancel the download |
| if (AttachmentDownloadService.cancelQueuedAttachment(attachment.mId)) { |
| attachment.loadButton.setVisibility(View.VISIBLE); |
| attachment.cancelButton.setVisibility(View.GONE); |
| attachment.hideProgress(); |
| } |
| } |
| |
| /** |
| * Called by ControllerResults. Show the "View" and "Save" buttons; hide "Load" and "Stop" |
| * |
| * @param attachmentId the attachment that was just downloaded |
| */ |
| private void doFinishLoadAttachment(long attachmentId) { |
| MessageViewAttachmentInfo info = findAttachmentInfo(attachmentId); |
| if (info != null) { |
| info.loaded = true; |
| updateAttachmentButtons(info); |
| } |
| } |
| |
| private void showPicturesInHtml() { |
| boolean picturesAlreadyLoaded = (mTabFlags & TAB_FLAGS_PICTURE_LOADED) != 0; |
| if ((mMessageContentView != null) && !picturesAlreadyLoaded) { |
| blockNetworkLoads(false); |
| // TODO: why is this calling setMessageHtml just because the images can load now? |
| setMessageHtml(mHtmlTextWebView); |
| |
| // Prompt the user to always show images from this sender. |
| makeVisible(UiUtilities.getView(getView(), R.id.always_show_pictures_button), true); |
| |
| addTabFlags(TAB_FLAGS_PICTURE_LOADED); |
| } |
| } |
| |
| private void showDetails() { |
| if (!isMessageOpen()) { |
| return; |
| } |
| |
| if (!mDetailsFilled) { |
| String date = formatDate(mMessage.mTimeStamp, true); |
| final String SEPARATOR = "\n"; |
| String to = Address.toString(Address.unpack(mMessage.mTo), SEPARATOR); |
| String cc = Address.toString(Address.unpack(mMessage.mCc), SEPARATOR); |
| String bcc = Address.toString(Address.unpack(mMessage.mBcc), SEPARATOR); |
| setDetailsRow(mDetailsExpanded, date, R.id.date, R.id.date_row); |
| setDetailsRow(mDetailsExpanded, to, R.id.to, R.id.to_row); |
| setDetailsRow(mDetailsExpanded, cc, R.id.cc, R.id.cc_row); |
| setDetailsRow(mDetailsExpanded, bcc, R.id.bcc, R.id.bcc_row); |
| mDetailsFilled = true; |
| } |
| |
| mDetailsCollapsed.setVisibility(View.GONE); |
| mDetailsExpanded.setVisibility(View.VISIBLE); |
| } |
| |
| private void hideDetails() { |
| mDetailsCollapsed.setVisibility(View.VISIBLE); |
| mDetailsExpanded.setVisibility(View.GONE); |
| } |
| |
| private static void setDetailsRow(View root, String text, int textViewId, int rowViewId) { |
| if (TextUtils.isEmpty(text)) { |
| root.findViewById(rowViewId).setVisibility(View.GONE); |
| return; |
| } |
| ((TextView) UiUtilities.getView(root, textViewId)).setText(text); |
| } |
| |
| |
| @Override |
| public void onClick(View view) { |
| if (!isMessageOpen()) { |
| return; // Ignore. |
| } |
| switch (view.getId()) { |
| case R.id.badge: |
| onClickSender(); |
| break; |
| case R.id.load: |
| onLoadAttachment((MessageViewAttachmentInfo) view.getTag()); |
| break; |
| case R.id.info: |
| onInfoAttachment((MessageViewAttachmentInfo) view.getTag()); |
| break; |
| case R.id.save: |
| onSaveAttachment((MessageViewAttachmentInfo) view.getTag()); |
| break; |
| case R.id.open: |
| onOpenAttachment((MessageViewAttachmentInfo) view.getTag()); |
| break; |
| case R.id.cancel: |
| onCancelAttachment((MessageViewAttachmentInfo) view.getTag()); |
| break; |
| case R.id.show_message: |
| setCurrentTab(TAB_MESSAGE); |
| break; |
| case R.id.show_invite: |
| setCurrentTab(TAB_INVITE); |
| break; |
| case R.id.show_attachments: |
| setCurrentTab(TAB_ATTACHMENT); |
| break; |
| case R.id.show_pictures: |
| showPicturesInHtml(); |
| break; |
| case R.id.always_show_pictures_button: |
| setShowImagesForSender(); |
| break; |
| case R.id.sub_header_contents_collapsed: |
| showDetails(); |
| break; |
| case R.id.sub_header_contents_expanded: |
| hideDetails(); |
| break; |
| } |
| } |
| |
| /** |
| * Start loading contact photo and presence. |
| */ |
| private void queryContactStatus() { |
| if (!isMessageOpen()) return; |
| initContactStatusViews(); // Initialize the state, just in case. |
| |
| // Find the sender email address, and start presence check. |
| Address sender = Address.unpackFirst(mMessage.mFrom); |
| if (sender != null) { |
| String email = sender.getAddress(); |
| if (email != null) { |
| getLoaderManager().restartLoader(PHOTO_LOADER_ID, |
| ContactStatusLoaderCallbacks.createArguments(email), |
| new ContactStatusLoaderCallbacks(this)); |
| } |
| } |
| } |
| |
| /** |
| * Called by {@link LoadMessageTask} and {@link ReloadMessageTask} to load a message in a |
| * subclass specific way. |
| * |
| * NOTE This method is called on a worker thread! Implementations must properly synchronize |
| * when accessing members. |
| * |
| * @param activity the parent activity. Subclass use it as a context, and to show a toast. |
| */ |
| protected abstract Message openMessageSync(Activity activity); |
| |
| /** |
| * Called in a background thread to reload a new copy of the Message in case something has |
| * changed. |
| */ |
| protected Message reloadMessageSync(Activity activity) { |
| return openMessageSync(activity); |
| } |
| |
| /** |
| * Async task for loading a single message outside of the UI thread |
| */ |
| private class LoadMessageTask extends EmailAsyncTask<Void, Void, Message> { |
| |
| private final boolean mOkToFetch; |
| private Mailbox mMailbox; |
| |
| /** |
| * Special constructor to cache some local info |
| */ |
| public LoadMessageTask(boolean okToFetch) { |
| super(mTaskTracker); |
| mOkToFetch = okToFetch; |
| } |
| |
| @Override |
| protected Message doInBackground(Void... params) { |
| Activity activity = getActivity(); |
| Message message = null; |
| if (activity != null) { |
| message = openMessageSync(activity); |
| } |
| if (message != null) { |
| mMailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey); |
| if (mMailbox == null) { |
| message = null; // mailbox removed?? |
| } |
| } |
| return message; |
| } |
| |
| @Override |
| protected void onSuccess(Message message) { |
| if (message == null) { |
| resetView(); |
| mCallback.onMessageNotExists(); |
| return; |
| } |
| mMessageId = message.mId; |
| |
| reloadUiFromMessage(message, mOkToFetch); |
| queryContactStatus(); |
| onMessageShown(mMessageId, mMailbox); |
| RecentMailboxManager.getInstance(mContext).touch(mAccountId, message.mMailboxKey); |
| } |
| } |
| |
| /** |
| * Kicked by {@link MessageObserver}. Reload the message and update the views. |
| */ |
| private class ReloadMessageTask extends EmailAsyncTask<Void, Void, Message> { |
| public ReloadMessageTask() { |
| super(mTaskTracker); |
| } |
| |
| @Override |
| protected Message doInBackground(Void... params) { |
| Activity activity = getActivity(); |
| if (activity == null) { |
| return null; |
| } else { |
| return reloadMessageSync(activity); |
| } |
| } |
| |
| @Override |
| protected void onSuccess(Message message) { |
| if (message == null || message.mMailboxKey != mMessage.mMailboxKey) { |
| // Message deleted or moved. |
| mCallback.onMessageNotExists(); |
| return; |
| } |
| mMessage = message; |
| updateHeaderView(mMessage); |
| } |
| } |
| |
| /** |
| * Called when a message is shown to the user. |
| */ |
| protected void onMessageShown(long messageId, Mailbox mailbox) { |
| } |
| |
| /** |
| * Called when the message body is loaded. |
| */ |
| protected void onPostLoadBody() { |
| } |
| |
| /** |
| * Async task for loading a single message body outside of the UI thread |
| */ |
| private class LoadBodyTask extends EmailAsyncTask<Void, Void, String[]> { |
| |
| private final long mId; |
| private boolean mErrorLoadingMessageBody; |
| private final boolean mAutoShowPictures; |
| |
| /** |
| * Special constructor to cache some local info |
| */ |
| public LoadBodyTask(long messageId, boolean autoShowPictures) { |
| super(mTaskTracker); |
| mId = messageId; |
| mAutoShowPictures = autoShowPictures; |
| } |
| |
| @Override |
| protected String[] doInBackground(Void... params) { |
| try { |
| String text = null; |
| String html = Body.restoreBodyHtmlWithMessageId(mContext, mId); |
| if (html == null) { |
| text = Body.restoreBodyTextWithMessageId(mContext, mId); |
| } |
| return new String[] { text, html }; |
| } catch (RuntimeException re) { |
| // This catches SQLiteException as well as other RTE's we've seen from the |
| // database calls, such as IllegalStateException |
| Log.d(Logging.LOG_TAG, "Exception while loading message body", re); |
| mErrorLoadingMessageBody = true; |
| return null; |
| } |
| } |
| |
| @Override |
| protected void onSuccess(String[] results) { |
| if (results == null) { |
| if (mErrorLoadingMessageBody) { |
| Utility.showToast(getActivity(), R.string.error_loading_message_body); |
| } |
| resetView(); |
| return; |
| } |
| reloadUiFromBody(results[0], results[1], mAutoShowPictures); // text, html |
| onPostLoadBody(); |
| } |
| } |
| |
| /** |
| * Async task for loading attachments |
| * |
| * Note: This really should only be called when the message load is complete - or, we should |
| * leave open a listener so the attachments can fill in as they are discovered. In either case, |
| * this implementation is incomplete, as it will fail to refresh properly if the message is |
| * partially loaded at this time. |
| */ |
| private class LoadAttachmentsTask extends EmailAsyncTask<Long, Void, Attachment[]> { |
| public LoadAttachmentsTask() { |
| super(mTaskTracker); |
| } |
| |
| @Override |
| protected Attachment[] doInBackground(Long... messageIds) { |
| return Attachment.restoreAttachmentsWithMessageId(mContext, messageIds[0]); |
| } |
| |
| @Override |
| protected void onSuccess(Attachment[] attachments) { |
| try { |
| if (attachments == null) { |
| return; |
| } |
| boolean htmlChanged = false; |
| int numDisplayedAttachments = 0; |
| for (Attachment attachment : attachments) { |
| if (mHtmlTextRaw != null && attachment.mContentId != null |
| && attachment.mContentUri != null) { |
| // for html body, replace CID for inline images |
| // Regexp which matches ' src="cid:contentId"'. |
| String contentIdRe = |
| "\\s+(?i)src=\"cid(?-i):\\Q" + attachment.mContentId + "\\E\""; |
| String srcContentUri = " src=\"" + attachment.mContentUri + "\""; |
| mHtmlTextRaw = mHtmlTextRaw.replaceAll(contentIdRe, srcContentUri); |
| htmlChanged = true; |
| } else { |
| addAttachment(attachment); |
| numDisplayedAttachments++; |
| } |
| } |
| setAttachmentCount(numDisplayedAttachments); |
| mHtmlTextWebView = mHtmlTextRaw; |
| mHtmlTextRaw = null; |
| if (htmlChanged) { |
| setMessageHtml(mHtmlTextWebView); |
| } |
| } finally { |
| showContent(true, false); |
| } |
| } |
| } |
| |
| private static Bitmap getPreviewIcon(Context context, AttachmentInfo attachment) { |
| try { |
| return BitmapFactory.decodeStream( |
| context.getContentResolver().openInputStream( |
| AttachmentUtilities.getAttachmentThumbnailUri( |
| attachment.mAccountKey, attachment.mId, |
| PREVIEW_ICON_WIDTH, |
| PREVIEW_ICON_HEIGHT))); |
| } catch (Exception e) { |
| Log.d(Logging.LOG_TAG, "Attachment preview failed with exception " + e.getMessage()); |
| return null; |
| } |
| } |
| |
| /** |
| * Subclass of AttachmentInfo which includes our views and buttons related to attachment |
| * handling, as well as our determination of suitability for viewing (based on availability of |
| * a viewer app) and saving (based upon the presence of external storage) |
| */ |
| private static class MessageViewAttachmentInfo extends AttachmentInfo { |
| private Button openButton; |
| private Button saveButton; |
| private Button loadButton; |
| private Button infoButton; |
| private Button cancelButton; |
| private ImageView iconView; |
| |
| private static final Map<AttachmentInfo, String> sSavedFileInfos = Maps.newHashMap(); |
| |
| // Don't touch it directly from the outer class. |
| private final ProgressBar mProgressView; |
| private boolean loaded; |
| |
| private MessageViewAttachmentInfo(Context context, Attachment attachment, |
| ProgressBar progressView) { |
| super(context, attachment); |
| mProgressView = progressView; |
| } |
| |
| /** |
| * Create a new attachment info based upon an existing attachment info. Display |
| * related fields (such as views and buttons) are copied from old to new. |
| */ |
| private MessageViewAttachmentInfo(Context context, MessageViewAttachmentInfo oldInfo) { |
| super(context, oldInfo); |
| openButton = oldInfo.openButton; |
| saveButton = oldInfo.saveButton; |
| loadButton = oldInfo.loadButton; |
| infoButton = oldInfo.infoButton; |
| cancelButton = oldInfo.cancelButton; |
| iconView = oldInfo.iconView; |
| mProgressView = oldInfo.mProgressView; |
| loaded = oldInfo.loaded; |
| } |
| |
| public void hideProgress() { |
| // Don't use GONE, which'll break the layout. |
| if (mProgressView.getVisibility() != View.INVISIBLE) { |
| mProgressView.setVisibility(View.INVISIBLE); |
| } |
| } |
| |
| public void showProgress(int progress) { |
| if (mProgressView.getVisibility() != View.VISIBLE) { |
| mProgressView.setVisibility(View.VISIBLE); |
| } |
| if (mProgressView.isIndeterminate()) { |
| mProgressView.setIndeterminate(false); |
| } |
| mProgressView.setProgress(progress); |
| |
| // Hide on completion. |
| if (progress == 100) { |
| hideProgress(); |
| } |
| } |
| |
| public void showProgressIndeterminate() { |
| if (mProgressView.getVisibility() != View.VISIBLE) { |
| mProgressView.setVisibility(View.VISIBLE); |
| } |
| if (!mProgressView.isIndeterminate()) { |
| mProgressView.setIndeterminate(true); |
| } |
| } |
| |
| /** |
| * Determines whether or not this attachment has a saved file in the external storage. That |
| * is, the user has at some point clicked "save" for this attachment. |
| * |
| * Note: this is an approximation and uses an in-memory cache that can get wiped when the |
| * process dies, and so is somewhat conservative. Additionally, the user can modify the file |
| * after saving, and so the file may not be the same (though this is unlikely). |
| */ |
| public boolean isFileSaved() { |
| String path = getSavedPath(); |
| if (path == null) { |
| return false; |
| } |
| boolean savedFileExists = new File(path).exists(); |
| if (!savedFileExists) { |
| // Purge the cache entry. |
| setSavedPath(null); |
| } |
| return savedFileExists; |
| } |
| |
| private void setSavedPath(String path) { |
| if (path == null) { |
| sSavedFileInfos.remove(this); |
| } else { |
| sSavedFileInfos.put(this, path); |
| } |
| } |
| |
| /** |
| * Returns an absolute file path for the given attachment if it has been saved. If one is |
| * not found, {@code null} is returned. |
| * |
| * Clients are expected to validate that the file at the given path is still valid. |
| */ |
| private String getSavedPath() { |
| return sSavedFileInfos.get(this); |
| } |
| |
| @Override |
| protected Uri getUriForIntent(Context context, long accountId) { |
| // Prefer to act on the saved file for intents. |
| String path = getSavedPath(); |
| return (path != null) |
| ? Uri.parse("file://" + getSavedPath()) |
| : super.getUriForIntent(context, accountId); |
| } |
| } |
| |
| /** |
| * Updates all current attachments on the attachment tab. |
| */ |
| private void updateAttachmentTab() { |
| for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { |
| View view = mAttachments.getChildAt(i); |
| MessageViewAttachmentInfo oldInfo = (MessageViewAttachmentInfo)view.getTag(); |
| MessageViewAttachmentInfo newInfo = |
| new MessageViewAttachmentInfo(getActivity(), oldInfo); |
| updateAttachmentButtons(newInfo); |
| view.setTag(newInfo); |
| } |
| } |
| |
| /** |
| * Updates the attachment buttons. Adjusts the visibility of the buttons as well |
| * as updating any tag information associated with the buttons. |
| */ |
| private void updateAttachmentButtons(MessageViewAttachmentInfo attachmentInfo) { |
| ImageView attachmentIcon = attachmentInfo.iconView; |
| Button openButton = attachmentInfo.openButton; |
| Button saveButton = attachmentInfo.saveButton; |
| Button loadButton = attachmentInfo.loadButton; |
| Button infoButton = attachmentInfo.infoButton; |
| Button cancelButton = attachmentInfo.cancelButton; |
| |
| if (!attachmentInfo.mAllowView) { |
| openButton.setVisibility(View.GONE); |
| } |
| if (!attachmentInfo.mAllowSave) { |
| saveButton.setVisibility(View.GONE); |
| } |
| |
| if (!attachmentInfo.mAllowView && !attachmentInfo.mAllowSave) { |
| // This attachment may never be viewed or saved, so block everything |
| attachmentInfo.hideProgress(); |
| openButton.setVisibility(View.GONE); |
| saveButton.setVisibility(View.GONE); |
| loadButton.setVisibility(View.GONE); |
| cancelButton.setVisibility(View.GONE); |
| infoButton.setVisibility(View.VISIBLE); |
| } else if (attachmentInfo.loaded) { |
| // If the attachment is loaded, show 100% progress |
| // Note that for POP3 messages, the user will only see "Open" and "Save", |
| // because the entire message is loaded before being shown. |
| // Hide "Load" and "Info", show "View" and "Save" |
| attachmentInfo.showProgress(100); |
| if (attachmentInfo.mAllowSave) { |
| saveButton.setVisibility(View.VISIBLE); |
| |
| boolean isFileSaved = attachmentInfo.isFileSaved(); |
| saveButton.setEnabled(!isFileSaved); |
| if (!isFileSaved) { |
| saveButton.setText(R.string.message_view_attachment_save_action); |
| } else { |
| saveButton.setText(R.string.message_view_attachment_saved); |
| } |
| } |
| if (attachmentInfo.mAllowView) { |
| // Set the attachment action button text accordingly |
| if (attachmentInfo.mContentType.startsWith("audio/") || |
| attachmentInfo.mContentType.startsWith("video/")) { |
| openButton.setText(R.string.message_view_attachment_play_action); |
| } else if (attachmentInfo.mAllowInstall) { |
| openButton.setText(R.string.message_view_attachment_install_action); |
| } else { |
| openButton.setText(R.string.message_view_attachment_view_action); |
| } |
| openButton.setVisibility(View.VISIBLE); |
| } |
| if (attachmentInfo.mDenyFlags == AttachmentInfo.ALLOW) { |
| infoButton.setVisibility(View.GONE); |
| } else { |
| infoButton.setVisibility(View.VISIBLE); |
| } |
| loadButton.setVisibility(View.GONE); |
| cancelButton.setVisibility(View.GONE); |
| |
| updatePreviewIcon(attachmentInfo); |
| } else { |
| // The attachment is not loaded, so present UI to start downloading it |
| |
| // Show "Load"; hide "View", "Save" and "Info" |
| saveButton.setVisibility(View.GONE); |
| openButton.setVisibility(View.GONE); |
| infoButton.setVisibility(View.GONE); |
| |
| // If the attachment is queued, show the indeterminate progress bar. From this point,. |
| // any progress changes will cause this to be replaced by the normal progress bar |
| if (AttachmentDownloadService.isAttachmentQueued(attachmentInfo.mId)) { |
| attachmentInfo.showProgressIndeterminate(); |
| loadButton.setVisibility(View.GONE); |
| cancelButton.setVisibility(View.VISIBLE); |
| } else { |
| loadButton.setVisibility(View.VISIBLE); |
| cancelButton.setVisibility(View.GONE); |
| } |
| } |
| openButton.setTag(attachmentInfo); |
| saveButton.setTag(attachmentInfo); |
| loadButton.setTag(attachmentInfo); |
| infoButton.setTag(attachmentInfo); |
| cancelButton.setTag(attachmentInfo); |
| } |
| |
| /** |
| * Copy data from a cursor-refreshed attachment into the UI. Called from UI thread. |
| * |
| * @param attachment A single attachment loaded from the provider |
| */ |
| private void addAttachment(Attachment attachment) { |
| LayoutInflater inflater = getActivity().getLayoutInflater(); |
| View view = inflater.inflate(R.layout.message_view_attachment, null); |
| |
| TextView attachmentName = (TextView) UiUtilities.getView(view, R.id.attachment_name); |
| TextView attachmentInfoView = (TextView) UiUtilities.getView(view, R.id.attachment_info); |
| ImageView attachmentIcon = (ImageView) UiUtilities.getView(view, R.id.attachment_icon); |
| Button openButton = (Button) UiUtilities.getView(view, R.id.open); |
| Button saveButton = (Button) UiUtilities.getView(view, R.id.save); |
| Button loadButton = (Button) UiUtilities.getView(view, R.id.load); |
| Button infoButton = (Button) UiUtilities.getView(view, R.id.info); |
| Button cancelButton = (Button) UiUtilities.getView(view, R.id.cancel); |
| ProgressBar attachmentProgress = (ProgressBar) UiUtilities.getView(view, R.id.progress); |
| |
| MessageViewAttachmentInfo attachmentInfo = new MessageViewAttachmentInfo( |
| mContext, attachment, attachmentProgress); |
| |
| // Check whether the attachment already exists |
| if (Utility.attachmentExists(mContext, attachment)) { |
| attachmentInfo.loaded = true; |
| } |
| |
| attachmentInfo.openButton = openButton; |
| attachmentInfo.saveButton = saveButton; |
| attachmentInfo.loadButton = loadButton; |
| attachmentInfo.infoButton = infoButton; |
| attachmentInfo.cancelButton = cancelButton; |
| attachmentInfo.iconView = attachmentIcon; |
| |
| updateAttachmentButtons(attachmentInfo); |
| |
| view.setTag(attachmentInfo); |
| openButton.setOnClickListener(this); |
| saveButton.setOnClickListener(this); |
| loadButton.setOnClickListener(this); |
| infoButton.setOnClickListener(this); |
| cancelButton.setOnClickListener(this); |
| |
| attachmentName.setText(attachmentInfo.mName); |
| attachmentInfoView.setText(UiUtilities.formatSize(mContext, attachmentInfo.mSize)); |
| |
| mAttachments.addView(view); |
| mAttachments.setVisibility(View.VISIBLE); |
| } |
| |
| private MessageViewAttachmentInfo findAttachmentInfoFromView(long attachmentId) { |
| for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { |
| MessageViewAttachmentInfo attachmentInfo = |
| (MessageViewAttachmentInfo) mAttachments.getChildAt(i).getTag(); |
| if (attachmentInfo.mId == attachmentId) { |
| return attachmentInfo; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Reload the UI from a provider cursor. {@link LoadMessageTask#onSuccess} calls it. |
| * |
| * Update the header views, and start loading the body. |
| * |
| * @param message A copy of the message loaded from the database |
| * @param okToFetch If true, and message is not fully loaded, it's OK to fetch from |
| * the network. Use false to prevent looping here. |
| */ |
| protected void reloadUiFromMessage(Message message, boolean okToFetch) { |
| mMessage = message; |
| mAccountId = message.mAccountKey; |
| |
| mMessageObserver.register(ContentUris.withAppendedId(Message.CONTENT_URI, mMessage.mId)); |
| |
| updateHeaderView(mMessage); |
| |
| // Handle partially-loaded email, as follows: |
| // 1. Check value of message.mFlagLoaded |
| // 2. If != LOADED, ask controller to load it |
| // 3. Controller callback (after loaded) should trigger LoadBodyTask & LoadAttachmentsTask |
| // 4. Else start the loader tasks right away (message already loaded) |
| if (okToFetch && message.mFlagLoaded != Message.FLAG_LOADED_COMPLETE) { |
| mControllerCallback.getWrappee().setWaitForLoadMessageId(message.mId); |
| mController.loadMessageForView(message.mId); |
| } else { |
| Address[] fromList = Address.unpack(mMessage.mFrom); |
| boolean autoShowImages = false; |
| for (Address sender : fromList) { |
| String email = sender.getAddress(); |
| if (shouldShowImagesFor(email)) { |
| autoShowImages = true; |
| break; |
| } |
| } |
| mControllerCallback.getWrappee().setWaitForLoadMessageId(Message.NO_MESSAGE); |
| // Ask for body |
| new LoadBodyTask(message.mId, autoShowImages).executeParallel(); |
| } |
| } |
| |
| protected void updateHeaderView(Message message) { |
| mSubjectView.setText(message.mSubject); |
| final Address from = Address.unpackFirst(message.mFrom); |
| |
| // Set sender address/display name |
| // Note we set " " for empty field, so TextView's won't get squashed. |
| // Otherwise their height will be 0, which breaks the layout. |
| if (from != null) { |
| final String fromFriendly = from.toFriendly(); |
| final String fromAddress = from.getAddress(); |
| mFromNameView.setText(fromFriendly); |
| mFromAddressView.setText(fromFriendly.equals(fromAddress) ? " " : fromAddress); |
| } else { |
| mFromNameView.setText(" "); |
| mFromAddressView.setText(" "); |
| } |
| mDateTimeView.setText(DateUtils.getRelativeTimeSpanString(mContext, message.mTimeStamp) |
| .toString()); |
| |
| // To/Cc/Bcc |
| final Resources res = mContext.getResources(); |
| final SpannableStringBuilder ssb = new SpannableStringBuilder(); |
| final String friendlyTo = Address.toFriendly(Address.unpack(message.mTo)); |
| final String friendlyCc = Address.toFriendly(Address.unpack(message.mCc)); |
| final String friendlyBcc = Address.toFriendly(Address.unpack(message.mBcc)); |
| |
| if (!TextUtils.isEmpty(friendlyTo)) { |
| Utility.appendBold(ssb, res.getString(R.string.message_view_to_label)); |
| ssb.append(" "); |
| ssb.append(friendlyTo); |
| } |
| if (!TextUtils.isEmpty(friendlyCc)) { |
| ssb.append(" "); |
| Utility.appendBold(ssb, res.getString(R.string.message_view_cc_label)); |
| ssb.append(" "); |
| ssb.append(friendlyCc); |
| } |
| if (!TextUtils.isEmpty(friendlyBcc)) { |
| ssb.append(" "); |
| Utility.appendBold(ssb, res.getString(R.string.message_view_bcc_label)); |
| ssb.append(" "); |
| ssb.append(friendlyBcc); |
| } |
| mAddressesView.setText(ssb); |
| } |
| |
| /** |
| * @return the given date/time in a human readable form. The returned string always have |
| * month and day (and year if {@code withYear} is set), so is usually long. |
| * Use {@link DateUtils#getRelativeTimeSpanString} instead to save the screen real estate. |
| */ |
| private String formatDate(long millis, boolean withYear) { |
| StringBuilder sb = new StringBuilder(); |
| Formatter formatter = new Formatter(sb); |
| DateUtils.formatDateRange(mContext, formatter, millis, millis, |
| DateUtils.FORMAT_SHOW_DATE |
| | DateUtils.FORMAT_ABBREV_ALL |
| | DateUtils.FORMAT_SHOW_TIME |
| | (withYear ? DateUtils.FORMAT_SHOW_YEAR : DateUtils.FORMAT_NO_YEAR)); |
| return sb.toString(); |
| } |
| |
| /** |
| * Reload the body from the provider cursor. This must only be called from the UI thread. |
| * |
| * @param bodyText text part |
| * @param bodyHtml html part |
| * |
| * TODO deal with html vs text and many other issues <- WHAT DOES IT MEAN?? |
| */ |
| private void reloadUiFromBody(String bodyText, String bodyHtml, boolean autoShowPictures) { |
| String text = null; |
| mHtmlTextRaw = null; |
| boolean hasImages = false; |
| |
| if (bodyHtml == null) { |
| text = bodyText; |
| /* |
| * Convert the plain text to HTML |
| */ |
| StringBuffer sb = new StringBuffer("<html><body>"); |
| if (text != null) { |
| // Escape any inadvertent HTML in the text message |
| text = EmailHtmlUtil.escapeCharacterToDisplay(text); |
| // Find any embedded URL's and linkify |
| Matcher m = Patterns.WEB_URL.matcher(text); |
| while (m.find()) { |
| int start = m.start(); |
| /* |
| * WEB_URL_PATTERN may match domain part of email address. To detect |
| * this false match, the character just before the matched string |
| * should not be '@'. |
| */ |
| if (start == 0 || text.charAt(start - 1) != '@') { |
| String url = m.group(); |
| Matcher proto = WEB_URL_PROTOCOL.matcher(url); |
| String link; |
| if (proto.find()) { |
| // This is work around to force URL protocol part be lower case, |
| // because WebView could follow only lower case protocol link. |
| link = proto.group().toLowerCase() + url.substring(proto.end()); |
| } else { |
| // Patterns.WEB_URL matches URL without protocol part, |
| // so added default protocol to link. |
| link = "http://" + url; |
| } |
| String href = String.format("<a href=\"%s\">%s</a>", link, url); |
| m.appendReplacement(sb, href); |
| } |
| else { |
| m.appendReplacement(sb, "$0"); |
| } |
| } |
| m.appendTail(sb); |
| } |
| sb.append("</body></html>"); |
| text = sb.toString(); |
| } else { |
| text = bodyHtml; |
| mHtmlTextRaw = bodyHtml; |
| hasImages = IMG_TAG_START_REGEX.matcher(text).find(); |
| } |
| |
| // TODO this is not really accurate. |
| // - Images aren't the only network resources. (e.g. CSS) |
| // - If images are attached to the email and small enough, we download them at once, |
| // and won't need network access when they're shown. |
| if (hasImages) { |
| if (mRestoredPictureLoaded || autoShowPictures) { |
| blockNetworkLoads(false); |
| addTabFlags(TAB_FLAGS_PICTURE_LOADED); // Set for next onSaveInstanceState |
| |
| // Make sure to reset the flag -- otherwise this will keep taking effect even after |
| // moving to another message. |
| mRestoredPictureLoaded = false; |
| } else { |
| addTabFlags(TAB_FLAGS_HAS_PICTURES); |
| } |
| } |
| setMessageHtml(text); |
| |
| // Ask for attachments after body |
| new LoadAttachmentsTask().executeParallel(mMessage.mId); |
| |
| mIsMessageLoadedForTest = true; |
| } |
| |
| /** |
| * Overrides for WebView behaviors. |
| */ |
| private class CustomWebViewClient extends WebViewClient { |
| @Override |
| public boolean shouldOverrideUrlLoading(WebView view, String url) { |
| return mCallback.onUrlInMessageClicked(url); |
| } |
| } |
| |
| private View findAttachmentView(long attachmentId) { |
| for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { |
| View view = mAttachments.getChildAt(i); |
| MessageViewAttachmentInfo attachment = (MessageViewAttachmentInfo) view.getTag(); |
| if (attachment.mId == attachmentId) { |
| return view; |
| } |
| } |
| return null; |
| } |
| |
| private MessageViewAttachmentInfo findAttachmentInfo(long attachmentId) { |
| View view = findAttachmentView(attachmentId); |
| if (view != null) { |
| return (MessageViewAttachmentInfo)view.getTag(); |
| } |
| return null; |
| } |
| |
| /** |
| * Controller results listener. We wrap it with {@link ControllerResultUiThreadWrapper}, |
| * so all methods are called on the UI thread. |
| */ |
| private class ControllerResults extends Controller.Result { |
| private long mWaitForLoadMessageId; |
| |
| public void setWaitForLoadMessageId(long messageId) { |
| mWaitForLoadMessageId = messageId; |
| } |
| |
| @Override |
| public void loadMessageForViewCallback(MessagingException result, long accountId, |
| long messageId, int progress) { |
| if (messageId != mWaitForLoadMessageId) { |
| // We are not waiting for this message to load, so exit quickly |
| return; |
| } |
| if (result == null) { |
| switch (progress) { |
| case 0: |
| mCallback.onLoadMessageStarted(); |
| // Loading from network -- show the progress icon. |
| showContent(false, true); |
| break; |
| case 100: |
| mWaitForLoadMessageId = -1; |
| mCallback.onLoadMessageFinished(); |
| // reload UI and reload everything else too |
| // pass false to LoadMessageTask to prevent looping here |
| cancelAllTasks(); |
| new LoadMessageTask(false).executeParallel(); |
| break; |
| default: |
| // do nothing - we don't have a progress bar at this time |
| break; |
| } |
| } else { |
| mWaitForLoadMessageId = Message.NO_MESSAGE; |
| String error = mContext.getString(R.string.status_network_error); |
| mCallback.onLoadMessageError(error); |
| resetView(); |
| } |
| } |
| |
| @Override |
| public void loadAttachmentCallback(MessagingException result, long accountId, |
| long messageId, long attachmentId, int progress) { |
| if (messageId == mMessageId) { |
| if (result == null) { |
| showAttachmentProgress(attachmentId, progress); |
| switch (progress) { |
| case 100: |
| final MessageViewAttachmentInfo attachmentInfo = |
| findAttachmentInfoFromView(attachmentId); |
| if (attachmentInfo != null) { |
| updatePreviewIcon(attachmentInfo); |
| } |
| doFinishLoadAttachment(attachmentId); |
| break; |
| default: |
| // do nothing - we don't have a progress bar at this time |
| break; |
| } |
| } else { |
| MessageViewAttachmentInfo attachment = findAttachmentInfo(attachmentId); |
| if (attachment == null) { |
| // Called before LoadAttachmentsTask finishes. |
| // (Possible if you quickly close & re-open a message) |
| return; |
| } |
| attachment.cancelButton.setVisibility(View.GONE); |
| attachment.loadButton.setVisibility(View.VISIBLE); |
| attachment.hideProgress(); |
| |
| final String error; |
| if (result.getCause() instanceof IOException) { |
| error = mContext.getString(R.string.status_network_error); |
| } else { |
| error = mContext.getString( |
| R.string.message_view_load_attachment_failed_toast, |
| attachment.mName); |
| } |
| mCallback.onLoadMessageError(error); |
| } |
| } |
| } |
| |
| private void showAttachmentProgress(long attachmentId, int progress) { |
| MessageViewAttachmentInfo attachment = findAttachmentInfo(attachmentId); |
| if (attachment != null) { |
| if (progress == 0) { |
| attachment.cancelButton.setVisibility(View.GONE); |
| } |
| attachment.showProgress(progress); |
| } |
| } |
| } |
| |
| /** |
| * Class to detect update on the current message (e.g. toggle star). When it gets content |
| * change notifications, it kicks {@link ReloadMessageTask}. |
| */ |
| private class MessageObserver extends ContentObserver implements Runnable { |
| private final Throttle mThrottle; |
| private final ContentResolver mContentResolver; |
| |
| private boolean mRegistered; |
| |
| public MessageObserver(Handler handler, Context context) { |
| super(handler); |
| mContentResolver = context.getContentResolver(); |
| mThrottle = new Throttle("MessageObserver", this, handler); |
| } |
| |
| public void unregister() { |
| if (!mRegistered) { |
| return; |
| } |
| mThrottle.cancelScheduledCallback(); |
| mContentResolver.unregisterContentObserver(this); |
| mRegistered = false; |
| } |
| |
| public void register(Uri notifyUri) { |
| unregister(); |
| mContentResolver.registerContentObserver(notifyUri, true, this); |
| mRegistered = true; |
| } |
| |
| @Override |
| public boolean deliverSelfNotifications() { |
| return true; |
| } |
| |
| @Override |
| public void onChange(boolean selfChange) { |
| if (mRegistered) { |
| mThrottle.onEvent(); |
| } |
| } |
| |
| /** This method is delay-called by {@link Throttle} on the UI thread. */ |
| @Override |
| public void run() { |
| // This method is delay-called, so need to make sure if it's still registered. |
| if (mRegistered) { |
| new ReloadMessageTask().cancelPreviousAndExecuteParallel(); |
| } |
| } |
| } |
| |
| private void updatePreviewIcon(MessageViewAttachmentInfo attachmentInfo) { |
| new UpdatePreviewIconTask(attachmentInfo).executeParallel(); |
| } |
| |
| private class UpdatePreviewIconTask extends EmailAsyncTask<Void, Void, Bitmap> { |
| @SuppressWarnings("hiding") |
| private final Context mContext; |
| private final MessageViewAttachmentInfo mAttachmentInfo; |
| |
| public UpdatePreviewIconTask(MessageViewAttachmentInfo attachmentInfo) { |
| super(mTaskTracker); |
| mContext = getActivity(); |
| mAttachmentInfo = attachmentInfo; |
| } |
| |
| @Override |
| protected Bitmap doInBackground(Void... params) { |
| return getPreviewIcon(mContext, mAttachmentInfo); |
| } |
| |
| @Override |
| protected void onSuccess(Bitmap result) { |
| if (result == null) { |
| return; |
| } |
| mAttachmentInfo.iconView.setImageBitmap(result); |
| } |
| } |
| |
| private boolean shouldShowImagesFor(String senderEmail) { |
| return Preferences.getPreferences(getActivity()).shouldShowImagesFor(senderEmail); |
| } |
| |
| private void setShowImagesForSender() { |
| makeVisible(UiUtilities.getView(getView(), R.id.always_show_pictures_button), false); |
| Utility.showToast(getActivity(), R.string.message_view_always_show_pictures_confirmation); |
| |
| // Force redraw of the container. |
| updateTabs(mTabFlags); |
| |
| Address[] fromList = Address.unpack(mMessage.mFrom); |
| Preferences prefs = Preferences.getPreferences(getActivity()); |
| for (Address sender : fromList) { |
| String email = sender.getAddress(); |
| prefs.setSenderAsTrusted(email); |
| } |
| } |
| |
| public boolean isMessageLoadedForTest() { |
| return mIsMessageLoadedForTest; |
| } |
| |
| public void clearIsMessageLoadedForTest() { |
| mIsMessageLoadedForTest = true; |
| } |
| } |