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