Home | History | Annotate | Download | only in activity
      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 
     17 package com.android.email.activity;
     18 
     19 import android.app.Activity;
     20 import android.content.res.Resources;
     21 import android.graphics.drawable.Drawable;
     22 import android.os.Bundle;
     23 import android.view.LayoutInflater;
     24 import android.view.Menu;
     25 import android.view.MenuInflater;
     26 import android.view.MenuItem;
     27 import android.view.View;
     28 import android.view.ViewGroup;
     29 import android.view.accessibility.AccessibilityEvent;
     30 import android.widget.ImageView;
     31 import android.widget.PopupMenu;
     32 import android.widget.PopupMenu.OnMenuItemClickListener;
     33 
     34 import com.android.email.Email;
     35 import com.android.email.Preferences;
     36 import com.android.email.R;
     37 import com.android.emailcommon.mail.MeetingInfo;
     38 import com.android.emailcommon.mail.PackedString;
     39 import com.android.emailcommon.provider.Account;
     40 import com.android.emailcommon.provider.EmailContent.Message;
     41 import com.android.emailcommon.provider.Mailbox;
     42 import com.android.emailcommon.service.EmailServiceConstants;
     43 import com.android.emailcommon.utility.Utility;
     44 
     45 /**
     46  * A {@link MessageViewFragmentBase} subclass for regular email messages.  (regular as in "not eml
     47  * files").
     48  */
     49 public class MessageViewFragment extends MessageViewFragmentBase
     50         implements MoveMessageToDialog.Callback, OnMenuItemClickListener {
     51     /** Argument name(s) */
     52     private static final String ARG_MESSAGE_ID = "messageId";
     53 
     54     private ImageView mFavoriteIcon;
     55 
     56     private View mReplyButton;
     57 
     58     private View mReplyAllButton;
     59 
     60     /* Nullable - not available on phone portrait. */
     61     private View mForwardButton;
     62 
     63     private View mMoreButton;
     64 
     65     // calendar meeting invite answers
     66     private View mMeetingYes;
     67     private View mMeetingMaybe;
     68     private View mMeetingNo;
     69     private Drawable mFavoriteIconOn;
     70     private Drawable mFavoriteIconOff;
     71 
     72     /** Default to ReplyAll if true. Otherwise Reply. */
     73     boolean mDefaultReplyAll;
     74 
     75     /** Whether or not to enable Reply/ReplyAll and Forward buttons */
     76     boolean mEnableReplyForwardButtons;
     77 
     78     /** Whether or not the message can be moved from the mailbox it's in. */
     79     private boolean mSupportsMove;
     80 
     81     private int mPreviousMeetingResponse = EmailServiceConstants.MEETING_REQUEST_NOT_RESPONDED;
     82 
     83     /**
     84      * This class has more call backs than {@link MessageViewFragmentBase}.
     85      *
     86      * - EML files can't be "mark unread".
     87      * - EML files can't have the invite buttons or the view in calender link.
     88      *   Note EML files can have ICS (calendar invitation) files, but we don't treat them as
     89      *   invites.  (Only exchange provider sets the FLAG_INCOMING_MEETING_INVITE
     90      *   flag.)
     91      *   It'd be weird to respond to an invitation in an EML that might not be addressed to you...
     92      */
     93     public interface Callback extends MessageViewFragmentBase.Callback {
     94         /** Called when the "view in calendar" link is clicked. */
     95         public void onCalendarLinkClicked(long epochEventStartTime);
     96 
     97         /**
     98          * Called when a calender response button is clicked.
     99          *
    100          * @param response one of {@link EmailServiceConstants#MEETING_REQUEST_ACCEPTED},
    101          * {@link EmailServiceConstants#MEETING_REQUEST_DECLINED}, or
    102          * {@link EmailServiceConstants#MEETING_REQUEST_TENTATIVE}.
    103          */
    104         public void onRespondedToInvite(int response);
    105 
    106         /** Called when the current message is set unread. */
    107         public void onMessageSetUnread();
    108 
    109         /**
    110          * Called right before the current message will be deleted or moved to another mailbox.
    111          *
    112          * Callees will usually close the fragment.
    113          */
    114         public void onBeforeMessageGone();
    115 
    116         /** Called when the forward button is pressed. */
    117         public void onForward();
    118         /** Called when the reply button is pressed. */
    119         public void onReply();
    120         /** Called when the reply-all button is pressed. */
    121         public void onReplyAll();
    122     }
    123 
    124     public static final class EmptyCallback extends MessageViewFragmentBase.EmptyCallback
    125             implements Callback {
    126         @SuppressWarnings("hiding")
    127         public static final Callback INSTANCE = new EmptyCallback();
    128 
    129         @Override public void onCalendarLinkClicked(long epochEventStartTime) { }
    130         @Override public void onMessageSetUnread() { }
    131         @Override public void onRespondedToInvite(int response) { }
    132         @Override public void onBeforeMessageGone() { }
    133         @Override public void onForward() { }
    134         @Override public void onReply() { }
    135         @Override public void onReplyAll() { }
    136     }
    137 
    138     private Callback mCallback = EmptyCallback.INSTANCE;
    139 
    140     /**
    141      * Create a new instance with initialization parameters.
    142      *
    143      * This fragment should be created only with this method.  (Arguments should always be set.)
    144      *
    145      * @param messageId ID of the message to open
    146      */
    147     public static MessageViewFragment newInstance(long messageId) {
    148         if (messageId == Message.NO_MESSAGE) {
    149             throw new IllegalArgumentException();
    150         }
    151         final MessageViewFragment instance = new MessageViewFragment();
    152         final Bundle args = new Bundle();
    153         args.putLong(ARG_MESSAGE_ID, messageId);
    154         instance.setArguments(args);
    155         return instance;
    156     }
    157 
    158     /**
    159      * We will display the message for this ID. This must never be a special message ID such as
    160      * {@link Message#NO_MESSAGE}. Do NOT use directly; instead, use {@link #getMessageId()}.
    161      * <p><em>NOTE:</em> Although we cannot force these to be immutable using Java language
    162      * constructs, this <em>must</em> be considered immutable.
    163      */
    164     private Long mImmutableMessageId;
    165 
    166     private void initializeArgCache() {
    167         if (mImmutableMessageId != null) return;
    168         mImmutableMessageId = getArguments().getLong(ARG_MESSAGE_ID);
    169     }
    170 
    171     /**
    172      * @return the message ID passed to {@link #newInstance}.  Safe to call even before onCreate.
    173      */
    174     public long getMessageId() {
    175         initializeArgCache();
    176         return mImmutableMessageId;
    177     }
    178 
    179     @Override
    180     public void onCreate(Bundle savedInstanceState) {
    181         super.onCreate(savedInstanceState);
    182 
    183         setHasOptionsMenu(true);
    184 
    185         final Resources res = getActivity().getResources();
    186         mFavoriteIconOn = res.getDrawable(R.drawable.btn_star_on_convo_holo_light);
    187         mFavoriteIconOff = res.getDrawable(R.drawable.btn_star_off_convo_holo_light);
    188     }
    189 
    190     @Override
    191     public void onResume() {
    192         super.onResume();
    193         if (mMoreButton != null) {
    194             mDefaultReplyAll = Preferences.getSharedPreferences(mContext).getBoolean(
    195                     Preferences.REPLY_ALL, Preferences.REPLY_ALL_DEFAULT);
    196 
    197             int replyVisibility = View.GONE;
    198             int replyAllVisibility = View.GONE;
    199             if (mEnableReplyForwardButtons) {
    200                 replyVisibility = mDefaultReplyAll ? View.GONE : View.VISIBLE;
    201                 replyAllVisibility = mDefaultReplyAll ? View.VISIBLE : View.GONE;
    202             }
    203             mReplyButton.setVisibility(replyVisibility);
    204             mReplyAllButton.setVisibility(replyAllVisibility);
    205         }
    206     }
    207 
    208     @Override
    209     public View onCreateView(
    210             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    211         final View view = super.onCreateView(inflater, container, savedInstanceState);
    212 
    213         mFavoriteIcon = (ImageView) UiUtilities.getView(view, R.id.favorite);
    214         mReplyButton = UiUtilities.getView(view, R.id.reply);
    215         mReplyAllButton = UiUtilities.getView(view, R.id.reply_all);
    216         mForwardButton = UiUtilities.getViewOrNull(view, R.id.forward);
    217         mMeetingYes = UiUtilities.getView(view, R.id.accept);
    218         mMeetingMaybe = UiUtilities.getView(view, R.id.maybe);
    219         mMeetingNo = UiUtilities.getView(view, R.id.decline);
    220         mMoreButton = UiUtilities.getViewOrNull(view, R.id.more);
    221 
    222         mFavoriteIcon.setOnClickListener(this);
    223         mReplyButton.setOnClickListener(this);
    224         mReplyAllButton.setOnClickListener(this);
    225         if (mMoreButton != null) {
    226             mMoreButton.setOnClickListener(this);
    227         }
    228         if (mForwardButton != null) {
    229             mForwardButton.setOnClickListener(this);
    230         }
    231         mMeetingYes.setOnClickListener(this);
    232         mMeetingMaybe.setOnClickListener(this);
    233         mMeetingNo.setOnClickListener(this);
    234         UiUtilities.getView(view, R.id.invite_link).setOnClickListener(this);
    235 
    236         enableReplyForwardButtons(false);
    237 
    238         return view;
    239     }
    240 
    241     @Override
    242     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    243         inflater.inflate(R.menu.message_view_fragment_option, menu);
    244     }
    245 
    246     @Override
    247     public void onPrepareOptionsMenu(Menu menu) {
    248         menu.findItem(R.id.move).setVisible(mSupportsMove);
    249     }
    250 
    251     private void enableReplyForwardButtons(boolean enabled) {
    252         mEnableReplyForwardButtons = enabled;
    253         // We don't have disabled button assets, so let's hide them for now
    254         final int visibility = enabled ? View.VISIBLE : View.GONE;
    255 
    256         // Modify Reply All button only if there's no overflow OR there is
    257         // overflow but default is to show the Reply All button
    258         if (mMoreButton == null || mDefaultReplyAll) {
    259             UiUtilities.setVisibilitySafe(mReplyAllButton, visibility);
    260         }
    261 
    262         // Modify Reply button only if there's no overflow OR there is
    263         // overflow but default is to show the Reply button
    264         if (mMoreButton == null || !mDefaultReplyAll) {
    265                UiUtilities.setVisibilitySafe(mReplyButton, visibility);
    266         }
    267 
    268         if (mForwardButton != null) {
    269             mForwardButton.setVisibility(visibility);
    270         }
    271         if (mMoreButton != null) {
    272             mMoreButton.setVisibility(visibility);
    273         }
    274     }
    275 
    276     public void setCallback(Callback callback) {
    277         mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
    278         super.setCallback(mCallback);
    279     }
    280 
    281     @Override
    282     protected void resetView() {
    283         super.resetView();
    284         mPreviousMeetingResponse = EmailServiceConstants.MEETING_REQUEST_NOT_RESPONDED;
    285     }
    286 
    287     /**
    288      * NOTE See the comment on the super method.  It's called on a worker thread.
    289      */
    290     @Override
    291     protected Message openMessageSync(Activity activity) {
    292         return Message.restoreMessageWithId(activity, getMessageId());
    293     }
    294 
    295     @Override
    296     protected void onMessageShown(long messageId, Mailbox mailbox) {
    297         super.onMessageShown(messageId, mailbox);
    298 
    299         Account account = Account.restoreAccountWithId(mContext, getAccountId());
    300         boolean supportsMove = account.supportsMoveMessages(mContext)
    301                 && mailbox.canHaveMessagesMoved();
    302         if (mSupportsMove != supportsMove) {
    303             mSupportsMove = supportsMove;
    304             Activity host = getActivity();
    305             if (host != null) {
    306                 host.invalidateOptionsMenu();
    307             }
    308         }
    309 
    310         // Disable forward/reply buttons as necessary.
    311         enableReplyForwardButtons(Mailbox.isMailboxTypeReplyAndForwardable(mailbox.mType));
    312     }
    313 
    314     /**
    315      * Sets the content description for the star icon based on whether it's currently starred.
    316      */
    317     private void setStarContentDescription(boolean isFavorite) {
    318         if (isFavorite) {
    319             mFavoriteIcon.setContentDescription(
    320                     mContext.getResources().getString(R.string.remove_star_action));
    321         } else {
    322             mFavoriteIcon.setContentDescription(
    323                     mContext.getResources().getString(R.string.set_star_action));
    324         }
    325     }
    326 
    327     /**
    328      * Toggle favorite status and write back to provider
    329      */
    330     private void onClickFavorite() {
    331         if (!isMessageOpen()) return;
    332         Message message = getMessage();
    333 
    334         // Update UI
    335         boolean newFavorite = ! message.mFlagFavorite;
    336         mFavoriteIcon.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff);
    337 
    338         // Handle accessibility event
    339         setStarContentDescription(newFavorite);
    340         mFavoriteIcon.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
    341 
    342         // Update provider
    343         message.mFlagFavorite = newFavorite;
    344         getController().setMessageFavorite(message.mId, newFavorite);
    345     }
    346 
    347     /**
    348      * Set message read/unread.
    349      */
    350     public void onMarkMessageAsRead(boolean isRead) {
    351         if (!isMessageOpen()) return;
    352         Message message = getMessage();
    353         if (message.mFlagRead != isRead) {
    354             message.mFlagRead = isRead;
    355             getController().setMessageRead(message.mId, isRead);
    356             if (!isRead) { // Became unread.  We need to close the message.
    357                 mCallback.onMessageSetUnread();
    358             }
    359         }
    360     }
    361 
    362     /**
    363      * Send a service message indicating that a meeting invite button has been clicked.
    364      */
    365     private void onRespondToInvite(int response, int toastResId) {
    366         if (!isMessageOpen()) return;
    367         Message message = getMessage();
    368         // do not send twice in a row the same response
    369         if (mPreviousMeetingResponse != response) {
    370             getController().sendMeetingResponse(message.mId, response);
    371             mPreviousMeetingResponse = response;
    372         }
    373         Utility.showToast(getActivity(), toastResId);
    374         mCallback.onRespondedToInvite(response);
    375     }
    376 
    377     private void onInviteLinkClicked() {
    378         if (!isMessageOpen()) return;
    379         Message message = getMessage();
    380         String startTime = new PackedString(message.mMeetingInfo).get(MeetingInfo.MEETING_DTSTART);
    381         if (startTime != null) {
    382             long epochTimeMillis = Utility.parseEmailDateTimeToMillis(startTime);
    383             mCallback.onCalendarLinkClicked(epochTimeMillis);
    384         } else {
    385             Email.log("meetingInfo without DTSTART " + message.mMeetingInfo);
    386         }
    387     }
    388 
    389     @Override
    390     public void onClick(View view) {
    391         if (!isMessageOpen()) {
    392             return; // Ignore.
    393         }
    394         switch (view.getId()) {
    395             case R.id.reply:
    396                 mCallback.onReply();
    397                 return;
    398             case R.id.reply_all:
    399                 mCallback.onReplyAll();
    400                 return;
    401             case R.id.forward:
    402                 mCallback.onForward();
    403                 return;
    404 
    405             case R.id.favorite:
    406                 onClickFavorite();
    407                 return;
    408 
    409             case R.id.invite_link:
    410                 onInviteLinkClicked();
    411                 return;
    412 
    413             case R.id.accept:
    414                 onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_ACCEPTED,
    415                         R.string.message_view_invite_toast_yes);
    416                 return;
    417             case R.id.maybe:
    418                 onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_TENTATIVE,
    419                         R.string.message_view_invite_toast_maybe);
    420                 return;
    421             case R.id.decline:
    422                 onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_DECLINED,
    423                         R.string.message_view_invite_toast_no);
    424                 return;
    425 
    426             case R.id.more: {
    427                 PopupMenu popup = new PopupMenu(getActivity(), mMoreButton);
    428                 Menu menu = popup.getMenu();
    429                 popup.getMenuInflater().inflate(R.menu.message_header_overflow_menu,
    430                         menu);
    431 
    432                 // Remove Reply if ReplyAll icon is visible or vice versa
    433                 menu.removeItem(mDefaultReplyAll ? R.id.reply_all : R.id.reply);
    434                 popup.setOnMenuItemClickListener(this);
    435                 popup.show();
    436                 break;
    437             }
    438 
    439         }
    440         super.onClick(view);
    441     }
    442 
    443     @Override
    444     public boolean onMenuItemClick(MenuItem item) {
    445         if (isMessageOpen()) {
    446             switch (item.getItemId()) {
    447                 case R.id.reply:
    448                     mCallback.onReply();
    449                     return true;
    450                 case R.id.reply_all:
    451                     mCallback.onReplyAll();
    452                     return true;
    453                 case R.id.forward:
    454                     mCallback.onForward();
    455                     return true;
    456             }
    457         }
    458         return false;
    459     }
    460 
    461 
    462     @Override
    463     public boolean onOptionsItemSelected(MenuItem item) {
    464         switch (item.getItemId()) {
    465             case R.id.move:
    466                 onMove();
    467                 return true;
    468             case R.id.delete:
    469                 onDelete();
    470                 return true;
    471             case R.id.mark_as_unread:
    472                 onMarkAsUnread();
    473                 return true;
    474         }
    475         return super.onOptionsItemSelected(item);
    476     }
    477 
    478     private void onMove() {
    479         MoveMessageToDialog dialog = MoveMessageToDialog.newInstance(new long[] {getMessageId()},
    480                 this);
    481         dialog.show(getFragmentManager(), "dialog");
    482     }
    483 
    484     // MoveMessageToDialog$Callback
    485     @Override
    486     public void onMoveToMailboxSelected(long newMailboxId, long[] messageIds) {
    487         mCallback.onBeforeMessageGone();
    488         ActivityHelper.moveMessages(mContext, newMailboxId, messageIds);
    489     }
    490 
    491     private void onDelete() {
    492         mCallback.onBeforeMessageGone();
    493         ActivityHelper.deleteMessage(mContext, getMessageId());
    494     }
    495 
    496     private void onMarkAsUnread() {
    497         onMarkMessageAsRead(false);
    498     }
    499 
    500     /**
    501      * {@inheritDoc}
    502      *
    503      * Mark the current as unread.
    504      */
    505     @Override
    506     protected void onPostLoadBody() {
    507         onMarkMessageAsRead(true);
    508 
    509         // Initialize star content description for accessibility
    510         Message message = getMessage();
    511         setStarContentDescription(message.mFlagFavorite);
    512     }
    513 
    514     @Override
    515     protected void updateHeaderView(Message message) {
    516         super.updateHeaderView(message);
    517 
    518         mFavoriteIcon.setImageDrawable(message.mFlagFavorite ? mFavoriteIconOn : mFavoriteIconOff);
    519 
    520         // Enable the invite tab if necessary
    521         if ((message.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) {
    522             addTabFlags(TAB_FLAGS_HAS_INVITE);
    523         }
    524     }
    525 }
    526