MessageViewFragment.java revision f5418f1f93b02e7fab9f15eb201800b65510998e
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.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 setHasOptionsMenu(true); 208 209 final Resources res = getActivity().getResources(); 210 mFavoriteIconOn = res.getDrawable(R.drawable.btn_star_on_normal_email_holo_light); 211 mFavoriteIconOff = res.getDrawable(R.drawable.btn_star_off_normal_email_holo_light); 212 } 213 214 @Override 215 public View onCreateView( 216 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 217 final View view = super.onCreateView(inflater, container, savedInstanceState); 218 219 mFavoriteIcon = (ImageView) UiUtilities.getView(view, R.id.favorite); 220 mReplyButton = UiUtilities.getView(view, R.id.reply); 221 mReplyAllButton = UiUtilities.getView(view, R.id.reply_all); 222 mForwardButton = UiUtilities.getView(view, R.id.forward); 223 mMeetingYes = (CheckBox) UiUtilities.getView(view, R.id.accept); 224 mMeetingMaybe = (CheckBox) UiUtilities.getView(view, R.id.maybe); 225 mMeetingNo = (CheckBox) UiUtilities.getView(view, R.id.decline); 226 227 // Star is only visible on this fragment (as opposed to MessageFileViewFragment.) 228 UiUtilities.getView(view, R.id.favorite).setVisibility(View.VISIBLE); 229 230 mFavoriteIcon.setOnClickListener(this); 231 mReplyButton.setOnClickListener(this); 232 mReplyAllButton.setOnClickListener(this); 233 mForwardButton.setOnClickListener(this); 234 mMeetingYes.setOnCheckedChangeListener(this); 235 mMeetingMaybe.setOnCheckedChangeListener(this); 236 mMeetingNo.setOnCheckedChangeListener(this); 237 UiUtilities.getView(view, R.id.invite_link).setOnClickListener(this); 238 239 enableReplyForwardButtons(false); 240 241 return view; 242 } 243 244 @Override 245 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 246 inflater.inflate(R.menu.message_view_fragment_option, menu); 247 } 248 249 private void enableReplyForwardButtons(boolean enabled) { 250 // We don't have disabled button assets, so let's hide them for now 251 final int visibility = enabled ? View.VISIBLE : View.GONE; 252 mReplyButton.setVisibility(visibility); 253 mReplyAllButton.setVisibility(visibility); 254 mForwardButton.setVisibility(visibility); 255 } 256 257 public void setCallback(Callback callback) { 258 mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback; 259 super.setCallback(mCallback); 260 } 261 262 @Override 263 protected void resetView() { 264 super.resetView(); 265 mMeetingYes.setChecked(false); 266 mMeetingNo.setChecked(false); 267 mMeetingMaybe.setChecked(false); 268 mPreviousMeetingResponse = EmailServiceConstants.MEETING_REQUEST_NOT_RESPONDED; 269 } 270 271 /** 272 * NOTE See the comment on the super method. It's called on a worker thread. 273 */ 274 @Override 275 protected Message openMessageSync(Activity activity) { 276 return Message.restoreMessageWithId(activity, getMessageId()); 277 } 278 279 @Override 280 protected void onMessageShown(long messageId, int mailboxType) { 281 super.onMessageShown(messageId, mailboxType); 282 283 // Disable forward/reply buttons as necessary. 284 enableReplyForwardButtons(Mailbox.isMailboxTypeReplyAndForwardable(mailboxType)); 285 } 286 287 /** 288 * Toggle favorite status and write back to provider 289 */ 290 private void onClickFavorite() { 291 if (!isMessageOpen()) return; 292 Message message = getMessage(); 293 294 // Update UI 295 boolean newFavorite = ! message.mFlagFavorite; 296 mFavoriteIcon.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff); 297 298 // Update provider 299 message.mFlagFavorite = newFavorite; 300 getController().setMessageFavorite(message.mId, newFavorite); 301 } 302 303 /** 304 * Set message read/unread. 305 */ 306 public void onMarkMessageAsRead(boolean isRead) { 307 if (!isMessageOpen()) return; 308 Message message = getMessage(); 309 if (message.mFlagRead != isRead) { 310 message.mFlagRead = isRead; 311 getController().setMessageRead(message.mId, isRead); 312 if (!isRead) { // Became unread. We need to close the message. 313 mCallback.onMessageSetUnread(); 314 } 315 } 316 } 317 318 /** 319 * Send a service message indicating that a meeting invite button has been clicked. 320 */ 321 private void onRespondToInvite(int response, int toastResId) { 322 if (!isMessageOpen()) return; 323 Message message = getMessage(); 324 // do not send twice in a row the same response 325 if (mPreviousMeetingResponse != response) { 326 getController().sendMeetingResponse(message.mId, response); 327 mPreviousMeetingResponse = response; 328 } 329 Utility.showToast(getActivity(), toastResId); 330 mCallback.onRespondedToInvite(response); 331 } 332 333 private void onInviteLinkClicked() { 334 if (!isMessageOpen()) return; 335 Message message = getMessage(); 336 String startTime = new PackedString(message.mMeetingInfo).get(MeetingInfo.MEETING_DTSTART); 337 if (startTime != null) { 338 long epochTimeMillis = Utility.parseEmailDateTimeToMillis(startTime); 339 mCallback.onCalendarLinkClicked(epochTimeMillis); 340 } else { 341 Email.log("meetingInfo without DTSTART " + message.mMeetingInfo); 342 } 343 } 344 345 @Override 346 public void onClick(View view) { 347 if (!isMessageOpen()) { 348 return; // Ignore. 349 } 350 switch (view.getId()) { 351 case R.id.reply: 352 mCallback.onReply(); 353 return; 354 case R.id.reply_all: 355 mCallback.onReplyAll(); 356 return; 357 case R.id.forward: 358 mCallback.onForward(); 359 return; 360 361 case R.id.favorite: 362 onClickFavorite(); 363 return; 364 365 case R.id.invite_link: 366 onInviteLinkClicked(); 367 return; 368 } 369 super.onClick(view); 370 } 371 372 @Override 373 public void onCheckedChanged(CompoundButton view, boolean isChecked) { 374 if (!isChecked) return; 375 switch (view.getId()) { 376 case R.id.accept: 377 onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_ACCEPTED, 378 R.string.message_view_invite_toast_yes); 379 return; 380 case R.id.maybe: 381 onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_TENTATIVE, 382 R.string.message_view_invite_toast_maybe); 383 return; 384 case R.id.decline: 385 onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_DECLINED, 386 R.string.message_view_invite_toast_no); 387 return; 388 } 389 } 390 391 @Override 392 public boolean onOptionsItemSelected(MenuItem item) { 393 switch (item.getItemId()) { 394 case R.id.move: 395 onMove(); 396 return true; 397 case R.id.delete: 398 onDelete(); 399 return true; 400 case R.id.mark_as_unread: 401 onMarkAsUnread(); 402 return true; 403 } 404 return super.onOptionsItemSelected(item); 405 } 406 407 private void onMove() { 408 MoveMessageToDialog dialog = MoveMessageToDialog.newInstance(new long[] {getMessageId()}, 409 this); 410 dialog.show(getFragmentManager(), "dialog"); 411 } 412 413 // MoveMessageToDialog$Callback 414 @Override 415 public void onMoveToMailboxSelected(long newMailboxId, long[] messageIds) { 416 mCallback.onBeforeMessageGone(); 417 ActivityHelper.moveMessages(mContext, newMailboxId, messageIds); 418 } 419 420 private void onDelete() { 421 mCallback.onBeforeMessageGone(); 422 ActivityHelper.deleteMessage(mContext, getMessageId()); 423 } 424 425 private void onMarkAsUnread() { 426 onMarkMessageAsRead(false); 427 } 428 429 /** 430 * {@inheritDoc} 431 * 432 * Mark the current as unread. 433 */ 434 @Override 435 protected void onPostLoadBody() { 436 onMarkMessageAsRead(true); 437 } 438 439 @Override 440 protected void updateHeaderView(Message message) { 441 super.updateHeaderView(message); 442 443 mFavoriteIcon.setImageDrawable(message.mFlagFavorite ? mFavoriteIconOn : mFavoriteIconOff); 444 445 // Enable the invite tab if necessary 446 if ((message.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) { 447 addTabFlags(TAB_FLAGS_HAS_INVITE); 448 } 449 } 450} 451