• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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. */
onCalendarLinkClicked(long epochEventStartTime)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          */
onRespondedToInvite(int response)104         public void onRespondedToInvite(int response);
105 
106         /** Called when the current message is set unread. */
onMessageSetUnread()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          */
onBeforeMessageGone()114         public void onBeforeMessageGone();
115 
116         /** Called when the forward button is pressed. */
onForward()117         public void onForward();
118         /** Called when the reply button is pressed. */
onReply()119         public void onReply();
120         /** Called when the reply-all button is pressed. */
onReplyAll()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 
onCalendarLinkClicked(long epochEventStartTime)129         @Override public void onCalendarLinkClicked(long epochEventStartTime) { }
onMessageSetUnread()130         @Override public void onMessageSetUnread() { }
onRespondedToInvite(int response)131         @Override public void onRespondedToInvite(int response) { }
onBeforeMessageGone()132         @Override public void onBeforeMessageGone() { }
onForward()133         @Override public void onForward() { }
onReply()134         @Override public void onReply() { }
onReplyAll()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      */
newInstance(long messageId)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 
initializeArgCache()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      */
getMessageId()174     public long getMessageId() {
175         initializeArgCache();
176         return mImmutableMessageId;
177     }
178 
179     @Override
onCreate(Bundle savedInstanceState)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
onResume()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
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onPrepareOptionsMenu(Menu menu)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 
enableReplyForwardButtons(boolean enabled)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 
setCallback(Callback callback)274     public void setCallback(Callback callback) {
275         mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
276         super.setCallback(mCallback);
277     }
278 
279     @Override
resetView()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
openMessageSync(Activity activity)289     protected Message openMessageSync(Activity activity) {
290         return Message.restoreMessageWithId(activity, getMessageId());
291     }
292 
293     @Override
onMessageShown(long messageId, Mailbox mailbox)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      */
setStarContentDescription(boolean isFavorite)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      */
onClickFavorite()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      */
onMarkMessageAsRead(boolean isRead)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      */
onRespondToInvite(int response, int toastResId)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 
onInviteLinkClicked()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
onClick(View view)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
onMenuItemClick(MenuItem item)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
onOptionsItemSelected(MenuItem item)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 
onMove()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
onMoveToMailboxSelected(long newMailboxId, long[] messageIds)484     public void onMoveToMailboxSelected(long newMailboxId, long[] messageIds) {
485         mCallback.onBeforeMessageGone();
486         ActivityHelper.moveMessages(mContext, newMailboxId, messageIds);
487     }
488 
onDelete()489     private void onDelete() {
490         mCallback.onBeforeMessageGone();
491         ActivityHelper.deleteMessage(mContext, getMessageId());
492     }
493 
onMarkAsUnread()494     private void onMarkAsUnread() {
495         onMarkMessageAsRead(false);
496     }
497 
498     /**
499      * {@inheritDoc}
500      *
501      * Mark the current as unread.
502      */
503     @Override
onPostLoadBody()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
updateHeaderView(Message message)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