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