blob: b842ad4c1c484ad466672e3913183645017c9a3a [file] [log] [blame]
Mindy Pereira326c6602012-01-04 15:32:42 -08001/**
2 * Copyright (c) 2011, Google Inc.
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
Andy Huang30e2c242012-01-06 18:14:30 -080017package com.android.mail.browse;
Mindy Pereira326c6602012-01-04 15:32:42 -080018
mindyp77a46052012-09-18 09:31:06 -070019import android.app.AlertDialog;
Andy Huang3233bff2012-03-20 19:38:45 -070020import android.content.AsyncQueryHandler;
Mindy Pereira326c6602012-01-04 15:32:42 -080021import android.content.Context;
Andy Huangb8331b42012-07-16 19:08:53 -070022import android.database.DataSetObserver;
Mindy Pereira326c6602012-01-04 15:32:42 -080023import android.graphics.Typeface;
24import android.provider.ContactsContract;
25import android.text.Spannable;
26import android.text.SpannableStringBuilder;
27import android.text.TextUtils;
28import android.text.style.StyleSpan;
29import android.util.AttributeSet;
30import android.view.LayoutInflater;
31import android.view.MenuItem;
32import android.view.View;
Mindy Pereira326c6602012-01-04 15:32:42 -080033import android.view.View.OnClickListener;
Andy Huangbb56a152012-02-24 18:26:47 -080034import android.view.ViewGroup;
Mindy Pereira326c6602012-01-04 15:32:42 -080035import android.widget.ImageView;
36import android.widget.LinearLayout;
37import android.widget.PopupMenu;
Andy Huangbb56a152012-02-24 18:26:47 -080038import android.widget.PopupMenu.OnMenuItemClickListener;
Mindy Pereira326c6602012-01-04 15:32:42 -080039import android.widget.QuickContactBadge;
40import android.widget.TextView;
41import android.widget.Toast;
Mindy Pereira326c6602012-01-04 15:32:42 -080042
Andy Huangb8331b42012-07-16 19:08:53 -070043import com.android.mail.ContactInfo;
Andy Huang30e2c242012-01-06 18:14:30 -080044import com.android.mail.ContactInfoSource;
45import com.android.mail.FormattedDateBuilder;
Andy Huangbb56a152012-02-24 18:26:47 -080046import com.android.mail.R;
Andy Huangc7543572012-04-03 15:34:29 -070047import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
Andy Huang839ada22012-07-20 15:48:40 -070048import com.android.mail.browse.MessageCursor.ConversationMessage;
Andy Huang30e2c242012-01-06 18:14:30 -080049import com.android.mail.compose.ComposeActivity;
50import com.android.mail.perf.Timer;
Mindy Pereira33fe9082012-01-09 16:24:30 -080051import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080052import com.android.mail.providers.Address;
Mark Wei0a135882012-10-16 17:11:02 -070053import com.android.mail.providers.Folder;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080054import com.android.mail.providers.Message;
Andy Huang30e2c242012-01-06 18:14:30 -080055import com.android.mail.providers.UIProvider;
Paul Westbrookb334c902012-06-25 11:42:46 -070056import com.android.mail.utils.LogTag;
Andy Huang30e2c242012-01-06 18:14:30 -080057import com.android.mail.utils.LogUtils;
58import com.android.mail.utils.Utils;
Marc Blankbec51152012-03-22 19:27:34 -070059import com.google.common.annotations.VisibleForTesting;
Mindy Pereira326c6602012-01-04 15:32:42 -080060
61import java.io.IOException;
62import java.io.StringReader;
Mark Wei0a135882012-10-16 17:11:02 -070063import java.util.ArrayList;
Andy Huang65fe28f2012-04-06 18:08:53 -070064import java.util.Map;
Mindy Pereira326c6602012-01-04 15:32:42 -080065
Mindy Pereira326c6602012-01-04 15:32:42 -080066public class MessageHeaderView extends LinearLayout implements OnClickListener,
Andy Huang31c38a82012-08-15 21:39:43 -070067 OnMenuItemClickListener, ConversationContainer.DetachListener {
Mindy Pereira326c6602012-01-04 15:32:42 -080068
69 /**
70 * Cap very long recipient lists during summary construction for efficiency.
71 */
72 private static final int SUMMARY_MAX_RECIPIENTS = 50;
73
74 private static final int MAX_SNIPPET_LENGTH = 100;
75
76 private static final int SHOW_IMAGE_PROMPT_ONCE = 1;
77 private static final int SHOW_IMAGE_PROMPT_ALWAYS = 2;
78
79 private static final String HEADER_INFLATE_TAG = "message header inflate";
80 private static final String HEADER_ADDVIEW_TAG = "message header addView";
81 private static final String HEADER_RENDER_TAG = "message header render";
82 private static final String PREMEASURE_TAG = "message header pre-measure";
83 private static final String LAYOUT_TAG = "message header layout";
84 private static final String MEASURE_TAG = "message header measure";
85
86 private static final String RECIPIENT_HEADING_DELIMITER = " ";
87
Paul Westbrookb334c902012-06-25 11:42:46 -070088 private static final String LOG_TAG = LogTag.getLogTag();
Mindy Pereira326c6602012-01-04 15:32:42 -080089
mindyp77a46052012-09-18 09:31:06 -070090 public static final int DEFAULT_MODE = 0;
91
92 public static final int POPUP_MODE = 1;
93
Mindy Pereira326c6602012-01-04 15:32:42 -080094 private MessageHeaderViewCallbacks mCallbacks;
Mindy Pereira326c6602012-01-04 15:32:42 -080095
Andy Huang9f369592012-04-05 20:10:12 -070096 private ViewGroup mUpperHeaderView;
Mindy Pereira326c6602012-01-04 15:32:42 -080097 private TextView mSenderNameView;
98 private TextView mSenderEmailView;
99 private QuickContactBadge mPhotoView;
100 private ImageView mStarView;
101 private ViewGroup mTitleContainerView;
102 private ViewGroup mCollapsedDetailsView;
103 private ViewGroup mExpandedDetailsView;
Andrew Sapperstein3b08c512012-07-11 16:51:07 -0700104 private SpamWarningView mSpamWarningView;
Mindy Pereira326c6602012-01-04 15:32:42 -0800105 private ViewGroup mImagePromptView;
Andy Huanga71f1f32012-04-24 20:17:22 -0700106 private MessageInviteView mInviteView;
Mindy Pereira326c6602012-01-04 15:32:42 -0800107 private View mBottomBorderView;
108 private ImageView mPresenceView;
Andy Huang65fe28f2012-04-06 18:08:53 -0700109 private View mPhotoSpacerView;
110 private View mForwardButton;
111 private View mOverflowButton;
112 private View mDraftIcon;
113 private View mEditDraftButton;
114 private TextView mUpperDateView;
115 private View mReplyButton;
116 private View mReplyAllButton;
117 private View mAttachmentIcon;
Mark Wei1dc57532012-09-13 14:56:50 -0700118 private View mLeftSpacer;
119 private View mRightSpacer;
Mindy Pereira326c6602012-01-04 15:32:42 -0800120
121 // temporary fields to reference raw data between initial render and details
122 // expansion
Andrew Sappersteind5b369b2012-07-13 12:38:46 -0700123 private String[] mFrom;
Mindy Pereira326c6602012-01-04 15:32:42 -0800124 private String[] mTo;
125 private String[] mCc;
126 private String[] mBcc;
127 private String[] mReplyTo;
128 private long mTimestampMs;
129 private FormattedDateBuilder mDateBuilder;
130
131 private boolean mIsDraft = false;
132
133 private boolean mIsSending;
134
Mindy Pereira326c6602012-01-04 15:32:42 -0800135 /**
136 * The snappy header has special visibility rules (i.e. no details header,
137 * even though it has an expanded appearance)
138 */
139 private boolean mIsSnappy;
140
141 private String mSnippet;
142
143 private Address mSender;
144
145 private ContactInfoSource mContactInfoSource;
146
147 private boolean mPreMeasuring;
148
Andy Huang28b7aee2012-08-20 20:27:32 -0700149 private ConversationAccountController mAccountController;
Mindy Pereira326c6602012-01-04 15:32:42 -0800150
Andy Huang65fe28f2012-04-06 18:08:53 -0700151 private Map<String, Address> mAddressCache;
152
Mindy Pereira326c6602012-01-04 15:32:42 -0800153 private boolean mShowImagePrompt;
154
Mindy Pereira326c6602012-01-04 15:32:42 -0800155 private CharSequence mTimestampShort;
156
157 /**
158 * Take the initial visibility of the star view to mean its collapsed
159 * visibility. Star is always visible when expanded, but sometimes, like on
160 * phones, there isn't enough room to warrant showing star when collapsed.
161 */
Mark Wei0a135882012-10-16 17:11:02 -0700162 private boolean mCollapsedStarVisible;
163 private boolean mStarShown;
Mindy Pereira326c6602012-01-04 15:32:42 -0800164
165 /**
166 * Take the initial right margin of the header title container to mean its
167 * right margin when collapsed. There's currently no need for additional
168 * margin when expanded, but if that need ever arises, title_container can
169 * simply tack on some extra right padding.
170 */
171 private int mTitleContainerCollapsedMarginRight;
172
Mindy Pereira326c6602012-01-04 15:32:42 -0800173 private PopupMenu mPopup;
174
Andy Huangc7543572012-04-03 15:34:29 -0700175 private MessageHeaderItem mMessageHeaderItem;
Andy Huang839ada22012-07-20 15:48:40 -0700176 private ConversationMessage mMessage;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800177
Andy Huangf70fc402012-02-17 15:37:42 -0800178 private boolean mCollapsedDetailsValid;
179 private boolean mExpandedDetailsValid;
180
Andy Huang88fc42e2012-03-08 15:02:43 -0800181 private final LayoutInflater mInflater;
182
Andy Huang3233bff2012-03-20 19:38:45 -0700183 private AsyncQueryHandler mQueryHandler;
184
Andy Huang59e0b182012-08-14 14:32:23 -0700185 private boolean mObservingContactInfo;
186
Andy Huangb8331b42012-07-16 19:08:53 -0700187 private final DataSetObserver mContactInfoObserver = new DataSetObserver() {
188 @Override
189 public void onChanged() {
190 updateContactInfo();
191 }
192 };
193
mindypf4fce122012-09-14 15:55:33 -0700194 private boolean mExpandable = true;
195
mindyp77a46052012-09-18 09:31:06 -0700196 private int mExpandMode = DEFAULT_MODE;
197
198 private AlertDialog mDetailsPopup;
199
Andy Huang3233bff2012-03-20 19:38:45 -0700200 public interface MessageHeaderViewCallbacks {
Andy Huangc7543572012-04-03 15:34:29 -0700201 void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeight);
Andy Huang3233bff2012-03-20 19:38:45 -0700202
Andy Huangc7543572012-04-03 15:34:29 -0700203 void setMessageExpanded(MessageHeaderItem item, int newSpacerHeight);
Andy Huang3233bff2012-03-20 19:38:45 -0700204
mindyp1b3cc472012-09-27 11:32:59 -0700205 void setMessageDetailsExpanded(MessageHeaderItem messageHeaderItem, boolean expanded,
206 int previousMessageHeaderItemHeight);
207
Andy Huang3233bff2012-03-20 19:38:45 -0700208 void showExternalResources(Message msg);
209 }
210
Mindy Pereira326c6602012-01-04 15:32:42 -0800211 public MessageHeaderView(Context context) {
212 this(context, null);
213 }
214
215 public MessageHeaderView(Context context, AttributeSet attrs) {
216 this(context, attrs, -1);
217 }
218
219 public MessageHeaderView(Context context, AttributeSet attrs, int defStyle) {
220 super(context, attrs, defStyle);
Andy Huang88fc42e2012-03-08 15:02:43 -0800221
222 mInflater = LayoutInflater.from(context);
Mindy Pereira326c6602012-01-04 15:32:42 -0800223 }
224
mindyp77a46052012-09-18 09:31:06 -0700225 /**
226 * Expand mode is DEFAULT_MODE by default.
227 */
228 public void setExpandMode(int mode) {
229 mExpandMode = mode;
230 }
231
Mindy Pereira326c6602012-01-04 15:32:42 -0800232 @Override
233 protected void onFinishInflate() {
234 super.onFinishInflate();
Andy Huang9f369592012-04-05 20:10:12 -0700235 mUpperHeaderView = (ViewGroup) findViewById(R.id.upper_header);
Mindy Pereira326c6602012-01-04 15:32:42 -0800236 mSenderNameView = (TextView) findViewById(R.id.sender_name);
237 mSenderEmailView = (TextView) findViewById(R.id.sender_email);
238 mPhotoView = (QuickContactBadge) findViewById(R.id.photo);
Andy Huang65fe28f2012-04-06 18:08:53 -0700239 mPhotoSpacerView = findViewById(R.id.photo_spacer);
240 mReplyButton = findViewById(R.id.reply);
241 mReplyAllButton = findViewById(R.id.reply_all);
242 mForwardButton = findViewById(R.id.forward);
Mindy Pereira326c6602012-01-04 15:32:42 -0800243 mStarView = (ImageView) findViewById(R.id.star);
244 mPresenceView = (ImageView) findViewById(R.id.presence);
245 mTitleContainerView = (ViewGroup) findViewById(R.id.title_container);
Andy Huang65fe28f2012-04-06 18:08:53 -0700246 mOverflowButton = findViewById(R.id.overflow);
247 mDraftIcon = findViewById(R.id.draft);
248 mEditDraftButton = findViewById(R.id.edit_draft);
249 mUpperDateView = (TextView) findViewById(R.id.upper_date);
250 mAttachmentIcon = findViewById(R.id.attachment);
Mindy Pereira326c6602012-01-04 15:32:42 -0800251
Mark Wei0a135882012-10-16 17:11:02 -0700252 mCollapsedStarVisible = mStarView.getVisibility() == VISIBLE;
Mindy Pereira326c6602012-01-04 15:32:42 -0800253 mTitleContainerCollapsedMarginRight = ((MarginLayoutParams) mTitleContainerView
254 .getLayoutParams()).rightMargin;
255
Andy Huang9f369592012-04-05 20:10:12 -0700256 mBottomBorderView = findViewById(R.id.details_bottom_border);
Mark Wei1dc57532012-09-13 14:56:50 -0700257 mLeftSpacer = findViewById(R.id.left_spacer);
258 mRightSpacer = findViewById(R.id.right_spacer);
Andy Huang9f369592012-04-05 20:10:12 -0700259
Mindy Pereira326c6602012-01-04 15:32:42 -0800260 setExpanded(true);
261
262 registerMessageClickTargets(R.id.reply, R.id.reply_all, R.id.forward, R.id.star,
263 R.id.edit_draft, R.id.overflow, R.id.upper_header);
264 }
265
266 private void registerMessageClickTargets(int... ids) {
267 for (int id : ids) {
268 View v = findViewById(id);
269 if (v != null) {
270 v.setOnClickListener(this);
271 }
272 }
273 }
274
Mindy Pereira326c6602012-01-04 15:32:42 -0800275 /**
276 * Associate the header with a contact info source for later contact
277 * presence/photo lookup.
278 */
279 public void setContactInfoSource(ContactInfoSource contactInfoSource) {
280 mContactInfoSource = contactInfoSource;
281 }
282
283 public void setCallbacks(MessageHeaderViewCallbacks callbacks) {
284 mCallbacks = callbacks;
285 }
286
287 /**
288 * Find the header view corresponding to a message with given local ID.
289 *
290 * @param parent the view parent to search within
291 * @param localMessageId local message ID
292 * @return a header view or null
293 */
294 public static MessageHeaderView find(ViewGroup parent, long localMessageId) {
295 return (MessageHeaderView) parent.findViewWithTag(localMessageId);
296 }
297
Mindy Pereira326c6602012-01-04 15:32:42 -0800298 public boolean isExpanded() {
Andy Huangc7543572012-04-03 15:34:29 -0700299 // (let's just arbitrarily say that unbound views are expanded by default)
300 return mMessageHeaderItem == null || mMessageHeaderItem.isExpanded();
Mindy Pereira326c6602012-01-04 15:32:42 -0800301 }
302
Mindy Pereira326c6602012-01-04 15:32:42 -0800303 public void setSnappy(boolean snappy) {
304 mIsSnappy = snappy;
305 hideMessageDetails();
306 if (snappy) {
307 setBackgroundDrawable(null);
Mark Wei1dc57532012-09-13 14:56:50 -0700308 // snappy header overlay has no padding so we need spacers
309 mLeftSpacer.setVisibility(View.VISIBLE);
310 mRightSpacer.setVisibility(View.VISIBLE);
Mindy Pereira326c6602012-01-04 15:32:42 -0800311 } else {
312 setBackgroundColor(android.R.color.white);
Mark Wei1dc57532012-09-13 14:56:50 -0700313 // scrolling layer does have padding so we don't need spacers
314 mLeftSpacer.setVisibility(View.GONE);
315 mRightSpacer.setVisibility(View.GONE);
Mindy Pereira326c6602012-01-04 15:32:42 -0800316 }
317 }
318
Andy Huangb8331b42012-07-16 19:08:53 -0700319 @Override
320 public void onDetachedFromParent() {
321 unbind();
322 }
323
Mindy Pereira326c6602012-01-04 15:32:42 -0800324 /**
325 * Headers that are unbound will not match any rendered header (matches()
326 * will return false). Unbinding is not guaranteed to *hide* the view's old
327 * data, though. To re-bind this header to message data, call render() or
328 * renderUpperHeaderFrom().
329 */
330 public void unbind() {
Andy Huangc7543572012-04-03 15:34:29 -0700331 mMessageHeaderItem = null;
Andy Huang3233bff2012-03-20 19:38:45 -0700332 mMessage = null;
Andy Huangb8331b42012-07-16 19:08:53 -0700333
Andy Huang59e0b182012-08-14 14:32:23 -0700334 if (mObservingContactInfo) {
335 mContactInfoSource.unregisterObserver(mContactInfoObserver);
336 mObservingContactInfo = false;
337 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800338 }
339
Andy Huang28b7aee2012-08-20 20:27:32 -0700340 public void initialize(FormattedDateBuilder dateBuilder,
341 ConversationAccountController accountController,
Andy Huang65fe28f2012-04-06 18:08:53 -0700342 Map<String, Address> addressCache) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800343 mDateBuilder = dateBuilder;
Andy Huang28b7aee2012-08-20 20:27:32 -0700344 mAccountController = accountController;
Andy Huang65fe28f2012-04-06 18:08:53 -0700345 mAddressCache = addressCache;
Mindy Pereira326c6602012-01-04 15:32:42 -0800346 }
347
Andy Huang28b7aee2012-08-20 20:27:32 -0700348 private Account getAccount() {
349 return mAccountController.getAccount();
350 }
351
352 public void bind(MessageHeaderItem headerItem, boolean measureOnly) {
Andy Huang59e0b182012-08-14 14:32:23 -0700353 if (mMessageHeaderItem != null && mMessageHeaderItem == headerItem) {
354 return;
355 }
356
Andy Huang6b3d0d92012-10-30 15:46:48 -0700357 mMessageHeaderItem = headerItem;
358 render(measureOnly);
359 }
360
361 public void refresh() {
362 render(false);
363 }
364
365 private void render(boolean measureOnly) {
Andy Huang4baafcb2012-11-01 18:01:49 -0700366 if (mMessageHeaderItem == null) {
367 return;
368 }
369
Mindy Pereira326c6602012-01-04 15:32:42 -0800370 Timer t = new Timer();
371 t.start(HEADER_RENDER_TAG);
372
Andy Huangf70fc402012-02-17 15:37:42 -0800373 mCollapsedDetailsValid = false;
374 mExpandedDetailsValid = false;
375
Andy Huang6b3d0d92012-10-30 15:46:48 -0700376 mMessage = mMessageHeaderItem.getMessage();
Andy Huangc7543572012-04-03 15:34:29 -0700377 mShowImagePrompt = mMessage.shouldShowImagePrompt();
Andy Huang6b3d0d92012-10-30 15:46:48 -0700378 setExpanded(mMessageHeaderItem.isExpanded());
Mindy Pereira326c6602012-01-04 15:32:42 -0800379
Andy Huangf70fc402012-02-17 15:37:42 -0800380 mTimestampMs = mMessage.dateReceivedMs;
Andy Huang6b3d0d92012-10-30 15:46:48 -0700381 mTimestampShort = mMessageHeaderItem.timestampShort;
Andy Huang65fe28f2012-04-06 18:08:53 -0700382 if (mTimestampShort == null) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800383 mTimestampShort = mDateBuilder.formatShortDate(mTimestampMs);
Andy Huang6b3d0d92012-10-30 15:46:48 -0700384 mMessageHeaderItem.timestampShort = mTimestampShort;
Mindy Pereira326c6602012-01-04 15:32:42 -0800385 }
386
Andrew Sappersteind5b369b2012-07-13 12:38:46 -0700387 mFrom = mMessage.getFromAddresses();
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700388 mTo = mMessage.getToAddresses();
389 mCc = mMessage.getCcAddresses();
390 mBcc = mMessage.getBccAddresses();
391 mReplyTo = mMessage.getReplyToAddresses();
Mindy Pereira326c6602012-01-04 15:32:42 -0800392
393 /**
394 * Turns draft mode on or off. Draft mode hides message operations other
395 * than "edit", hides contact photo, hides presence, and changes the
396 * sender name to "Draft".
397 */
Andy Huangf70fc402012-02-17 15:37:42 -0800398 mIsDraft = mMessage.draftType != UIProvider.DraftType.NOT_A_DRAFT;
Andy Huang2a1e8e32012-10-23 18:54:57 -0700399 mIsSending = mMessage.isSending;
Mindy Pereira326c6602012-01-04 15:32:42 -0800400
Mindy Pereira326c6602012-01-04 15:32:42 -0800401 // If this was a sent message AND:
402 // 1. the account has a custom from, the cursor will populate the
403 // selected custom from as the fromAddress when a message is sent but
404 // not yet synced.
405 // 2. the account has no custom froms, fromAddress will be empty, and we
406 // can safely fall back and show the account name as sender since it's
407 // the only possible fromAddress.
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800408 String from = mMessage.getFrom();
Mindy Pereira326c6602012-01-04 15:32:42 -0800409 if (TextUtils.isEmpty(from)) {
Andy Huang28b7aee2012-08-20 20:27:32 -0700410 from = getAccount().name;
Mindy Pereira326c6602012-01-04 15:32:42 -0800411 }
Andy Huang65fe28f2012-04-06 18:08:53 -0700412 mSender = getAddress(from);
Mindy Pereira326c6602012-01-04 15:32:42 -0800413
Mark Wei0a135882012-10-16 17:11:02 -0700414 mStarView.setSelected(mMessage.starred);
415 mStarView.setContentDescription(getResources().getString(
416 mStarView.isSelected() ? R.string.remove_star : R.string.add_star));
417 mStarShown = true;
418 ArrayList<Folder> folders = mMessage.conversation.getRawFolders();
419 for (Folder folder : folders) {
420 if (folder.isTrash()) {
421 mStarShown = false;
422 break;
423 }
424 }
425
Andrew Sapperstein3b08c512012-07-11 16:51:07 -0700426 updateChildVisibility();
427
Andy Huang2a1e8e32012-10-23 18:54:57 -0700428 if (mIsDraft || mIsSending) {
Andrew Sapperstein3b08c512012-07-11 16:51:07 -0700429 mSnippet = makeSnippet(mMessage.snippet);
430 } else {
431 mSnippet = mMessage.snippet;
432 }
433
Mindy Pereira326c6602012-01-04 15:32:42 -0800434 mSenderNameView.setText(getHeaderTitle());
435 mSenderEmailView.setText(getHeaderSubtitle());
436
Andy Huang65fe28f2012-04-06 18:08:53 -0700437 if (mUpperDateView != null) {
438 mUpperDateView.setText(mTimestampShort);
Mindy Pereira326c6602012-01-04 15:32:42 -0800439 }
440
Andy Huang59e0b182012-08-14 14:32:23 -0700441 if (measureOnly) {
442 // avoid leaving any state around that would interfere with future regular bind() calls
443 unbind();
444 } else {
Andy Huangb8331b42012-07-16 19:08:53 -0700445 updateContactInfo();
Andy Huang59e0b182012-08-14 14:32:23 -0700446 if (!mObservingContactInfo) {
447 mContactInfoSource.registerObserver(mContactInfoObserver);
448 mObservingContactInfo = true;
449 }
Andy Huangb8331b42012-07-16 19:08:53 -0700450 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800451
452 t.pause(HEADER_RENDER_TAG);
Mindy Pereira326c6602012-01-04 15:32:42 -0800453 }
454
Andy Huang4baafcb2012-11-01 18:01:49 -0700455 public boolean isBoundTo(ConversationOverlayItem item) {
456 return item == mMessageHeaderItem;
457 }
458
Andy Huang65fe28f2012-04-06 18:08:53 -0700459 private Address getAddress(String emailStr) {
460 return getAddress(mAddressCache, emailStr);
461 }
462
463 private static Address getAddress(Map<String, Address> cache, String emailStr) {
464 Address addr = null;
465 if (cache != null) {
466 addr = cache.get(emailStr);
467 }
468 if (addr == null) {
469 addr = Address.getEmailAddress(emailStr);
470 if (cache != null) {
471 cache.put(emailStr, addr);
472 }
473 }
474 return addr;
475 }
476
Andy Huangc7543572012-04-03 15:34:29 -0700477 private void updateSpacerHeight() {
478 final int h = measureHeight();
479
480 mMessageHeaderItem.setHeight(h);
481 if (mCallbacks != null) {
482 mCallbacks.setMessageSpacerHeight(mMessageHeaderItem, h);
483 }
484 }
485
486 private int measureHeight() {
487 ViewGroup parent = (ViewGroup) getParent();
Mindy Pereira326c6602012-01-04 15:32:42 -0800488 if (parent == null) {
Andy Huangc7543572012-04-03 15:34:29 -0700489 LogUtils.e(LOG_TAG, new Error(), "Unable to measure height of detached header");
Mindy Pereira326c6602012-01-04 15:32:42 -0800490 return getHeight();
491 }
492 mPreMeasuring = true;
Andy Huangc7543572012-04-03 15:34:29 -0700493 final int h = Utils.measureViewHeight(this, parent);
Mindy Pereira326c6602012-01-04 15:32:42 -0800494 mPreMeasuring = false;
495 return h;
496 }
497
498 private CharSequence getHeaderTitle() {
499 CharSequence title;
500
501 if (mIsDraft) {
502 title = getResources().getQuantityText(R.plurals.draft, 1);
503 } else if (mIsSending) {
504 title = getResources().getString(R.string.sending);
505 } else {
506 title = getSenderName(mSender);
507 }
508
509 return title;
510 }
511
512 private CharSequence getHeaderSubtitle() {
513 CharSequence sub;
514 if (mIsSending) {
515 sub = null;
516 } else {
Andrew Sappersteind5b369b2012-07-13 12:38:46 -0700517 if (isExpanded()) {
518 if (mMessage.viaDomain != null) {
519 sub = getResources().getString(
520 R.string.via_domain, mMessage.viaDomain);
521 } else {
522 sub = getSenderAddress(mSender);
523 }
524 } else {
525 sub = mSnippet;
526 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800527 }
528 return sub;
529 }
530
531 /**
532 * Return the name, if known, or just the address.
533 */
534 private static CharSequence getSenderName(Address sender) {
Andy Huang65fe28f2012-04-06 18:08:53 -0700535 final String displayName = sender.getName();
536 return TextUtils.isEmpty(displayName) ? sender.getAddress() : displayName;
Mindy Pereira326c6602012-01-04 15:32:42 -0800537 }
538
539 /**
540 * Return the address, if a name is present, or null if not.
541 */
542 private static CharSequence getSenderAddress(Address sender) {
543 String displayName = sender == null ? "" : sender.getName();
544 return TextUtils.isEmpty(displayName) ? null : sender.getAddress();
545 }
546
Andy Huang65fe28f2012-04-06 18:08:53 -0700547 private void setChildVisibility(int visibility, View... children) {
548 for (View v : children) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800549 if (v != null) {
550 v.setVisibility(visibility);
551 }
552 }
553 }
554
555 private void setExpanded(final boolean expanded) {
556 // use View's 'activated' flag to store expanded state
557 // child view state lists can use this to toggle drawables
558 setActivated(expanded);
Andy Huangc7543572012-04-03 15:34:29 -0700559 if (mMessageHeaderItem != null) {
560 mMessageHeaderItem.setExpanded(expanded);
561 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800562 }
563
564 /**
565 * Update the visibility of the many child views based on expanded/collapsed
566 * and draft/normal state.
567 */
568 private void updateChildVisibility() {
Mindy Pereira326c6602012-01-04 15:32:42 -0800569 // Too bad this can't be done with an XML state list...
570
Andy Huangc7543572012-04-03 15:34:29 -0700571 if (isExpanded()) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800572 int normalVis, draftVis;
573
574 setMessageDetailsVisibility((mIsSnappy) ? GONE : VISIBLE);
575
576 if (mIsDraft) {
577 normalVis = GONE;
578 draftVis = VISIBLE;
579 } else {
580 normalVis = VISIBLE;
581 draftVis = GONE;
582 }
583
584 setReplyOrReplyAllVisible();
Andy Huang65fe28f2012-04-06 18:08:53 -0700585 setChildVisibility(normalVis, mPhotoView, mPhotoSpacerView, mForwardButton,
586 mSenderEmailView, mOverflowButton);
587 setChildVisibility(draftVis, mDraftIcon, mEditDraftButton);
588 setChildVisibility(GONE, mAttachmentIcon, mUpperDateView);
Mark Wei0a135882012-10-16 17:11:02 -0700589 setChildVisibility(mStarShown ? VISIBLE : GONE, mStarView);
Mindy Pereira326c6602012-01-04 15:32:42 -0800590
591 setChildMarginRight(mTitleContainerView, 0);
592
593 } else {
594
595 setMessageDetailsVisibility(GONE);
Andy Huang65fe28f2012-04-06 18:08:53 -0700596 setChildVisibility(VISIBLE, mSenderEmailView, mUpperDateView);
Mindy Pereira326c6602012-01-04 15:32:42 -0800597
Andy Huang65fe28f2012-04-06 18:08:53 -0700598 setChildVisibility(GONE, mEditDraftButton, mReplyButton, mReplyAllButton,
599 mForwardButton);
600 setChildVisibility(GONE, mOverflowButton);
Mindy Pereira326c6602012-01-04 15:32:42 -0800601
Andy Huang88fc42e2012-03-08 15:02:43 -0800602 setChildVisibility(mMessage.hasAttachments ? VISIBLE : GONE,
Andy Huang65fe28f2012-04-06 18:08:53 -0700603 mAttachmentIcon);
Mindy Pereira326c6602012-01-04 15:32:42 -0800604
Mark Wei0a135882012-10-16 17:11:02 -0700605 setChildVisibility(mCollapsedStarVisible && mStarShown ? VISIBLE : GONE, mStarView);
Mindy Pereira326c6602012-01-04 15:32:42 -0800606
607 setChildMarginRight(mTitleContainerView, mTitleContainerCollapsedMarginRight);
608
609 if (mIsDraft) {
610
Andy Huang65fe28f2012-04-06 18:08:53 -0700611 setChildVisibility(VISIBLE, mDraftIcon);
612 setChildVisibility(GONE, mPhotoView, mPhotoSpacerView);
Mindy Pereira326c6602012-01-04 15:32:42 -0800613
614 } else {
615
Andy Huang65fe28f2012-04-06 18:08:53 -0700616 setChildVisibility(GONE, mDraftIcon);
617 setChildVisibility(VISIBLE, mPhotoView, mPhotoSpacerView);
Mindy Pereira326c6602012-01-04 15:32:42 -0800618
619 }
620 }
621
622 }
623
624 /**
625 * If an overflow menu is present in this header's layout, set the
626 * visibility of "Reply" and "Reply All" actions based on a user preference.
627 * Only one of those actions will be visible when an overflow is present. If
628 * no overflow is present (e.g. big phone or tablet), it's assumed we have
629 * plenty of screen real estate and can show both.
630 */
631 private void setReplyOrReplyAllVisible() {
632 if (mIsDraft) {
Andy Huang65fe28f2012-04-06 18:08:53 -0700633 setChildVisibility(GONE, mReplyButton, mReplyAllButton);
Mindy Pereira326c6602012-01-04 15:32:42 -0800634 return;
Andy Huang65fe28f2012-04-06 18:08:53 -0700635 } else if (mOverflowButton == null) {
636 setChildVisibility(VISIBLE, mReplyButton, mReplyAllButton);
Mindy Pereira326c6602012-01-04 15:32:42 -0800637 return;
638 }
639
Andy Huang28b7aee2012-08-20 20:27:32 -0700640 final boolean defaultReplyAll = getAccount().settings.replyBehavior
641 == UIProvider.DefaultReplyBehavior.REPLY_ALL;
642 setChildVisibility(defaultReplyAll ? GONE : VISIBLE, mReplyButton);
643 setChildVisibility(defaultReplyAll ? VISIBLE : GONE, mReplyAllButton);
Mindy Pereira326c6602012-01-04 15:32:42 -0800644 }
645
646 private static void setChildMarginRight(View childView, int marginRight) {
647 MarginLayoutParams mlp = (MarginLayoutParams) childView.getLayoutParams();
648 mlp.rightMargin = marginRight;
649 childView.setLayoutParams(mlp);
650 }
651
mindyp77a46052012-09-18 09:31:06 -0700652 private void renderEmailList(int rowRes, int valueRes, String[] emails, boolean showViaDomain,
653 View rootView) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800654 if (emails == null || emails.length == 0) {
655 return;
656 }
657 String[] formattedEmails = new String[emails.length];
658 for (int i = 0; i < emails.length; i++) {
Andy Huang65fe28f2012-04-06 18:08:53 -0700659 Address e = getAddress(emails[i]);
Mindy Pereira326c6602012-01-04 15:32:42 -0800660 String name = e.getName();
661 String addr = e.getAddress();
662 if (name == null || name.length() == 0) {
663 formattedEmails[i] = addr;
664 } else {
Andrew Sappersteind5b369b2012-07-13 12:38:46 -0700665 // The one downside to having the showViaDomain here is that
666 // if the sender does not have a name, it will not show the via info
667 if (showViaDomain) {
668 formattedEmails[i] = getResources().getString(
669 R.string.address_display_format_with_via_domain,
670 name, addr, mMessage.viaDomain);
671 } else {
672 formattedEmails[i] = getResources().getString(R.string.address_display_format,
673 name, addr);
674 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800675 }
676 }
mindyp77a46052012-09-18 09:31:06 -0700677 ((TextView) rootView.findViewById(valueRes)).setText(TextUtils.join("\n", formattedEmails));
678 rootView.findViewById(rowRes).setVisibility(VISIBLE);
Mindy Pereira326c6602012-01-04 15:32:42 -0800679 }
680
Mindy Pereira326c6602012-01-04 15:32:42 -0800681 /**
682 * Utility class to build a list of recipient lists.
683 */
684 private static class RecipientListsBuilder {
685 private final Context mContext;
686 private final String mMe;
687 private final SpannableStringBuilder mBuilder = new SpannableStringBuilder();
688 private final CharSequence mComma;
Andy Huang65fe28f2012-04-06 18:08:53 -0700689 private final Map<String, Address> mAddressCache;
Mindy Pereira326c6602012-01-04 15:32:42 -0800690
691 int mRecipientCount = 0;
692 boolean mFirst = true;
693
Andy Huang65fe28f2012-04-06 18:08:53 -0700694 public RecipientListsBuilder(Context context, String me,
695 Map<String, Address> addressCache) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800696 mContext = context;
697 mMe = me;
698 mComma = mContext.getText(R.string.enumeration_comma);
Andy Huang65fe28f2012-04-06 18:08:53 -0700699 mAddressCache = addressCache;
Mindy Pereira326c6602012-01-04 15:32:42 -0800700 }
701
702 public void append(String[] recipients, int headingRes) {
703 int addLimit = SUMMARY_MAX_RECIPIENTS - mRecipientCount;
704 CharSequence recipientList = getSummaryTextForHeading(headingRes, recipients, addLimit);
705 if (recipientList != null) {
706 // duplicate TextUtils.join() logic to minimize temporary
707 // allocations, and because we need to support spans
708 if (mFirst) {
709 mFirst = false;
710 } else {
711 mBuilder.append(RECIPIENT_HEADING_DELIMITER);
712 }
713 mBuilder.append(recipientList);
714 mRecipientCount += Math.min(addLimit, recipients.length);
715 }
716 }
717
718 private CharSequence getSummaryTextForHeading(int headingStrRes, String[] rawAddrs,
719 int maxToCopy) {
720 if (rawAddrs == null || rawAddrs.length == 0 || maxToCopy == 0) {
721 return null;
722 }
723
724 SpannableStringBuilder ssb = new SpannableStringBuilder(
725 mContext.getString(headingStrRes));
726 ssb.setSpan(new StyleSpan(Typeface.BOLD), 0, ssb.length(),
727 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
728 ssb.append(' ');
729
730 final int len = Math.min(maxToCopy, rawAddrs.length);
731 boolean first = true;
732 for (int i = 0; i < len; i++) {
Andy Huang65fe28f2012-04-06 18:08:53 -0700733 Address email = getAddress(mAddressCache, rawAddrs[i]);
Mindy Pereira326c6602012-01-04 15:32:42 -0800734 String name = (mMe.equals(email.getAddress())) ? mContext.getString(R.string.me)
735 : email.getSimplifiedName();
736
737 // duplicate TextUtils.join() logic to minimize temporary
738 // allocations, and because we need to support spans
739 if (first) {
740 first = false;
741 } else {
742 ssb.append(mComma);
743 }
744 ssb.append(name);
745 }
746
747 return ssb;
748 }
749
750 public CharSequence build() {
751 return mBuilder;
752 }
753 }
754
755 @VisibleForTesting
756 static CharSequence getRecipientSummaryText(Context context, String me, String[] to,
Andy Huang65fe28f2012-04-06 18:08:53 -0700757 String[] cc, String[] bcc, Map<String, Address> addressCache) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800758
Andy Huang65fe28f2012-04-06 18:08:53 -0700759 RecipientListsBuilder builder = new RecipientListsBuilder(context, me, addressCache);
Mindy Pereira326c6602012-01-04 15:32:42 -0800760
761 builder.append(to, R.string.to_heading);
762 builder.append(cc, R.string.cc_heading);
763 builder.append(bcc, R.string.bcc_heading);
764
765 return builder.build();
766 }
767
Andy Huang31c38a82012-08-15 21:39:43 -0700768 private void updateContactInfo() {
Mindy Pereira326c6602012-01-04 15:32:42 -0800769
770 mPresenceView.setImageDrawable(null);
771 mPresenceView.setVisibility(GONE);
772 if (mContactInfoSource == null || mSender == null) {
773 mPhotoView.setImageToDefault();
774 mPhotoView.setContentDescription(getResources().getString(
775 R.string.contact_info_string_default));
776 return;
777 }
778
779 // Set the photo to either a found Bitmap or the default
780 // and ensure either the contact URI or email is set so the click
781 // handling works
782 String contentDesc = getResources().getString(R.string.contact_info_string,
783 !TextUtils.isEmpty(mSender.getName()) ? mSender.getName() : mSender.getAddress());
784 mPhotoView.setContentDescription(contentDesc);
785 boolean photoSet = false;
786 String email = mSender.getAddress();
787 ContactInfo info = mContactInfoSource.getContactInfo(email);
788 if (info != null) {
789 mPhotoView.assignContactUri(info.contactUri);
790 if (info.photo != null) {
791 mPhotoView.setImageBitmap(info.photo);
792 contentDesc = String.format(contentDesc, mSender.getName());
793 photoSet = true;
794 }
795 if (!mIsDraft && info.status != null) {
796 mPresenceView.setImageResource(ContactsContract.StatusUpdates
797 .getPresenceIconResourceId(info.status));
798 mPresenceView.setVisibility(VISIBLE);
799 }
800 } else {
801 mPhotoView.assignContactFromEmail(email, true /* lazyLookup */);
802 }
803
804 if (!photoSet) {
805 mPhotoView.setImageToDefault();
806 }
807 }
808
809
810 @Override
811 public boolean onMenuItemClick(MenuItem item) {
812 mPopup.dismiss();
813 return onClick(null, item.getItemId());
814 }
815
816 @Override
817 public void onClick(View v) {
818 onClick(v, v.getId());
819 }
820
821 /**
822 * Handles clicks on either views or menu items. View parameter can be null
823 * for menu item clicks.
824 */
825 public boolean onClick(View v, int id) {
Andy Huange9523532012-08-27 12:42:24 -0700826 if (mMessage == null) {
827 LogUtils.i(LOG_TAG, "ignoring message header tap on unbound view");
828 return false;
829 }
830
Mindy Pereira326c6602012-01-04 15:32:42 -0800831 boolean handled = true;
832
833 switch (id) {
834 case R.id.reply:
Andy Huang28b7aee2012-08-20 20:27:32 -0700835 ComposeActivity.reply(getContext(), getAccount(), mMessage);
Mindy Pereira326c6602012-01-04 15:32:42 -0800836 break;
837 case R.id.reply_all:
Andy Huang28b7aee2012-08-20 20:27:32 -0700838 ComposeActivity.replyAll(getContext(), getAccount(), mMessage);
Mindy Pereira326c6602012-01-04 15:32:42 -0800839 break;
840 case R.id.forward:
Andy Huang28b7aee2012-08-20 20:27:32 -0700841 ComposeActivity.forward(getContext(), getAccount(), mMessage);
Mindy Pereira326c6602012-01-04 15:32:42 -0800842 break;
843 case R.id.star: {
Andy Huangd8e249e2012-03-21 17:01:37 -0700844 final boolean newValue = !v.isSelected();
Mindy Pereira326c6602012-01-04 15:32:42 -0800845 v.setSelected(newValue);
Andy Huang839ada22012-07-20 15:48:40 -0700846 mMessage.star(newValue);
Mindy Pereira326c6602012-01-04 15:32:42 -0800847 break;
848 }
849 case R.id.edit_draft:
Andy Huang28b7aee2012-08-20 20:27:32 -0700850 ComposeActivity.editDraft(getContext(), getAccount(), mMessage);
Mindy Pereira326c6602012-01-04 15:32:42 -0800851 break;
852 case R.id.overflow: {
853 if (mPopup == null) {
854 mPopup = new PopupMenu(getContext(), v);
855 mPopup.getMenuInflater().inflate(R.menu.message_header_overflow_menu,
856 mPopup.getMenu());
857 mPopup.setOnMenuItemClickListener(this);
858 }
Andy Huang28b7aee2012-08-20 20:27:32 -0700859 final boolean defaultReplyAll = getAccount().settings.replyBehavior
860 == UIProvider.DefaultReplyBehavior.REPLY_ALL;
861 mPopup.getMenu().findItem(R.id.reply).setVisible(defaultReplyAll);
862 mPopup.getMenu().findItem(R.id.reply_all).setVisible(!defaultReplyAll);
Mindy Pereira326c6602012-01-04 15:32:42 -0800863
864 mPopup.show();
865 break;
866 }
867 case R.id.details_collapsed_content:
868 case R.id.details_expanded_content:
869 toggleMessageDetails(v);
870 break;
871 case R.id.upper_header:
872 toggleExpanded();
873 break;
874 case R.id.show_pictures:
875 handleShowImagePromptClick(v);
876 break;
877 default:
878 LogUtils.i(LOG_TAG, "unrecognized header tap: %d", id);
879 handled = false;
880 break;
881 }
882 return handled;
883 }
884
mindypf4fce122012-09-14 15:55:33 -0700885 public void setExpandable(boolean expandable) {
886 mExpandable = expandable;
887 }
888
Mindy Pereira326c6602012-01-04 15:32:42 -0800889 public void toggleExpanded() {
mindypf4fce122012-09-14 15:55:33 -0700890 if (!mExpandable) {
891 return;
892 }
Andy Huangc7543572012-04-03 15:34:29 -0700893 setExpanded(!isExpanded());
Mindy Pereira326c6602012-01-04 15:32:42 -0800894
mindypf69a05b2012-09-07 15:43:11 -0700895 // The snappy header will disappear; no reason to update text.
896 if (!mIsSnappy) {
897 mSenderNameView.setText(getHeaderTitle());
898 mSenderEmailView.setText(getHeaderSubtitle());
899 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800900
901 updateChildVisibility();
902
903 // Force-measure the new header height so we can set the spacer size and
mindypf69a05b2012-09-07 15:43:11 -0700904 // reveal the message div in one pass. Force-measuring makes it unnecessary to set
Mindy Pereira326c6602012-01-04 15:32:42 -0800905 // mSizeChanged.
Andy Huangc7543572012-04-03 15:34:29 -0700906 int h = measureHeight();
907 mMessageHeaderItem.setHeight(h);
Mindy Pereira326c6602012-01-04 15:32:42 -0800908 if (mCallbacks != null) {
Andy Huangc7543572012-04-03 15:34:29 -0700909 mCallbacks.setMessageExpanded(mMessageHeaderItem, h);
Mindy Pereira326c6602012-01-04 15:32:42 -0800910 }
911 }
912
913 private void toggleMessageDetails(View visibleDetailsView) {
mindyp1b3cc472012-09-27 11:32:59 -0700914 int heightBefore = measureHeight();
Andy Huangc7543572012-04-03 15:34:29 -0700915 final boolean detailsExpanded = (visibleDetailsView == mCollapsedDetailsView);
916 setMessageDetailsExpanded(detailsExpanded);
917 updateSpacerHeight();
mindyp1b3cc472012-09-27 11:32:59 -0700918 if (mCallbacks != null) {
919 mCallbacks.setMessageDetailsExpanded(mMessageHeaderItem, detailsExpanded, heightBefore);
920 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800921 }
922
923 private void setMessageDetailsExpanded(boolean expand) {
mindyp77a46052012-09-18 09:31:06 -0700924 if (mExpandMode == DEFAULT_MODE) {
925 if (expand) {
926 showExpandedDetails();
927 hideCollapsedDetails();
928 } else {
929 hideExpandedDetails();
930 showCollapsedDetails();
931 }
932 } else if (mExpandMode == POPUP_MODE) {
933 if (expand) {
934 showDetailsPopup();
935 } else {
936 hideDetailsPopup();
937 showCollapsedDetails();
938 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800939 }
Andy Huangc7543572012-04-03 15:34:29 -0700940 if (mMessageHeaderItem != null) {
941 mMessageHeaderItem.detailsExpanded = expand;
942 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800943 }
944
945 public void setMessageDetailsVisibility(int vis) {
946 if (vis == GONE) {
947 hideCollapsedDetails();
948 hideExpandedDetails();
Andrew Sapperstein3b08c512012-07-11 16:51:07 -0700949 hideSpamWarning();
Mindy Pereira326c6602012-01-04 15:32:42 -0800950 hideShowImagePrompt();
Andy Huanga71f1f32012-04-24 20:17:22 -0700951 hideInvite();
Mindy Pereira326c6602012-01-04 15:32:42 -0800952 } else {
Andy Huangc7543572012-04-03 15:34:29 -0700953 setMessageDetailsExpanded(mMessageHeaderItem.detailsExpanded);
Andrew Sapperstein3b08c512012-07-11 16:51:07 -0700954 if (mMessage.spamWarningString == null) {
955 hideSpamWarning();
956 } else {
957 showSpamWarning();
958 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800959 if (mShowImagePrompt) {
960 showImagePrompt();
Andy Huang3233bff2012-03-20 19:38:45 -0700961 } else {
962 hideShowImagePrompt();
Mindy Pereira326c6602012-01-04 15:32:42 -0800963 }
Andy Huanga71f1f32012-04-24 20:17:22 -0700964 if (mMessage.isFlaggedCalendarInvite()) {
965 showInvite();
966 } else {
967 hideInvite();
968 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800969 }
970 if (mBottomBorderView != null) {
971 mBottomBorderView.setVisibility(vis);
972 }
973 }
974
Mindy Pereira326c6602012-01-04 15:32:42 -0800975 public void hideMessageDetails() {
976 setMessageDetailsVisibility(GONE);
977 }
978
Mindy Pereira326c6602012-01-04 15:32:42 -0800979 private void hideCollapsedDetails() {
980 if (mCollapsedDetailsView != null) {
981 mCollapsedDetailsView.setVisibility(GONE);
982 }
983 }
984
985 private void hideExpandedDetails() {
986 if (mExpandedDetailsView != null) {
987 mExpandedDetailsView.setVisibility(GONE);
988 }
989 }
990
Andy Huanga71f1f32012-04-24 20:17:22 -0700991 private void hideInvite() {
992 if (mInviteView != null) {
993 mInviteView.setVisibility(GONE);
994 }
995 }
996
997 private void showInvite() {
998 if (mInviteView == null) {
999 mInviteView = (MessageInviteView) mInflater.inflate(
1000 R.layout.conversation_message_invite, this, false);
1001 addView(mInviteView);
1002 }
1003 mInviteView.bind(mMessage);
1004 mInviteView.setVisibility(VISIBLE);
1005 }
1006
Mindy Pereira326c6602012-01-04 15:32:42 -08001007 private void hideShowImagePrompt() {
1008 if (mImagePromptView != null) {
1009 mImagePromptView.setVisibility(GONE);
1010 }
1011 }
1012
1013 private void showImagePrompt() {
1014 if (mImagePromptView == null) {
Andy Huang3233bff2012-03-20 19:38:45 -07001015 ViewGroup v = (ViewGroup) mInflater.inflate(R.layout.conversation_message_show_pics,
1016 this, false);
Mindy Pereira326c6602012-01-04 15:32:42 -08001017 addView(v);
1018 v.setOnClickListener(this);
1019 v.setTag(SHOW_IMAGE_PROMPT_ONCE);
1020
1021 mImagePromptView = v;
1022 }
1023 mImagePromptView.setVisibility(VISIBLE);
1024 }
1025
Andrew Sapperstein3b08c512012-07-11 16:51:07 -07001026 private void hideSpamWarning() {
1027 if (mSpamWarningView != null) {
1028 mSpamWarningView.setVisibility(GONE);
1029 }
1030 }
1031
1032 private void showSpamWarning() {
1033 if (mSpamWarningView == null) {
1034 mSpamWarningView = (SpamWarningView)
1035 mInflater.inflate(R.layout.conversation_message_spam_warning, this, false);
1036 addView(mSpamWarningView);
1037 }
1038
1039 mSpamWarningView.showSpamWarning(mMessage, mSender);
1040 }
1041
Mindy Pereira326c6602012-01-04 15:32:42 -08001042 private void handleShowImagePromptClick(View v) {
1043 Integer state = (Integer) v.getTag();
1044 if (state == null) {
1045 return;
1046 }
1047 switch (state) {
1048 case SHOW_IMAGE_PROMPT_ONCE:
1049 if (mCallbacks != null) {
Andy Huang3233bff2012-03-20 19:38:45 -07001050 mCallbacks.showExternalResources(mMessage);
Mindy Pereira326c6602012-01-04 15:32:42 -08001051 }
1052 ImageView descriptionViewIcon = (ImageView) v.findViewById(R.id.show_pictures_icon);
1053 descriptionViewIcon.setContentDescription(getResources().getString(
1054 R.string.always_show_images));
1055 TextView descriptionView = (TextView) v.findViewById(R.id.show_pictures_text);
1056 descriptionView.setText(R.string.always_show_images);
1057 v.setTag(SHOW_IMAGE_PROMPT_ALWAYS);
Andy Huangc7543572012-04-03 15:34:29 -07001058 // the new text's line count may differ, so update the spacer height
1059 updateSpacerHeight();
Mindy Pereira326c6602012-01-04 15:32:42 -08001060 break;
1061 case SHOW_IMAGE_PROMPT_ALWAYS:
Andy Huang3233bff2012-03-20 19:38:45 -07001062 mMessage.markAlwaysShowImages(getQueryHandler(), 0 /* token */, null /* cookie */);
1063
Mindy Pereira326c6602012-01-04 15:32:42 -08001064 mShowImagePrompt = false;
1065 v.setTag(null);
1066 v.setVisibility(GONE);
Andy Huangc7543572012-04-03 15:34:29 -07001067 updateSpacerHeight();
Mindy Pereira326c6602012-01-04 15:32:42 -08001068 Toast.makeText(getContext(), R.string.always_show_images_toast, Toast.LENGTH_SHORT)
1069 .show();
1070 break;
1071 }
1072 }
1073
Andy Huang3233bff2012-03-20 19:38:45 -07001074 private AsyncQueryHandler getQueryHandler() {
1075 if (mQueryHandler == null) {
1076 mQueryHandler = new AsyncQueryHandler(getContext().getContentResolver()) {};
1077 }
1078 return mQueryHandler;
1079 }
1080
Mindy Pereira326c6602012-01-04 15:32:42 -08001081 /**
1082 * Makes collapsed details visible. If necessary, will inflate details
1083 * layout and render using saved-off state (senders, timestamp, etc).
1084 */
1085 private void showCollapsedDetails() {
1086 if (mCollapsedDetailsView == null) {
Andy Huang9f369592012-04-05 20:10:12 -07001087 mCollapsedDetailsView = (ViewGroup) mInflater.inflate(
1088 R.layout.conversation_message_details_header, this, false);
1089 addView(mCollapsedDetailsView, indexOfChild(mUpperHeaderView) + 1);
Mindy Pereira326c6602012-01-04 15:32:42 -08001090 mCollapsedDetailsView.setOnClickListener(this);
Andy Huangf70fc402012-02-17 15:37:42 -08001091 }
1092 if (!mCollapsedDetailsValid) {
Andy Huang65fe28f2012-04-06 18:08:53 -07001093 if (mMessageHeaderItem.recipientSummaryText == null) {
1094 mMessageHeaderItem.recipientSummaryText = getRecipientSummaryText(getContext(),
Andy Huang28b7aee2012-08-20 20:27:32 -07001095 getAccount().name, mTo, mCc, mBcc, mAddressCache);
Andy Huang65fe28f2012-04-06 18:08:53 -07001096 }
1097 ((TextView) findViewById(R.id.recipients_summary))
1098 .setText(mMessageHeaderItem.recipientSummaryText);
Mindy Pereira326c6602012-01-04 15:32:42 -08001099
1100 ((TextView) findViewById(R.id.date_summary)).setText(mTimestampShort);
Andy Huangf70fc402012-02-17 15:37:42 -08001101
1102 mCollapsedDetailsValid = true;
Mindy Pereira326c6602012-01-04 15:32:42 -08001103 }
1104 mCollapsedDetailsView.setVisibility(VISIBLE);
1105 }
1106
1107 /**
1108 * Makes expanded details visible. If necessary, will inflate expanded
1109 * details layout and render using saved-off state (senders, timestamp,
1110 * etc).
1111 */
1112 private void showExpandedDetails() {
1113 // lazily create expanded details view
Paul Westbrook63f9b482012-09-20 13:06:51 -07001114 final boolean expandedViewCreated = ensureExpandedDetailsView();
1115 if (expandedViewCreated) {
1116 addView(mExpandedDetailsView, indexOfChild(mUpperHeaderView) + 1);
1117 }
mindyp77a46052012-09-18 09:31:06 -07001118 mExpandedDetailsView.setVisibility(VISIBLE);
1119 }
1120
Paul Westbrook63f9b482012-09-20 13:06:51 -07001121 private boolean ensureExpandedDetailsView() {
1122 boolean viewCreated = false;
Mindy Pereira326c6602012-01-04 15:32:42 -08001123 if (mExpandedDetailsView == null) {
mindyp77a46052012-09-18 09:31:06 -07001124 View v = mInflater.inflate(R.layout.conversation_message_details_header_expanded, null,
1125 false);
Mindy Pereira326c6602012-01-04 15:32:42 -08001126 v.setOnClickListener(this);
1127
Andy Huangf70fc402012-02-17 15:37:42 -08001128 mExpandedDetailsView = (ViewGroup) v;
Paul Westbrook63f9b482012-09-20 13:06:51 -07001129 viewCreated = true;
Andy Huangf70fc402012-02-17 15:37:42 -08001130 }
1131 if (!mExpandedDetailsValid) {
Andy Huang65fe28f2012-04-06 18:08:53 -07001132 if (mMessageHeaderItem.timestampLong == null) {
1133 mMessageHeaderItem.timestampLong = mDateBuilder.formatLongDateTime(mTimestampMs);
1134 }
mindyp77a46052012-09-18 09:31:06 -07001135 ((TextView) mExpandedDetailsView.findViewById(R.id.date_value))
1136 .setText(mMessageHeaderItem.timestampLong);
1137 renderEmailList(R.id.replyto_row, R.id.replyto_value, mReplyTo, false,
1138 mExpandedDetailsView);
Andrew Sappersteind5b369b2012-07-13 12:38:46 -07001139 if (mMessage.viaDomain != null) {
mindyp77a46052012-09-18 09:31:06 -07001140 renderEmailList(R.id.from_row, R.id.from_value, mFrom, true, mExpandedDetailsView);
Andrew Sappersteind5b369b2012-07-13 12:38:46 -07001141 }
mindyp77a46052012-09-18 09:31:06 -07001142 renderEmailList(R.id.to_row, R.id.to_value, mTo, false, mExpandedDetailsView);
1143 renderEmailList(R.id.cc_row, R.id.cc_value, mCc, false, mExpandedDetailsView);
1144 renderEmailList(R.id.bcc_row, R.id.bcc_value, mBcc, false, mExpandedDetailsView);
Mindy Pereira326c6602012-01-04 15:32:42 -08001145
Andy Huangf70fc402012-02-17 15:37:42 -08001146 mExpandedDetailsValid = true;
Mindy Pereira326c6602012-01-04 15:32:42 -08001147 }
Paul Westbrook63f9b482012-09-20 13:06:51 -07001148 return viewCreated;
mindyp77a46052012-09-18 09:31:06 -07001149 }
1150
1151 private void showDetailsPopup() {
1152 ensureExpandedDetailsView();
1153 if (mDetailsPopup == null) {
1154 AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
1155 mExpandedDetailsView.findViewById(R.id.details_expander)
1156 .setVisibility(View.GONE);
1157 builder.setView(mExpandedDetailsView)
1158 .setCancelable(true)
1159 .setTitle(getContext().getString(R.string.message_details_title));
1160 mDetailsPopup = builder.show();
1161 } else {
1162 mDetailsPopup.show();
1163 }
1164 }
1165
1166 private void hideDetailsPopup() {
1167 if (mDetailsPopup != null) {
1168 mDetailsPopup.hide();
1169 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001170 }
1171
Mindy Pereira326c6602012-01-04 15:32:42 -08001172 /**
1173 * Returns a short plaintext snippet generated from the given HTML message
1174 * body. Collapses whitespace, ignores '&lt;' and '&gt;' characters and
1175 * everything in between, and truncates the snippet to no more than 100
1176 * characters.
1177 *
1178 * @return Short plaintext snippet
1179 */
1180 @VisibleForTesting
1181 static String makeSnippet(final String messageBody) {
Andy Huange805ab42012-08-09 20:05:58 -07001182 if (TextUtils.isEmpty(messageBody)) {
1183 return null;
1184 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001185
Andy Huange805ab42012-08-09 20:05:58 -07001186 final StringBuilder snippet = new StringBuilder(MAX_SNIPPET_LENGTH);
1187
1188 final StringReader reader = new StringReader(messageBody);
Mindy Pereira326c6602012-01-04 15:32:42 -08001189 try {
1190 int c;
1191 while ((c = reader.read()) != -1 && snippet.length() < MAX_SNIPPET_LENGTH) {
1192 // Collapse whitespace.
1193 if (Character.isWhitespace(c)) {
1194 snippet.append(' ');
1195 do {
1196 c = reader.read();
1197 } while (Character.isWhitespace(c));
1198 if (c == -1) {
1199 break;
1200 }
1201 }
1202
1203 if (c == '<') {
1204 // Ignore everything up to and including the next '>'
1205 // character.
1206 while ((c = reader.read()) != -1) {
1207 if (c == '>') {
1208 break;
1209 }
1210 }
1211
1212 // If we reached the end of the message body, exit.
1213 if (c == -1) {
1214 break;
1215 }
1216 } else if (c == '&') {
1217 // Read HTML entity.
1218 StringBuilder sb = new StringBuilder();
1219
1220 while ((c = reader.read()) != -1) {
1221 if (c == ';') {
1222 break;
1223 }
1224 sb.append((char) c);
1225 }
1226
1227 String entity = sb.toString();
1228 if ("nbsp".equals(entity)) {
1229 snippet.append(' ');
1230 } else if ("lt".equals(entity)) {
1231 snippet.append('<');
1232 } else if ("gt".equals(entity)) {
1233 snippet.append('>');
1234 } else if ("amp".equals(entity)) {
1235 snippet.append('&');
1236 } else if ("quot".equals(entity)) {
1237 snippet.append('"');
1238 } else if ("apos".equals(entity) || "#39".equals(entity)) {
1239 snippet.append('\'');
1240 } else {
1241 // Unknown entity; just append the literal string.
1242 snippet.append('&').append(entity);
1243 if (c == ';') {
1244 snippet.append(';');
1245 }
1246 }
1247
1248 // If we reached the end of the message body, exit.
1249 if (c == -1) {
1250 break;
1251 }
1252 } else {
1253 // The current character is a non-whitespace character that
1254 // isn't inside some
1255 // HTML tag and is not part of an HTML entity.
1256 snippet.append((char) c);
1257 }
1258 }
1259 } catch (IOException e) {
1260 LogUtils.wtf(LOG_TAG, e, "Really? IOException while reading a freaking string?!? ");
1261 }
1262
1263 return snippet.toString();
1264 }
1265
1266 @Override
Mindy Pereira326c6602012-01-04 15:32:42 -08001267 protected void onLayout(boolean changed, int l, int t, int r, int b) {
1268 Timer perf = new Timer();
1269 perf.start(LAYOUT_TAG);
1270 super.onLayout(changed, l, t, r, b);
1271 perf.pause(LAYOUT_TAG);
1272 }
1273
1274 @Override
1275 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1276 Timer t = new Timer();
1277 if (Timer.ENABLE_TIMER && !mPreMeasuring) {
Andy Huang3233bff2012-03-20 19:38:45 -07001278 t.count("header measure id=" + mMessage.id);
Mindy Pereira326c6602012-01-04 15:32:42 -08001279 t.start(MEASURE_TAG);
1280 }
1281 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1282 if (!mPreMeasuring) {
1283 t.pause(MEASURE_TAG);
1284 }
1285 }
Andy Huangbb56a152012-02-24 18:26:47 -08001286
Mindy Pereira326c6602012-01-04 15:32:42 -08001287}