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