MessageViewFragment.java revision 53ea83ebf91f820692e8fa8e781f5cc982dd94db
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.Logging; 22import com.android.emailcommon.mail.MeetingInfo; 23import com.android.emailcommon.mail.PackedString; 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.util.Log; 34import android.view.LayoutInflater; 35import android.view.Menu; 36import android.view.MenuInflater; 37import android.view.MenuItem; 38import android.view.View; 39import android.view.ViewGroup; 40import android.widget.CheckBox; 41import android.widget.CompoundButton; 42import android.widget.ImageView; 43 44import java.security.InvalidParameterException; 45 46/** 47 * A {@link MessageViewFragmentBase} subclass for regular email messages. (regular as in "not eml 48 * files"). 49 * 50 * See {@link MessageViewBase} for the class relation diagram. 51 */ 52public class MessageViewFragment extends MessageViewFragmentBase 53 implements CheckBox.OnCheckedChangeListener, MoveMessageToDialog.Callback { 54 /** Argument name(s) */ 55 private static final String ARG_MESSAGE_ID = "messageId"; 56 57 private ImageView mFavoriteIcon; 58 59 private View mReplyButton; 60 private View mReplyAllButton; 61 private View mForwardButton; 62 63 // calendar meeting invite answers 64 private CheckBox mMeetingYes; 65 private CheckBox mMeetingMaybe; 66 private CheckBox mMeetingNo; 67 private Drawable mFavoriteIconOn; 68 private Drawable mFavoriteIconOff; 69 70 private int mPreviousMeetingResponse = EmailServiceConstants.MEETING_REQUEST_NOT_RESPONDED; 71 72 /** 73 * ID of the message that will be loaded. Protect with {@link #mLock}. 74 */ 75 private long mMessageIdToOpen = -1; 76 77 /** Lock object to protect {@link #mMessageIdToOpen} */ 78 private final Object mLock = new Object(); 79 80 /** ID of the currently shown message */ 81 private long mCurrentMessageId = -1; 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 public static MessageViewFragment newInstance(long messageId) { 146 final MessageViewFragment instance = new MessageViewFragment(); 147 final Bundle args = new Bundle(); 148 args.putLong(ARG_MESSAGE_ID, messageId); 149 instance.setArguments(args); 150 return instance; 151 } 152 153 @Override 154 public void onCreate(Bundle savedInstanceState) { 155 super.onCreate(savedInstanceState); 156 157 final Resources res = getActivity().getResources(); 158 mFavoriteIconOn = res.getDrawable(R.drawable.btn_star_on_normal_email_holo_light); 159 mFavoriteIconOff = res.getDrawable(R.drawable.btn_star_off_normal_email_holo_light); 160 } 161 162 @Override 163 public View onCreateView( 164 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 165 final View view = super.onCreateView(inflater, container, savedInstanceState); 166 167 mFavoriteIcon = (ImageView) UiUtilities.getView(view, R.id.favorite); 168 mReplyButton = UiUtilities.getView(view, R.id.reply); 169 mReplyAllButton = UiUtilities.getView(view, R.id.reply_all); 170 mForwardButton = UiUtilities.getView(view, R.id.forward); 171 mMeetingYes = (CheckBox) UiUtilities.getView(view, R.id.accept); 172 mMeetingMaybe = (CheckBox) UiUtilities.getView(view, R.id.maybe); 173 mMeetingNo = (CheckBox) UiUtilities.getView(view, R.id.decline); 174 175 // Star is only visible on this fragment (as opposed to MessageFileViewFragment.) 176 UiUtilities.getView(view, R.id.favorite).setVisibility(View.VISIBLE); 177 178 mFavoriteIcon.setOnClickListener(this); 179 mReplyButton.setOnClickListener(this); 180 mReplyAllButton.setOnClickListener(this); 181 mForwardButton.setOnClickListener(this); 182 mMeetingYes.setOnCheckedChangeListener(this); 183 mMeetingMaybe.setOnCheckedChangeListener(this); 184 mMeetingNo.setOnCheckedChangeListener(this); 185 UiUtilities.getView(view, R.id.invite_link).setOnClickListener(this); 186 187 enableReplyForwardButtons(false); 188 189 return view; 190 } 191 192 @Override 193 public void onActivityCreated(Bundle savedInstanceState) { 194 super.onActivityCreated(savedInstanceState); 195 196 final Bundle args = getArguments(); 197 // STOPSHIP remove the check. Right now it's needed for the obsolete phone activities. 198 if (args != null) { 199 openMessage(args.getLong(ARG_MESSAGE_ID)); 200 } 201 } 202 203 @Override 204 public void onResume() { 205 super.onResume(); 206 } 207 208 @Override 209 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 210 inflater.inflate(R.menu.message_view_fragment_option, menu); 211 } 212 213 private void enableReplyForwardButtons(boolean enabled) { 214 // We don't have disabled button assets, so let's hide them for now 215 final int visibility = enabled ? View.VISIBLE : View.GONE; 216 mReplyButton.setVisibility(visibility); 217 mReplyAllButton.setVisibility(visibility); 218 mForwardButton.setVisibility(visibility); 219 } 220 221 public void setCallback(Callback callback) { 222 mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback; 223 super.setCallback(mCallback); 224 } 225 226 /** Called by activities to set an id of a message to open. */ 227 // STOPSHIP Make it private once phone activities are gone 228 void openMessage(long messageId) { 229 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 230 Log.d(Logging.LOG_TAG, "MessageViewFragment openMessage"); 231 } 232 if (messageId == -1) { 233 throw new InvalidParameterException(); 234 } 235 synchronized (mLock) { 236 mMessageIdToOpen = messageId; 237 } 238 loadMessageIfResumed(); 239 } 240 241 @Override 242 protected void clearContent() { 243 synchronized (mLock) { 244 super.clearContent(); 245 mMessageIdToOpen = -1; 246 247 // Hide the menu. 248 // This isn't really necessary if we're really hiding the fragment. However, 249 // for now, we're actually *not* hiding the fragment (just hiding the root view of it), 250 // so need to remove menus manually. 251 setHasOptionsMenu(false); 252 } 253 } 254 255 @Override 256 protected void resetView() { 257 super.resetView(); 258 mMeetingYes.setChecked(false); 259 mMeetingNo.setChecked(false); 260 mMeetingMaybe.setChecked(false); 261 mPreviousMeetingResponse = EmailServiceConstants.MEETING_REQUEST_NOT_RESPONDED; 262 } 263 264 @Override 265 protected boolean isMessageSpecified() { 266 synchronized (mLock) { 267 return mMessageIdToOpen != -1; 268 } 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 synchronized (mLock) { 277 long messageId = mMessageIdToOpen; 278 if (messageId < 0) { 279 return null; // Called after clearContent(). 280 } 281 return Message.restoreMessageWithId(activity, messageId); 282 } 283 } 284 285 @Override 286 protected void onMessageShown(long messageId, int mailboxType) { 287 super.onMessageShown(messageId, mailboxType); 288 289 // Remember the currently shown message ID. 290 mCurrentMessageId = messageId; 291 292 // Disable forward/reply buttons as necessary. 293 enableReplyForwardButtons(Mailbox.isMailboxTypeReplyAndForwardable(mailboxType)); 294 295 // Show the menu when it's showing a message. 296 setHasOptionsMenu(true); 297 } 298 299 /** 300 * Toggle favorite status and write back to provider 301 */ 302 private void onClickFavorite() { 303 Message message = getMessage(); 304 305 // Update UI 306 boolean newFavorite = ! message.mFlagFavorite; 307 mFavoriteIcon.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff); 308 309 // Update provider 310 message.mFlagFavorite = newFavorite; 311 getController().setMessageFavorite(message.mId, newFavorite); 312 } 313 314 /** 315 * Set message read/unread. 316 */ 317 public void onMarkMessageAsRead(boolean isRead) { 318 Message message = getMessage(); 319 if (message.mFlagRead != isRead) { 320 message.mFlagRead = isRead; 321 getController().setMessageRead(message.mId, isRead); 322 if (!isRead) { // Became unread. We need to close the message. 323 mCallback.onMessageSetUnread(); 324 } 325 } 326 } 327 328 /** 329 * Send a service message indicating that a meeting invite button has been clicked. 330 */ 331 private void onRespondToInvite(int response, int toastResId) { 332 Message message = getMessage(); 333 // do not send twice in a row the same response 334 if (mPreviousMeetingResponse != response) { 335 getController().sendMeetingResponse(message.mId, response); 336 mPreviousMeetingResponse = response; 337 } 338 Utility.showToast(getActivity(), toastResId); 339 mCallback.onRespondedToInvite(response); 340 } 341 342 private void onInviteLinkClicked() { 343 Message message = getMessage(); 344 String startTime = new PackedString(message.mMeetingInfo).get(MeetingInfo.MEETING_DTSTART); 345 if (startTime != null) { 346 long epochTimeMillis = Utility.parseEmailDateTimeToMillis(startTime); 347 mCallback.onCalendarLinkClicked(epochTimeMillis); 348 } else { 349 Email.log("meetingInfo without DTSTART " + message.mMeetingInfo); 350 } 351 } 352 353 @Override 354 public void onClick(View view) { 355 if (!isMessageOpen()) { 356 return; // Ignore. 357 } 358 switch (view.getId()) { 359 case R.id.reply: 360 mCallback.onReply(); 361 return; 362 case R.id.reply_all: 363 mCallback.onReplyAll(); 364 return; 365 case R.id.forward: 366 mCallback.onForward(); 367 return; 368 369 case R.id.favorite: 370 onClickFavorite(); 371 return; 372 373 case R.id.invite_link: 374 onInviteLinkClicked(); 375 return; 376 } 377 super.onClick(view); 378 } 379 380 @Override 381 public void onCheckedChanged(CompoundButton view, boolean isChecked) { 382 if (!isChecked) return; 383 switch (view.getId()) { 384 case R.id.accept: 385 onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_ACCEPTED, 386 R.string.message_view_invite_toast_yes); 387 return; 388 case R.id.maybe: 389 onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_TENTATIVE, 390 R.string.message_view_invite_toast_maybe); 391 return; 392 case R.id.decline: 393 onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_DECLINED, 394 R.string.message_view_invite_toast_no); 395 return; 396 } 397 } 398 399 @Override 400 public boolean onOptionsItemSelected(MenuItem item) { 401 switch (item.getItemId()) { 402 case R.id.move: 403 onMove(); 404 return true; 405 case R.id.delete: 406 onDelete(); 407 return true; 408 case R.id.mark_as_unread: 409 onMarkAsUnread(); 410 return true; 411 } 412 return super.onOptionsItemSelected(item); 413 } 414 415 private void onMove() { 416 // STOPSHIP mCurrentMessageId is not a good one to use here. See b/4346486 417 // (We use it for now just to keep it consistent with onDelete) 418 MoveMessageToDialog dialog = MoveMessageToDialog.newInstance(new long[] {mCurrentMessageId}, 419 this); 420 dialog.show(getFragmentManager(), "dialog"); 421 } 422 423 // MoveMessageToDialog$Callback 424 @Override 425 public void onMoveToMailboxSelected(long newMailboxId, long[] messageIds) { 426 mCallback.onBeforeMessageGone(); 427 ActivityHelper.moveMessages(mContext, newMailboxId, messageIds); 428 } 429 430 private void onDelete() { 431 mCallback.onBeforeMessageGone(); 432 ActivityHelper.deleteMessage(mContext, mCurrentMessageId); 433 } 434 435 private void onMarkAsUnread() { 436 onMarkMessageAsRead(false); 437 } 438 439 /** 440 * {@inheritDoc} 441 * 442 * Mark the current as unread. 443 */ 444 @Override 445 protected void onPostLoadBody() { 446 onMarkMessageAsRead(true); 447 } 448 449 @Override 450 protected void updateHeaderView(Message message) { 451 super.updateHeaderView(message); 452 453 mFavoriteIcon.setImageDrawable(message.mFlagFavorite ? mFavoriteIconOn : mFavoriteIconOff); 454 455 // Enable the invite tab if necessary 456 if ((message.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) { 457 addTabFlags(TAB_FLAGS_HAS_INVITE); 458 } 459 } 460} 461