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