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