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 onPrepareOptionsMenu(Menu menu) {
    243         MenuItem move = menu.findItem(R.id.move);
    244         if (move != null) {
    245             menu.findItem(R.id.move).setVisible(mSupportsMove);
    246         }
    247     }
    248 
    249     private void enableReplyForwardButtons(boolean enabled) {
    250         mEnableReplyForwardButtons = enabled;
    251         // We don't have disabled button assets, so let's hide them for now
    252         final int visibility = enabled ? View.VISIBLE : View.GONE;
    253 
    254         // Modify Reply All button only if there's no overflow OR there is
    255         // overflow but default is to show the Reply All button
    256         if (mMoreButton == null || mDefaultReplyAll) {
    257             UiUtilities.setVisibilitySafe(mReplyAllButton, visibility);
    258         }
    259 
    260         // Modify Reply button only if there's no overflow OR there is
    261         // overflow but default is to show the Reply button
    262         if (mMoreButton == null || !mDefaultReplyAll) {
    263                UiUtilities.setVisibilitySafe(mReplyButton, visibility);
    264         }
    265 
    266         if (mForwardButton != null) {
    267             mForwardButton.setVisibility(visibility);
    268         }
    269         if (mMoreButton != null) {
    270             mMoreButton.setVisibility(visibility);
    271         }
    272     }
    273 
    274     public void setCallback(Callback callback) {
    275         mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
    276         super.setCallback(mCallback);
    277     }
    278 
    279     @Override
    280     protected void resetView() {
    281         super.resetView();
    282         mPreviousMeetingResponse = EmailServiceConstants.MEETING_REQUEST_NOT_RESPONDED;
    283     }
    284 
    285     /**
    286      * NOTE See the comment on the super method.  It's called on a worker thread.
    287      */
    288     @Override
    289     protected Message openMessageSync(Activity activity) {
    290         return Message.restoreMessageWithId(activity, getMessageId());
    291     }
    292 
    293     @Override
    294     protected void onMessageShown(long messageId, Mailbox mailbox) {
    295         super.onMessageShown(messageId, mailbox);
    296 
    297         Account account = Account.restoreAccountWithId(mContext, getAccountId());
    298         boolean supportsMove = account.supportsMoveMessages(mContext)
    299                 && mailbox.canHaveMessagesMoved();
    300         if (mSupportsMove != supportsMove) {
    301             mSupportsMove = supportsMove;
    302             Activity host = getActivity();
    303             if (host != null) {
    304                 host.invalidateOptionsMenu();
    305             }
    306         }
    307 
    308         // Disable forward/reply buttons as necessary.
    309         enableReplyForwardButtons(Mailbox.isMailboxTypeReplyAndForwardable(mailbox.mType));
    310     }
    311 
    312     /**
    313      * Sets the content description for the star icon based on whether it's currently starred.
    314      */
    315     private void setStarContentDescription(boolean isFavorite) {
    316         if (isFavorite) {
    317             mFavoriteIcon.setContentDescription(
    318                     mContext.getResources().getString(R.string.remove_star_action));
    319         } else {
    320             mFavoriteIcon.setContentDescription(
    321                     mContext.getResources().getString(R.string.set_star_action));
    322         }
    323     }
    324 
    325     /**
    326      * Toggle favorite status and write back to provider
    327      */
    328     private void onClickFavorite() {
    329         if (!isMessageOpen()) return;
    330         Message message = getMessage();
    331 
    332         // Update UI
    333         boolean newFavorite = ! message.mFlagFavorite;
    334         mFavoriteIcon.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff);
    335 
    336         // Handle accessibility event
    337         setStarContentDescription(newFavorite);
    338         mFavoriteIcon.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
    339 
    340         // Update provider
    341         message.mFlagFavorite = newFavorite;
    342         getController().setMessageFavorite(message.mId, newFavorite);
    343     }
    344 
    345     /**
    346      * Set message read/unread.
    347      */
    348     public void onMarkMessageAsRead(boolean isRead) {
    349         if (!isMessageOpen()) return;
    350         Message message = getMessage();
    351         if (message.mFlagRead != isRead) {
    352             message.mFlagRead = isRead;
    353             getController().setMessageRead(message.mId, isRead);
    354             if (!isRead) { // Became unread.  We need to close the message.
    355                 mCallback.onMessageSetUnread();
    356             }
    357         }
    358     }
    359 
    360     /**
    361      * Send a service message indicating that a meeting invite button has been clicked.
    362      */
    363     private void onRespondToInvite(int response, int toastResId) {
    364         if (!isMessageOpen()) return;
    365         Message message = getMessage();
    366         // do not send twice in a row the same response
    367         if (mPreviousMeetingResponse != response) {
    368             getController().sendMeetingResponse(message.mId, response);
    369             mPreviousMeetingResponse = response;
    370         }
    371         Utility.showToast(getActivity(), toastResId);
    372         mCallback.onRespondedToInvite(response);
    373     }
    374 
    375     private void onInviteLinkClicked() {
    376         if (!isMessageOpen()) return;
    377         Message message = getMessage();
    378         String startTime = new PackedString(message.mMeetingInfo).get(MeetingInfo.MEETING_DTSTART);
    379         if (startTime != null) {
    380             long epochTimeMillis = Utility.parseEmailDateTimeToMillis(startTime);
    381             mCallback.onCalendarLinkClicked(epochTimeMillis);
    382         } else {
    383             Email.log("meetingInfo without DTSTART " + message.mMeetingInfo);
    384         }
    385     }
    386 
    387     @Override
    388     public void onClick(View view) {
    389         if (!isMessageOpen()) {
    390             return; // Ignore.
    391         }
    392         switch (view.getId()) {
    393             case R.id.reply:
    394                 mCallback.onReply();
    395                 return;
    396             case R.id.reply_all:
    397                 mCallback.onReplyAll();
    398                 return;
    399             case R.id.forward:
    400                 mCallback.onForward();
    401                 return;
    402 
    403             case R.id.favorite:
    404                 onClickFavorite();
    405                 return;
    406 
    407             case R.id.invite_link:
    408                 onInviteLinkClicked();
    409                 return;
    410 
    411             case R.id.accept:
    412                 onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_ACCEPTED,
    413                         R.string.message_view_invite_toast_yes);
    414                 return;
    415             case R.id.maybe:
    416                 onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_TENTATIVE,
    417                         R.string.message_view_invite_toast_maybe);
    418                 return;
    419             case R.id.decline:
    420                 onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_DECLINED,
    421                         R.string.message_view_invite_toast_no);
    422                 return;
    423 
    424             case R.id.more: {
    425                 PopupMenu popup = new PopupMenu(getActivity(), mMoreButton);
    426                 Menu menu = popup.getMenu();
    427                 popup.getMenuInflater().inflate(R.menu.message_header_overflow_menu,
    428                         menu);
    429 
    430                 // Remove Reply if ReplyAll icon is visible or vice versa
    431                 menu.removeItem(mDefaultReplyAll ? R.id.reply_all : R.id.reply);
    432                 popup.setOnMenuItemClickListener(this);
    433                 popup.show();
    434                 break;
    435             }
    436 
    437         }
    438         super.onClick(view);
    439     }
    440 
    441     @Override
    442     public boolean onMenuItemClick(MenuItem item) {
    443         if (isMessageOpen()) {
    444             switch (item.getItemId()) {
    445                 case R.id.reply:
    446                     mCallback.onReply();
    447                     return true;
    448                 case R.id.reply_all:
    449                     mCallback.onReplyAll();
    450                     return true;
    451                 case R.id.forward:
    452                     mCallback.onForward();
    453                     return true;
    454             }
    455         }
    456         return false;
    457     }
    458 
    459 
    460     @Override
    461     public boolean onOptionsItemSelected(MenuItem item) {
    462         switch (item.getItemId()) {
    463             case R.id.move:
    464                 onMove();
    465                 return true;
    466             case R.id.delete:
    467                 onDelete();
    468                 return true;
    469             case R.id.mark_as_unread:
    470                 onMarkAsUnread();
    471                 return true;
    472         }
    473         return super.onOptionsItemSelected(item);
    474     }
    475 
    476     private void onMove() {
    477         MoveMessageToDialog dialog = MoveMessageToDialog.newInstance(new long[] {getMessageId()},
    478                 this);
    479         dialog.show(getFragmentManager(), "dialog");
    480     }
    481 
    482     // MoveMessageToDialog$Callback
    483     @Override
    484     public void onMoveToMailboxSelected(long newMailboxId, long[] messageIds) {
    485         mCallback.onBeforeMessageGone();
    486         ActivityHelper.moveMessages(mContext, newMailboxId, messageIds);
    487     }
    488 
    489     private void onDelete() {
    490         mCallback.onBeforeMessageGone();
    491         ActivityHelper.deleteMessage(mContext, getMessageId());
    492     }
    493 
    494     private void onMarkAsUnread() {
    495         onMarkMessageAsRead(false);
    496     }
    497 
    498     /**
    499      * {@inheritDoc}
    500      *
    501      * Mark the current as unread.
    502      */
    503     @Override
    504     protected void onPostLoadBody() {
    505         onMarkMessageAsRead(true);
    506 
    507         // Initialize star content description for accessibility
    508         Message message = getMessage();
    509         setStarContentDescription(message.mFlagFavorite);
    510     }
    511 
    512     @Override
    513     protected void updateHeaderView(Message message) {
    514         super.updateHeaderView(message);
    515 
    516         mFavoriteIcon.setImageDrawable(message.mFlagFavorite ? mFavoriteIconOn : mFavoriteIconOff);
    517 
    518         // Enable the invite tab if necessary
    519         if ((message.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) {
    520             addTabFlags(TAB_FLAGS_HAS_INVITE);
    521         }
    522     }
    523 }
    524