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