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