MessageViewFragment.java revision a826d3fb03f29a07ea12e44237b2c02ea1926c74
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.email.Utility; 22import com.android.email.mail.MeetingInfo; 23import com.android.email.mail.PackedString; 24import com.android.email.provider.EmailContent.Mailbox; 25import com.android.email.provider.EmailContent.Message; 26import com.android.email.service.EmailServiceConstants; 27 28import android.app.Activity; 29import android.content.res.Resources; 30import android.graphics.drawable.Drawable; 31import android.os.Bundle; 32import android.util.Log; 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 43import java.security.InvalidParameterException; 44 45/** 46 * A {@link MessageViewFragmentBase} subclass for regular email messages. (regular as in "not eml 47 * files"). 48 * 49 * See {@link MessageViewBase} for the class relation diagram. 50 */ 51public class MessageViewFragment extends MessageViewFragmentBase 52 implements CheckBox.OnCheckedChangeListener { 53 private ImageView mFavoriteIcon; 54 private View mInviteSection; 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 MessageCommandButtonView mCommandButtons; 65 private int mPreviousMeetingResponse = -1; 66 67 private Drawable mFavoriteIconOn; 68 private Drawable mFavoriteIconOff; 69 70 /** 71 * ID of the message that will be loaded. Protect with {@link #mLock}. 72 */ 73 private long mMessageIdToOpen = -1; 74 75 /** Lock object to protect {@link #mMessageIdToOpen} */ 76 private final Object mLock = new Object(); 77 78 /** ID of the currently shown message */ 79 private long mCurrentMessageId = -1; 80 81 private final CommandButtonCallback mCommandButtonCallback = new CommandButtonCallback(); 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 /** Called when "move to newer" button is pressed. */ 110 public void onMoveToNewer(); 111 112 /** Called when "move to older" button is pressed. */ 113 public void onMoveToOlder(); 114 115 /** 116 * Called right before the current message will be deleted. 117 * Callees don't have to delete messages. The fragment does. 118 */ 119 public void onBeforeMessageDelete(); 120 121 /** Called when the move button is pressed. */ 122 public void onMoveMessage(); 123 /** Called when the forward button is pressed. */ 124 public void onForward(); 125 /** Called when the reply button is pressed. */ 126 public void onReply(); 127 /** Called when the reply-all button is pressed. */ 128 public void onReplyAll(); 129 } 130 131 public static final class EmptyCallback extends MessageViewFragmentBase.EmptyCallback 132 implements Callback { 133 public static final Callback INSTANCE = new EmptyCallback(); 134 135 @Override public void onCalendarLinkClicked(long epochEventStartTime) { } 136 @Override public void onMessageSetUnread() { } 137 @Override public void onRespondedToInvite(int response) { } 138 @Override public void onMoveToNewer() { } 139 @Override public void onMoveToOlder() { } 140 @Override public void onBeforeMessageDelete() { } 141 @Override public void onMoveMessage() { } 142 @Override public void onForward() { } 143 @Override public void onReply() { } 144 @Override public void onReplyAll() { } 145 } 146 147 private Callback mCallback = EmptyCallback.INSTANCE; 148 149 @Override 150 public void onCreate(Bundle savedInstanceState) { 151 super.onCreate(savedInstanceState); 152 153 final Resources res = getActivity().getResources(); 154 mFavoriteIconOn = res.getDrawable(R.drawable.btn_star_big_buttonless_on); 155 mFavoriteIconOff = res.getDrawable(R.drawable.ic_star_none_holo_light); 156 } 157 158 @Override 159 public View onCreateView( 160 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 161 final View view = super.onCreateView(inflater, container, savedInstanceState); 162 163 mFavoriteIcon = (ImageView) view.findViewById(R.id.favorite); 164 mInviteSection = view.findViewById(R.id.invite_section); 165 mReplyButton = view.findViewById(R.id.reply); 166 mReplyAllButton = view.findViewById(R.id.reply_all); 167 mForwardButton = view.findViewById(R.id.forward); 168 mMeetingYes = (CheckBox) view.findViewById(R.id.accept); 169 mMeetingMaybe = (CheckBox) view.findViewById(R.id.maybe); 170 mMeetingNo = (CheckBox) view.findViewById(R.id.decline); 171 172 // Star is only visible on this fragment (as opposed to MessageFileViewFragment.) 173 view.findViewById(R.id.favorite).setVisibility(View.VISIBLE); 174 175 mFavoriteIcon.setOnClickListener(this); 176 mReplyButton.setOnClickListener(this); 177 mReplyAllButton.setOnClickListener(this); 178 mForwardButton.setOnClickListener(this); 179 mMeetingYes.setOnCheckedChangeListener(this); 180 mMeetingMaybe.setOnCheckedChangeListener(this); 181 mMeetingNo.setOnCheckedChangeListener(this); 182 view.findViewById(R.id.invite_link).setOnClickListener(this); 183 184 // Show the command buttons at the bottom. 185 mCommandButtons = 186 (MessageCommandButtonView) view.findViewById(R.id.message_command_buttons); 187 mCommandButtons.setVisibility(View.VISIBLE); 188 mCommandButtons.setCallback(mCommandButtonCallback); 189 190 enableReplyForwardButtons(false); 191 192 return view; 193 } 194 195 @Override 196 public void onResume() { 197 super.onResume(); 198 } 199 200 @Override 201 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 202 inflater.inflate(R.menu.message_view_fragment_option, menu); 203 } 204 205 private void enableReplyForwardButtons(boolean enabled) { 206 // We don't have disabled button assets, so let's hide them for now 207 final int visibility = enabled ? View.VISIBLE : View.GONE; 208 mReplyButton.setVisibility(visibility); 209 mReplyAllButton.setVisibility(visibility); 210 mForwardButton.setVisibility(visibility); 211 } 212 213 public void setCallback(Callback callback) { 214 mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback; 215 super.setCallback(mCallback); 216 } 217 218 /** Called by activities to set an id of a message to open. */ 219 public void openMessage(long messageId) { 220 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 221 Log.d(Email.LOG_TAG, "MessageViewFragment openMessage"); 222 } 223 if (messageId == -1) { 224 throw new InvalidParameterException(); 225 } 226 synchronized (mLock) { 227 mMessageIdToOpen = messageId; 228 } 229 loadMessageIfResumed(); 230 } 231 232 @Override 233 public void clearContent() { 234 synchronized (mLock) { 235 super.clearContent(); 236 mMessageIdToOpen = -1; 237 238 // Hide the menu. 239 // This isn't really necessary if we're really hiding the fragment. However, 240 // for now, we're actually *not* hiding the fragment (just hiding the root view of it), 241 // so need to remove menus manually. 242 setHasOptionsMenu(false); 243 } 244 } 245 246 @Override 247 protected void resetView() { 248 super.resetView(); 249 mMeetingYes.setChecked(false); 250 mMeetingNo.setChecked(false); 251 mMeetingMaybe.setChecked(false); 252 } 253 254 @Override 255 protected boolean isMessageSpecified() { 256 synchronized (mLock) { 257 return mMessageIdToOpen != -1; 258 } 259 } 260 261 /** 262 * NOTE See the comment on the super method. It's called on a worker thread. 263 */ 264 @Override 265 protected Message openMessageSync(Activity activity) { 266 synchronized (mLock) { 267 long messageId = mMessageIdToOpen; 268 if (messageId < 0) { 269 return null; // Called after clearContent(). 270 } 271 return Message.restoreMessageWithId(activity, messageId); 272 } 273 } 274 275 @Override 276 protected void onMessageShown(long messageId, int mailboxType) { 277 super.onMessageShown(messageId, mailboxType); 278 279 // Remember the currently shown message ID. 280 mCurrentMessageId = messageId; 281 282 // Disable forward/reply buttons as necessary. 283 enableReplyForwardButtons(Mailbox.isMailboxTypeReplyAndForwardable(mailboxType)); 284 285 // Show the menu when it's showing a message. 286 setHasOptionsMenu(true); 287 } 288 289 public void enableNavigationButons(boolean enableMoveToNewer, boolean enableMoveToOlder, 290 int currentPosition, int countMessages) { 291 mCommandButtons.enableNavigationButtons(enableMoveToNewer, enableMoveToOlder, 292 currentPosition, countMessages); 293 } 294 295 /** 296 * Toggle favorite status and write back to provider 297 */ 298 private void onClickFavorite() { 299 Message message = getMessage(); 300 301 // Update UI 302 boolean newFavorite = ! message.mFlagFavorite; 303 mFavoriteIcon.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff); 304 305 // Update provider 306 message.mFlagFavorite = newFavorite; 307 getController().setMessageFavorite(message.mId, newFavorite); 308 } 309 310 /** 311 * Set message read/unread. 312 */ 313 public void onMarkMessageAsRead(boolean isRead) { 314 Message message = getMessage(); 315 if (message.mFlagRead != isRead) { 316 message.mFlagRead = isRead; 317 getController().setMessageRead(message.mId, isRead); 318 if (!isRead) { // Became unread. We need to close the message. 319 mCallback.onMessageSetUnread(); 320 } 321 } 322 } 323 324 /** 325 * Send a service message indicating that a meeting invite button has been clicked. 326 */ 327 private void onRespondToInvite(int response, int toastResId) { 328 Message message = getMessage(); 329 // do not send twice in a row the same response 330 if (mPreviousMeetingResponse != response) { 331 getController().sendMeetingResponse(message.mId, response); 332 mPreviousMeetingResponse = response; 333 } 334 Utility.showToast(getActivity(), toastResId); 335 mCallback.onRespondedToInvite(response); 336 } 337 338 private void onInviteLinkClicked() { 339 Message message = getMessage(); 340 String startTime = new PackedString(message.mMeetingInfo).get(MeetingInfo.MEETING_DTSTART); 341 if (startTime != null) { 342 long epochTimeMillis = Utility.parseEmailDateTimeToMillis(startTime); 343 mCallback.onCalendarLinkClicked(epochTimeMillis); 344 } else { 345 Email.log("meetingInfo without DTSTART " + message.mMeetingInfo); 346 } 347 } 348 349 @Override 350 public void onClick(View view) { 351 if (!isMessageOpen()) { 352 return; // Ignore. 353 } 354 switch (view.getId()) { 355 case R.id.reply: 356 mCallback.onReply(); 357 return; 358 case R.id.reply_all: 359 mCallback.onReplyAll(); 360 return; 361 case R.id.forward: 362 mCallback.onForward(); 363 return; 364 365 case R.id.favorite: 366 onClickFavorite(); 367 return; 368 369 case R.id.invite_link: 370 onInviteLinkClicked(); 371 return; 372 } 373 super.onClick(view); 374 } 375 376 @Override 377 public void onCheckedChanged(CompoundButton view, boolean isChecked) { 378 if (!isChecked) return; 379 switch (view.getId()) { 380 case R.id.accept: 381 onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_ACCEPTED, 382 R.string.message_view_invite_toast_yes); 383 return; 384 case R.id.maybe: 385 onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_TENTATIVE, 386 R.string.message_view_invite_toast_maybe); 387 return; 388 case R.id.decline: 389 onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_DECLINED, 390 R.string.message_view_invite_toast_no); 391 return; 392 } 393 } 394 395 @Override 396 public boolean onOptionsItemSelected(MenuItem item) { 397 switch (item.getItemId()) { 398 case R.id.move: 399 onMove(); 400 return true; 401 case R.id.delete: 402 onDelete(); 403 return true; 404 case R.id.mark_as_unread: 405 onMarkAsUnread(); 406 return true; 407 } 408 return super.onOptionsItemSelected(item); 409 } 410 411 private void onMove() { 412 mCallback.onMoveMessage(); 413 } 414 415 private void onDelete() { 416 mCallback.onBeforeMessageDelete(); 417 ActivityHelper.deleteMessage(getActivity(), mCurrentMessageId); 418 } 419 420 private void onMarkAsUnread() { 421 onMarkMessageAsRead(false); 422 } 423 424 /** 425 * {@inheritDoc} 426 * 427 * Mark the current as unread. 428 */ 429 @Override 430 protected void onPostLoadBody() { 431 onMarkMessageAsRead(true); 432 } 433 434 @Override 435 protected void updateHeaderView(Message message) { 436 super.updateHeaderView(message); 437 438 mFavoriteIcon.setImageDrawable(message.mFlagFavorite ? mFavoriteIconOn : mFavoriteIconOff); 439 440 // Enable the invite tab if necessary 441 if ((message.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) { 442 addTabFlags(TAB_FLAGS_HAS_INVITE); 443 } 444 } 445 446 private class CommandButtonCallback implements MessageCommandButtonView.Callback { 447 @Override 448 public void onMoveToNewer() { 449 mCallback.onMoveToNewer(); 450 } 451 452 @Override 453 public void onMoveToOlder() { 454 mCallback.onMoveToOlder(); 455 } 456 } 457} 458