blob: 6ee746d67730d25d64f99d359be85c4d94ad6fc4 [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 Huang28b7aee2012-08-20 20:27:32 -070047import com.android.mail.browse.ConversationViewAdapter.ConversationAccountController;
Andy Huangc7543572012-04-03 15:34:29 -070048import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
Andy Huang839ada22012-07-20 15:48:40 -070049import com.android.mail.browse.MessageCursor.ConversationMessage;
Andy Huang30e2c242012-01-06 18:14:30 -080050import com.android.mail.compose.ComposeActivity;
51import com.android.mail.perf.Timer;
Mindy Pereira33fe9082012-01-09 16:24:30 -080052import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080053import com.android.mail.providers.Address;
Mark Wei0a135882012-10-16 17:11:02 -070054import com.android.mail.providers.Folder;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080055import com.android.mail.providers.Message;
Andy Huang30e2c242012-01-06 18:14:30 -080056import com.android.mail.providers.UIProvider;
Paul Westbrookb334c902012-06-25 11:42:46 -070057import com.android.mail.utils.LogTag;
Andy Huang30e2c242012-01-06 18:14:30 -080058import com.android.mail.utils.LogUtils;
59import com.android.mail.utils.Utils;
Marc Blankbec51152012-03-22 19:27:34 -070060import com.google.common.annotations.VisibleForTesting;
Mindy Pereira326c6602012-01-04 15:32:42 -080061
62import java.io.IOException;
63import java.io.StringReader;
Mark Wei0a135882012-10-16 17:11:02 -070064import java.util.ArrayList;
Andy Huang65fe28f2012-04-06 18:08:53 -070065import java.util.Map;
Mindy Pereira326c6602012-01-04 15:32:42 -080066
Mindy Pereira326c6602012-01-04 15:32:42 -080067public class MessageHeaderView extends LinearLayout implements OnClickListener,
Andy Huang31c38a82012-08-15 21:39:43 -070068 OnMenuItemClickListener, ConversationContainer.DetachListener {
Mindy Pereira326c6602012-01-04 15:32:42 -080069
70 /**
71 * Cap very long recipient lists during summary construction for efficiency.
72 */
73 private static final int SUMMARY_MAX_RECIPIENTS = 50;
74
75 private static final int MAX_SNIPPET_LENGTH = 100;
76
77 private static final int SHOW_IMAGE_PROMPT_ONCE = 1;
78 private static final int SHOW_IMAGE_PROMPT_ALWAYS = 2;
79
80 private static final String HEADER_INFLATE_TAG = "message header inflate";
81 private static final String HEADER_ADDVIEW_TAG = "message header addView";
82 private static final String HEADER_RENDER_TAG = "message header render";
83 private static final String PREMEASURE_TAG = "message header pre-measure";
84 private static final String LAYOUT_TAG = "message header layout";
85 private static final String MEASURE_TAG = "message header measure";
86
87 private static final String RECIPIENT_HEADING_DELIMITER = " ";
88
Paul Westbrookb334c902012-06-25 11:42:46 -070089 private static final String LOG_TAG = LogTag.getLogTag();
Mindy Pereira326c6602012-01-04 15:32:42 -080090
mindyp77a46052012-09-18 09:31:06 -070091 public static final int DEFAULT_MODE = 0;
92
93 public static final int POPUP_MODE = 1;
94
Mindy Pereira326c6602012-01-04 15:32:42 -080095 private MessageHeaderViewCallbacks mCallbacks;
Mindy Pereira326c6602012-01-04 15:32:42 -080096
Andy Huang9f369592012-04-05 20:10:12 -070097 private ViewGroup mUpperHeaderView;
Mindy Pereira326c6602012-01-04 15:32:42 -080098 private TextView mSenderNameView;
99 private TextView mSenderEmailView;
100 private QuickContactBadge mPhotoView;
101 private ImageView mStarView;
102 private ViewGroup mTitleContainerView;
103 private ViewGroup mCollapsedDetailsView;
104 private ViewGroup mExpandedDetailsView;
Andrew Sapperstein3b08c512012-07-11 16:51:07 -0700105 private SpamWarningView mSpamWarningView;
Mindy Pereira326c6602012-01-04 15:32:42 -0800106 private ViewGroup mImagePromptView;
Andy Huanga71f1f32012-04-24 20:17:22 -0700107 private MessageInviteView mInviteView;
Mindy Pereira326c6602012-01-04 15:32:42 -0800108 private View mBottomBorderView;
109 private ImageView mPresenceView;
Andy Huang65fe28f2012-04-06 18:08:53 -0700110 private View mPhotoSpacerView;
111 private View mForwardButton;
112 private View mOverflowButton;
113 private View mDraftIcon;
114 private View mEditDraftButton;
115 private TextView mUpperDateView;
116 private View mReplyButton;
117 private View mReplyAllButton;
118 private View mAttachmentIcon;
Mark Wei1dc57532012-09-13 14:56:50 -0700119 private View mLeftSpacer;
120 private View mRightSpacer;
Mindy Pereira326c6602012-01-04 15:32:42 -0800121
122 // temporary fields to reference raw data between initial render and details
123 // expansion
Andrew Sappersteind5b369b2012-07-13 12:38:46 -0700124 private String[] mFrom;
Mindy Pereira326c6602012-01-04 15:32:42 -0800125 private String[] mTo;
126 private String[] mCc;
127 private String[] mBcc;
128 private String[] mReplyTo;
129 private long mTimestampMs;
130 private FormattedDateBuilder mDateBuilder;
131
132 private boolean mIsDraft = false;
133
134 private boolean mIsSending;
135
Mindy Pereira326c6602012-01-04 15:32:42 -0800136 /**
137 * The snappy header has special visibility rules (i.e. no details header,
138 * even though it has an expanded appearance)
139 */
140 private boolean mIsSnappy;
141
142 private String mSnippet;
143
144 private Address mSender;
145
146 private ContactInfoSource mContactInfoSource;
147
148 private boolean mPreMeasuring;
149
Andy Huang28b7aee2012-08-20 20:27:32 -0700150 private ConversationAccountController mAccountController;
Mindy Pereira326c6602012-01-04 15:32:42 -0800151
Andy Huang65fe28f2012-04-06 18:08:53 -0700152 private Map<String, Address> mAddressCache;
153
Mindy Pereira326c6602012-01-04 15:32:42 -0800154 private boolean mShowImagePrompt;
155
Mindy Pereira326c6602012-01-04 15:32:42 -0800156 private CharSequence mTimestampShort;
157
158 /**
159 * Take the initial visibility of the star view to mean its collapsed
160 * visibility. Star is always visible when expanded, but sometimes, like on
161 * phones, there isn't enough room to warrant showing star when collapsed.
162 */
Mark Wei0a135882012-10-16 17:11:02 -0700163 private boolean mCollapsedStarVisible;
164 private boolean mStarShown;
Mindy Pereira326c6602012-01-04 15:32:42 -0800165
166 /**
167 * Take the initial right margin of the header title container to mean its
168 * right margin when collapsed. There's currently no need for additional
169 * margin when expanded, but if that need ever arises, title_container can
170 * simply tack on some extra right padding.
171 */
172 private int mTitleContainerCollapsedMarginRight;
173
Mindy Pereira326c6602012-01-04 15:32:42 -0800174 private PopupMenu mPopup;
175
Andy Huangc7543572012-04-03 15:34:29 -0700176 private MessageHeaderItem mMessageHeaderItem;
Andy Huang839ada22012-07-20 15:48:40 -0700177 private ConversationMessage mMessage;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800178
Andy Huangf70fc402012-02-17 15:37:42 -0800179 private boolean mCollapsedDetailsValid;
180 private boolean mExpandedDetailsValid;
181
Andy Huang88fc42e2012-03-08 15:02:43 -0800182 private final LayoutInflater mInflater;
183
Andy Huang3233bff2012-03-20 19:38:45 -0700184 private AsyncQueryHandler mQueryHandler;
185
Andy Huang59e0b182012-08-14 14:32:23 -0700186 private boolean mObservingContactInfo;
187
Andy Huangb8331b42012-07-16 19:08:53 -0700188 private final DataSetObserver mContactInfoObserver = new DataSetObserver() {
189 @Override
190 public void onChanged() {
191 updateContactInfo();
192 }
193 };
194
mindypf4fce122012-09-14 15:55:33 -0700195 private boolean mExpandable = true;
196
mindyp77a46052012-09-18 09:31:06 -0700197 private int mExpandMode = DEFAULT_MODE;
198
199 private AlertDialog mDetailsPopup;
200
Andy Huang3233bff2012-03-20 19:38:45 -0700201 public interface MessageHeaderViewCallbacks {
Andy Huangc7543572012-04-03 15:34:29 -0700202 void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeight);
Andy Huang3233bff2012-03-20 19:38:45 -0700203
Andy Huangc7543572012-04-03 15:34:29 -0700204 void setMessageExpanded(MessageHeaderItem item, int newSpacerHeight);
Andy Huang3233bff2012-03-20 19:38:45 -0700205
mindyp1b3cc472012-09-27 11:32:59 -0700206 void setMessageDetailsExpanded(MessageHeaderItem messageHeaderItem, boolean expanded,
207 int previousMessageHeaderItemHeight);
208
Andy Huang3233bff2012-03-20 19:38:45 -0700209 void showExternalResources(Message msg);
210 }
211
Mindy Pereira326c6602012-01-04 15:32:42 -0800212 public MessageHeaderView(Context context) {
213 this(context, null);
214 }
215
216 public MessageHeaderView(Context context, AttributeSet attrs) {
217 this(context, attrs, -1);
218 }
219
220 public MessageHeaderView(Context context, AttributeSet attrs, int defStyle) {
221 super(context, attrs, defStyle);
Andy Huang88fc42e2012-03-08 15:02:43 -0800222
223 mInflater = LayoutInflater.from(context);
Mindy Pereira326c6602012-01-04 15:32:42 -0800224 }
225
mindyp77a46052012-09-18 09:31:06 -0700226 /**
227 * Expand mode is DEFAULT_MODE by default.
228 */
229 public void setExpandMode(int mode) {
230 mExpandMode = mode;
231 }
232
Mindy Pereira326c6602012-01-04 15:32:42 -0800233 @Override
234 protected void onFinishInflate() {
235 super.onFinishInflate();
Andy Huang9f369592012-04-05 20:10:12 -0700236 mUpperHeaderView = (ViewGroup) findViewById(R.id.upper_header);
Mindy Pereira326c6602012-01-04 15:32:42 -0800237 mSenderNameView = (TextView) findViewById(R.id.sender_name);
238 mSenderEmailView = (TextView) findViewById(R.id.sender_email);
239 mPhotoView = (QuickContactBadge) findViewById(R.id.photo);
Andy Huang65fe28f2012-04-06 18:08:53 -0700240 mPhotoSpacerView = findViewById(R.id.photo_spacer);
241 mReplyButton = findViewById(R.id.reply);
242 mReplyAllButton = findViewById(R.id.reply_all);
243 mForwardButton = findViewById(R.id.forward);
Mindy Pereira326c6602012-01-04 15:32:42 -0800244 mStarView = (ImageView) findViewById(R.id.star);
245 mPresenceView = (ImageView) findViewById(R.id.presence);
246 mTitleContainerView = (ViewGroup) findViewById(R.id.title_container);
Andy Huang65fe28f2012-04-06 18:08:53 -0700247 mOverflowButton = findViewById(R.id.overflow);
248 mDraftIcon = findViewById(R.id.draft);
249 mEditDraftButton = findViewById(R.id.edit_draft);
250 mUpperDateView = (TextView) findViewById(R.id.upper_date);
251 mAttachmentIcon = findViewById(R.id.attachment);
Mindy Pereira326c6602012-01-04 15:32:42 -0800252
Mark Wei0a135882012-10-16 17:11:02 -0700253 mCollapsedStarVisible = mStarView.getVisibility() == VISIBLE;
Mindy Pereira326c6602012-01-04 15:32:42 -0800254 mTitleContainerCollapsedMarginRight = ((MarginLayoutParams) mTitleContainerView
255 .getLayoutParams()).rightMargin;
256
Andy Huang9f369592012-04-05 20:10:12 -0700257 mBottomBorderView = findViewById(R.id.details_bottom_border);
Mark Wei1dc57532012-09-13 14:56:50 -0700258 mLeftSpacer = findViewById(R.id.left_spacer);
259 mRightSpacer = findViewById(R.id.right_spacer);
Andy Huang9f369592012-04-05 20:10:12 -0700260
Mindy Pereira326c6602012-01-04 15:32:42 -0800261 setExpanded(true);
262
263 registerMessageClickTargets(R.id.reply, R.id.reply_all, R.id.forward, R.id.star,
264 R.id.edit_draft, R.id.overflow, R.id.upper_header);
265 }
266
267 private void registerMessageClickTargets(int... ids) {
268 for (int id : ids) {
269 View v = findViewById(id);
270 if (v != null) {
271 v.setOnClickListener(this);
272 }
273 }
274 }
275
Mindy Pereira326c6602012-01-04 15:32:42 -0800276 /**
277 * Associate the header with a contact info source for later contact
278 * presence/photo lookup.
279 */
280 public void setContactInfoSource(ContactInfoSource contactInfoSource) {
281 mContactInfoSource = contactInfoSource;
282 }
283
284 public void setCallbacks(MessageHeaderViewCallbacks callbacks) {
285 mCallbacks = callbacks;
286 }
287
288 /**
289 * Find the header view corresponding to a message with given local ID.
290 *
291 * @param parent the view parent to search within
292 * @param localMessageId local message ID
293 * @return a header view or null
294 */
295 public static MessageHeaderView find(ViewGroup parent, long localMessageId) {
296 return (MessageHeaderView) parent.findViewWithTag(localMessageId);
297 }
298
Mindy Pereira326c6602012-01-04 15:32:42 -0800299 public boolean isExpanded() {
Andy Huangc7543572012-04-03 15:34:29 -0700300 // (let's just arbitrarily say that unbound views are expanded by default)
301 return mMessageHeaderItem == null || mMessageHeaderItem.isExpanded();
Mindy Pereira326c6602012-01-04 15:32:42 -0800302 }
303
Mindy Pereira326c6602012-01-04 15:32:42 -0800304 public void setSnappy(boolean snappy) {
305 mIsSnappy = snappy;
306 hideMessageDetails();
307 if (snappy) {
308 setBackgroundDrawable(null);
Mark Wei1dc57532012-09-13 14:56:50 -0700309 // snappy header overlay has no padding so we need spacers
310 mLeftSpacer.setVisibility(View.VISIBLE);
311 mRightSpacer.setVisibility(View.VISIBLE);
Mindy Pereira326c6602012-01-04 15:32:42 -0800312 } else {
313 setBackgroundColor(android.R.color.white);
Mark Wei1dc57532012-09-13 14:56:50 -0700314 // scrolling layer does have padding so we don't need spacers
315 mLeftSpacer.setVisibility(View.GONE);
316 mRightSpacer.setVisibility(View.GONE);
Mindy Pereira326c6602012-01-04 15:32:42 -0800317 }
318 }
319
Andy Huangb8331b42012-07-16 19:08:53 -0700320 @Override
321 public void onDetachedFromParent() {
322 unbind();
323 }
324
Mindy Pereira326c6602012-01-04 15:32:42 -0800325 /**
326 * Headers that are unbound will not match any rendered header (matches()
327 * will return false). Unbinding is not guaranteed to *hide* the view's old
328 * data, though. To re-bind this header to message data, call render() or
329 * renderUpperHeaderFrom().
330 */
331 public void unbind() {
Andy Huangc7543572012-04-03 15:34:29 -0700332 mMessageHeaderItem = null;
Andy Huang3233bff2012-03-20 19:38:45 -0700333 mMessage = null;
Andy Huangb8331b42012-07-16 19:08:53 -0700334
Andy Huang59e0b182012-08-14 14:32:23 -0700335 if (mObservingContactInfo) {
336 mContactInfoSource.unregisterObserver(mContactInfoObserver);
337 mObservingContactInfo = false;
338 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800339 }
340
Andy Huang28b7aee2012-08-20 20:27:32 -0700341 public void initialize(FormattedDateBuilder dateBuilder,
342 ConversationAccountController accountController,
Andy Huang65fe28f2012-04-06 18:08:53 -0700343 Map<String, Address> addressCache) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800344 mDateBuilder = dateBuilder;
Andy Huang28b7aee2012-08-20 20:27:32 -0700345 mAccountController = accountController;
Andy Huang65fe28f2012-04-06 18:08:53 -0700346 mAddressCache = addressCache;
Mindy Pereira326c6602012-01-04 15:32:42 -0800347 }
348
Andy Huang28b7aee2012-08-20 20:27:32 -0700349 private Account getAccount() {
350 return mAccountController.getAccount();
351 }
352
353 public void bind(MessageHeaderItem headerItem, boolean measureOnly) {
Andy Huang59e0b182012-08-14 14:32:23 -0700354 if (mMessageHeaderItem != null && mMessageHeaderItem == headerItem) {
355 return;
356 }
357
Andy Huang6b3d0d92012-10-30 15:46:48 -0700358 mMessageHeaderItem = headerItem;
359 render(measureOnly);
360 }
361
362 public void refresh() {
363 render(false);
364 }
365
366 private void render(boolean measureOnly) {
Andy Huang4baafcb2012-11-01 18:01:49 -0700367 if (mMessageHeaderItem == null) {
368 return;
369 }
370
Mindy Pereira326c6602012-01-04 15:32:42 -0800371 Timer t = new Timer();
372 t.start(HEADER_RENDER_TAG);
373
Andy Huangf70fc402012-02-17 15:37:42 -0800374 mCollapsedDetailsValid = false;
375 mExpandedDetailsValid = false;
376
Andy Huang6b3d0d92012-10-30 15:46:48 -0700377 mMessage = mMessageHeaderItem.getMessage();
Andy Huangc7543572012-04-03 15:34:29 -0700378 mShowImagePrompt = mMessage.shouldShowImagePrompt();
Andy Huang6b3d0d92012-10-30 15:46:48 -0700379 setExpanded(mMessageHeaderItem.isExpanded());
Mindy Pereira326c6602012-01-04 15:32:42 -0800380
Andy Huangf70fc402012-02-17 15:37:42 -0800381 mTimestampMs = mMessage.dateReceivedMs;
Andy Huang6b3d0d92012-10-30 15:46:48 -0700382 mTimestampShort = mMessageHeaderItem.timestampShort;
Andy Huang65fe28f2012-04-06 18:08:53 -0700383 if (mTimestampShort == null) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800384 mTimestampShort = mDateBuilder.formatShortDate(mTimestampMs);
Andy Huang6b3d0d92012-10-30 15:46:48 -0700385 mMessageHeaderItem.timestampShort = mTimestampShort;
Mindy Pereira326c6602012-01-04 15:32:42 -0800386 }
387
Andrew Sappersteind5b369b2012-07-13 12:38:46 -0700388 mFrom = mMessage.getFromAddresses();
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700389 mTo = mMessage.getToAddresses();
390 mCc = mMessage.getCcAddresses();
391 mBcc = mMessage.getBccAddresses();
392 mReplyTo = mMessage.getReplyToAddresses();
Mindy Pereira326c6602012-01-04 15:32:42 -0800393
394 /**
395 * Turns draft mode on or off. Draft mode hides message operations other
396 * than "edit", hides contact photo, hides presence, and changes the
397 * sender name to "Draft".
398 */
Andy Huangf70fc402012-02-17 15:37:42 -0800399 mIsDraft = mMessage.draftType != UIProvider.DraftType.NOT_A_DRAFT;
Andy Huang2a1e8e32012-10-23 18:54:57 -0700400 mIsSending = mMessage.isSending;
Mindy Pereira326c6602012-01-04 15:32:42 -0800401
Mindy Pereira326c6602012-01-04 15:32:42 -0800402 // If this was a sent message AND:
403 // 1. the account has a custom from, the cursor will populate the
404 // selected custom from as the fromAddress when a message is sent but
405 // not yet synced.
406 // 2. the account has no custom froms, fromAddress will be empty, and we
407 // can safely fall back and show the account name as sender since it's
408 // the only possible fromAddress.
Andy Huangf70fc402012-02-17 15:37:42 -0800409 String from = mMessage.from;
Mindy Pereira326c6602012-01-04 15:32:42 -0800410 if (TextUtils.isEmpty(from)) {
Andy Huang28b7aee2012-08-20 20:27:32 -0700411 from = getAccount().name;
Mindy Pereira326c6602012-01-04 15:32:42 -0800412 }
Andy Huang65fe28f2012-04-06 18:08:53 -0700413 mSender = getAddress(from);
Mindy Pereira326c6602012-01-04 15:32:42 -0800414
Mark Wei0a135882012-10-16 17:11:02 -0700415 mStarView.setSelected(mMessage.starred);
416 mStarView.setContentDescription(getResources().getString(
417 mStarView.isSelected() ? R.string.remove_star : R.string.add_star));
418 mStarShown = true;
419 ArrayList<Folder> folders = mMessage.conversation.getRawFolders();
420 for (Folder folder : folders) {
421 if (folder.isTrash()) {
422 mStarShown = false;
423 break;
424 }
425 }
426
Andrew Sapperstein3b08c512012-07-11 16:51:07 -0700427 updateChildVisibility();
428
Andy Huang2a1e8e32012-10-23 18:54:57 -0700429 if (mIsDraft || mIsSending) {
Andrew Sapperstein3b08c512012-07-11 16:51:07 -0700430 mSnippet = makeSnippet(mMessage.snippet);
431 } else {
432 mSnippet = mMessage.snippet;
433 }
434
Mindy Pereira326c6602012-01-04 15:32:42 -0800435 mSenderNameView.setText(getHeaderTitle());
436 mSenderEmailView.setText(getHeaderSubtitle());
437
Andy Huang65fe28f2012-04-06 18:08:53 -0700438 if (mUpperDateView != null) {
439 mUpperDateView.setText(mTimestampShort);
Mindy Pereira326c6602012-01-04 15:32:42 -0800440 }
441
Andy Huang59e0b182012-08-14 14:32:23 -0700442 if (measureOnly) {
443 // avoid leaving any state around that would interfere with future regular bind() calls
444 unbind();
445 } else {
Andy Huangb8331b42012-07-16 19:08:53 -0700446 updateContactInfo();
Andy Huang59e0b182012-08-14 14:32:23 -0700447 if (!mObservingContactInfo) {
448 mContactInfoSource.registerObserver(mContactInfoObserver);
449 mObservingContactInfo = true;
450 }
Andy Huangb8331b42012-07-16 19:08:53 -0700451 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800452
453 t.pause(HEADER_RENDER_TAG);
Mindy Pereira326c6602012-01-04 15:32:42 -0800454 }
455
Andy Huang4baafcb2012-11-01 18:01:49 -0700456 public boolean isBoundTo(ConversationOverlayItem item) {
457 return item == mMessageHeaderItem;
458 }
459
Andy Huang65fe28f2012-04-06 18:08:53 -0700460 private Address getAddress(String emailStr) {
461 return getAddress(mAddressCache, emailStr);
462 }
463
464 private static Address getAddress(Map<String, Address> cache, String emailStr) {
465 Address addr = null;
466 if (cache != null) {
467 addr = cache.get(emailStr);
468 }
469 if (addr == null) {
470 addr = Address.getEmailAddress(emailStr);
471 if (cache != null) {
472 cache.put(emailStr, addr);
473 }
474 }
475 return addr;
476 }
477
Andy Huangc7543572012-04-03 15:34:29 -0700478 private void updateSpacerHeight() {
479 final int h = measureHeight();
480
481 mMessageHeaderItem.setHeight(h);
482 if (mCallbacks != null) {
483 mCallbacks.setMessageSpacerHeight(mMessageHeaderItem, h);
484 }
485 }
486
487 private int measureHeight() {
488 ViewGroup parent = (ViewGroup) getParent();
Mindy Pereira326c6602012-01-04 15:32:42 -0800489 if (parent == null) {
Andy Huangc7543572012-04-03 15:34:29 -0700490 LogUtils.e(LOG_TAG, new Error(), "Unable to measure height of detached header");
Mindy Pereira326c6602012-01-04 15:32:42 -0800491 return getHeight();
492 }
493 mPreMeasuring = true;
Andy Huangc7543572012-04-03 15:34:29 -0700494 final int h = Utils.measureViewHeight(this, parent);
Mindy Pereira326c6602012-01-04 15:32:42 -0800495 mPreMeasuring = false;
496 return h;
497 }
498
499 private CharSequence getHeaderTitle() {
500 CharSequence title;
501
502 if (mIsDraft) {
503 title = getResources().getQuantityText(R.plurals.draft, 1);
504 } else if (mIsSending) {
505 title = getResources().getString(R.string.sending);
506 } else {
507 title = getSenderName(mSender);
508 }
509
510 return title;
511 }
512
513 private CharSequence getHeaderSubtitle() {
514 CharSequence sub;
515 if (mIsSending) {
516 sub = null;
517 } else {
Andrew Sappersteind5b369b2012-07-13 12:38:46 -0700518 if (isExpanded()) {
519 if (mMessage.viaDomain != null) {
520 sub = getResources().getString(
521 R.string.via_domain, mMessage.viaDomain);
522 } else {
523 sub = getSenderAddress(mSender);
524 }
525 } else {
526 sub = mSnippet;
527 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800528 }
529 return sub;
530 }
531
532 /**
533 * Return the name, if known, or just the address.
534 */
535 private static CharSequence getSenderName(Address sender) {
Andy Huang65fe28f2012-04-06 18:08:53 -0700536 final String displayName = sender.getName();
537 return TextUtils.isEmpty(displayName) ? sender.getAddress() : displayName;
Mindy Pereira326c6602012-01-04 15:32:42 -0800538 }
539
540 /**
541 * Return the address, if a name is present, or null if not.
542 */
543 private static CharSequence getSenderAddress(Address sender) {
544 String displayName = sender == null ? "" : sender.getName();
545 return TextUtils.isEmpty(displayName) ? null : sender.getAddress();
546 }
547
Andy Huang65fe28f2012-04-06 18:08:53 -0700548 private void setChildVisibility(int visibility, View... children) {
549 for (View v : children) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800550 if (v != null) {
551 v.setVisibility(visibility);
552 }
553 }
554 }
555
556 private void setExpanded(final boolean expanded) {
557 // use View's 'activated' flag to store expanded state
558 // child view state lists can use this to toggle drawables
559 setActivated(expanded);
Andy Huangc7543572012-04-03 15:34:29 -0700560 if (mMessageHeaderItem != null) {
561 mMessageHeaderItem.setExpanded(expanded);
562 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800563 }
564
565 /**
566 * Update the visibility of the many child views based on expanded/collapsed
567 * and draft/normal state.
568 */
569 private void updateChildVisibility() {
Mindy Pereira326c6602012-01-04 15:32:42 -0800570 // Too bad this can't be done with an XML state list...
571
Andy Huangc7543572012-04-03 15:34:29 -0700572 if (isExpanded()) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800573 int normalVis, draftVis;
574
575 setMessageDetailsVisibility((mIsSnappy) ? GONE : VISIBLE);
576
577 if (mIsDraft) {
578 normalVis = GONE;
579 draftVis = VISIBLE;
580 } else {
581 normalVis = VISIBLE;
582 draftVis = GONE;
583 }
584
585 setReplyOrReplyAllVisible();
Andy Huang65fe28f2012-04-06 18:08:53 -0700586 setChildVisibility(normalVis, mPhotoView, mPhotoSpacerView, mForwardButton,
587 mSenderEmailView, mOverflowButton);
588 setChildVisibility(draftVis, mDraftIcon, mEditDraftButton);
589 setChildVisibility(GONE, mAttachmentIcon, mUpperDateView);
Mark Wei0a135882012-10-16 17:11:02 -0700590 setChildVisibility(mStarShown ? VISIBLE : GONE, mStarView);
Mindy Pereira326c6602012-01-04 15:32:42 -0800591
592 setChildMarginRight(mTitleContainerView, 0);
593
594 } else {
595
596 setMessageDetailsVisibility(GONE);
Andy Huang65fe28f2012-04-06 18:08:53 -0700597 setChildVisibility(VISIBLE, mSenderEmailView, mUpperDateView);
Mindy Pereira326c6602012-01-04 15:32:42 -0800598
Andy Huang65fe28f2012-04-06 18:08:53 -0700599 setChildVisibility(GONE, mEditDraftButton, mReplyButton, mReplyAllButton,
600 mForwardButton);
601 setChildVisibility(GONE, mOverflowButton);
Mindy Pereira326c6602012-01-04 15:32:42 -0800602
Andy Huang88fc42e2012-03-08 15:02:43 -0800603 setChildVisibility(mMessage.hasAttachments ? VISIBLE : GONE,
Andy Huang65fe28f2012-04-06 18:08:53 -0700604 mAttachmentIcon);
Mindy Pereira326c6602012-01-04 15:32:42 -0800605
Mark Wei0a135882012-10-16 17:11:02 -0700606 setChildVisibility(mCollapsedStarVisible && mStarShown ? VISIBLE : GONE, mStarView);
Mindy Pereira326c6602012-01-04 15:32:42 -0800607
608 setChildMarginRight(mTitleContainerView, mTitleContainerCollapsedMarginRight);
609
610 if (mIsDraft) {
611
Andy Huang65fe28f2012-04-06 18:08:53 -0700612 setChildVisibility(VISIBLE, mDraftIcon);
613 setChildVisibility(GONE, mPhotoView, mPhotoSpacerView);
Mindy Pereira326c6602012-01-04 15:32:42 -0800614
615 } else {
616
Andy Huang65fe28f2012-04-06 18:08:53 -0700617 setChildVisibility(GONE, mDraftIcon);
618 setChildVisibility(VISIBLE, mPhotoView, mPhotoSpacerView);
Mindy Pereira326c6602012-01-04 15:32:42 -0800619
620 }
621 }
622
623 }
624
625 /**
626 * If an overflow menu is present in this header's layout, set the
627 * visibility of "Reply" and "Reply All" actions based on a user preference.
628 * Only one of those actions will be visible when an overflow is present. If
629 * no overflow is present (e.g. big phone or tablet), it's assumed we have
630 * plenty of screen real estate and can show both.
631 */
632 private void setReplyOrReplyAllVisible() {
633 if (mIsDraft) {
Andy Huang65fe28f2012-04-06 18:08:53 -0700634 setChildVisibility(GONE, mReplyButton, mReplyAllButton);
Mindy Pereira326c6602012-01-04 15:32:42 -0800635 return;
Andy Huang65fe28f2012-04-06 18:08:53 -0700636 } else if (mOverflowButton == null) {
637 setChildVisibility(VISIBLE, mReplyButton, mReplyAllButton);
Mindy Pereira326c6602012-01-04 15:32:42 -0800638 return;
639 }
640
Andy Huang28b7aee2012-08-20 20:27:32 -0700641 final boolean defaultReplyAll = getAccount().settings.replyBehavior
642 == UIProvider.DefaultReplyBehavior.REPLY_ALL;
643 setChildVisibility(defaultReplyAll ? GONE : VISIBLE, mReplyButton);
644 setChildVisibility(defaultReplyAll ? VISIBLE : GONE, mReplyAllButton);
Mindy Pereira326c6602012-01-04 15:32:42 -0800645 }
646
647 private static void setChildMarginRight(View childView, int marginRight) {
648 MarginLayoutParams mlp = (MarginLayoutParams) childView.getLayoutParams();
649 mlp.rightMargin = marginRight;
650 childView.setLayoutParams(mlp);
651 }
652
mindyp77a46052012-09-18 09:31:06 -0700653 private void renderEmailList(int rowRes, int valueRes, String[] emails, boolean showViaDomain,
654 View rootView) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800655 if (emails == null || emails.length == 0) {
656 return;
657 }
658 String[] formattedEmails = new String[emails.length];
659 for (int i = 0; i < emails.length; i++) {
Andy Huang65fe28f2012-04-06 18:08:53 -0700660 Address e = getAddress(emails[i]);
Mindy Pereira326c6602012-01-04 15:32:42 -0800661 String name = e.getName();
662 String addr = e.getAddress();
663 if (name == null || name.length() == 0) {
664 formattedEmails[i] = addr;
665 } else {
Andrew Sappersteind5b369b2012-07-13 12:38:46 -0700666 // The one downside to having the showViaDomain here is that
667 // if the sender does not have a name, it will not show the via info
668 if (showViaDomain) {
669 formattedEmails[i] = getResources().getString(
670 R.string.address_display_format_with_via_domain,
671 name, addr, mMessage.viaDomain);
672 } else {
673 formattedEmails[i] = getResources().getString(R.string.address_display_format,
674 name, addr);
675 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800676 }
677 }
mindyp77a46052012-09-18 09:31:06 -0700678 ((TextView) rootView.findViewById(valueRes)).setText(TextUtils.join("\n", formattedEmails));
679 rootView.findViewById(rowRes).setVisibility(VISIBLE);
Mindy Pereira326c6602012-01-04 15:32:42 -0800680 }
681
Mindy Pereira326c6602012-01-04 15:32:42 -0800682 /**
683 * Utility class to build a list of recipient lists.
684 */
685 private static class RecipientListsBuilder {
686 private final Context mContext;
687 private final String mMe;
688 private final SpannableStringBuilder mBuilder = new SpannableStringBuilder();
689 private final CharSequence mComma;
Andy Huang65fe28f2012-04-06 18:08:53 -0700690 private final Map<String, Address> mAddressCache;
Mindy Pereira326c6602012-01-04 15:32:42 -0800691
692 int mRecipientCount = 0;
693 boolean mFirst = true;
694
Andy Huang65fe28f2012-04-06 18:08:53 -0700695 public RecipientListsBuilder(Context context, String me,
696 Map<String, Address> addressCache) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800697 mContext = context;
698 mMe = me;
699 mComma = mContext.getText(R.string.enumeration_comma);
Andy Huang65fe28f2012-04-06 18:08:53 -0700700 mAddressCache = addressCache;
Mindy Pereira326c6602012-01-04 15:32:42 -0800701 }
702
703 public void append(String[] recipients, int headingRes) {
704 int addLimit = SUMMARY_MAX_RECIPIENTS - mRecipientCount;
705 CharSequence recipientList = getSummaryTextForHeading(headingRes, recipients, addLimit);
706 if (recipientList != null) {
707 // duplicate TextUtils.join() logic to minimize temporary
708 // allocations, and because we need to support spans
709 if (mFirst) {
710 mFirst = false;
711 } else {
712 mBuilder.append(RECIPIENT_HEADING_DELIMITER);
713 }
714 mBuilder.append(recipientList);
715 mRecipientCount += Math.min(addLimit, recipients.length);
716 }
717 }
718
719 private CharSequence getSummaryTextForHeading(int headingStrRes, String[] rawAddrs,
720 int maxToCopy) {
721 if (rawAddrs == null || rawAddrs.length == 0 || maxToCopy == 0) {
722 return null;
723 }
724
725 SpannableStringBuilder ssb = new SpannableStringBuilder(
726 mContext.getString(headingStrRes));
727 ssb.setSpan(new StyleSpan(Typeface.BOLD), 0, ssb.length(),
728 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
729 ssb.append(' ');
730
731 final int len = Math.min(maxToCopy, rawAddrs.length);
732 boolean first = true;
733 for (int i = 0; i < len; i++) {
Andy Huang65fe28f2012-04-06 18:08:53 -0700734 Address email = getAddress(mAddressCache, rawAddrs[i]);
Mindy Pereira326c6602012-01-04 15:32:42 -0800735 String name = (mMe.equals(email.getAddress())) ? mContext.getString(R.string.me)
736 : email.getSimplifiedName();
737
738 // duplicate TextUtils.join() logic to minimize temporary
739 // allocations, and because we need to support spans
740 if (first) {
741 first = false;
742 } else {
743 ssb.append(mComma);
744 }
745 ssb.append(name);
746 }
747
748 return ssb;
749 }
750
751 public CharSequence build() {
752 return mBuilder;
753 }
754 }
755
756 @VisibleForTesting
757 static CharSequence getRecipientSummaryText(Context context, String me, String[] to,
Andy Huang65fe28f2012-04-06 18:08:53 -0700758 String[] cc, String[] bcc, Map<String, Address> addressCache) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800759
Andy Huang65fe28f2012-04-06 18:08:53 -0700760 RecipientListsBuilder builder = new RecipientListsBuilder(context, me, addressCache);
Mindy Pereira326c6602012-01-04 15:32:42 -0800761
762 builder.append(to, R.string.to_heading);
763 builder.append(cc, R.string.cc_heading);
764 builder.append(bcc, R.string.bcc_heading);
765
766 return builder.build();
767 }
768
Andy Huang31c38a82012-08-15 21:39:43 -0700769 private void updateContactInfo() {
Mindy Pereira326c6602012-01-04 15:32:42 -0800770
771 mPresenceView.setImageDrawable(null);
772 mPresenceView.setVisibility(GONE);
773 if (mContactInfoSource == null || mSender == null) {
774 mPhotoView.setImageToDefault();
775 mPhotoView.setContentDescription(getResources().getString(
776 R.string.contact_info_string_default));
777 return;
778 }
779
780 // Set the photo to either a found Bitmap or the default
781 // and ensure either the contact URI or email is set so the click
782 // handling works
783 String contentDesc = getResources().getString(R.string.contact_info_string,
784 !TextUtils.isEmpty(mSender.getName()) ? mSender.getName() : mSender.getAddress());
785 mPhotoView.setContentDescription(contentDesc);
786 boolean photoSet = false;
787 String email = mSender.getAddress();
788 ContactInfo info = mContactInfoSource.getContactInfo(email);
789 if (info != null) {
790 mPhotoView.assignContactUri(info.contactUri);
791 if (info.photo != null) {
792 mPhotoView.setImageBitmap(info.photo);
793 contentDesc = String.format(contentDesc, mSender.getName());
794 photoSet = true;
795 }
796 if (!mIsDraft && info.status != null) {
797 mPresenceView.setImageResource(ContactsContract.StatusUpdates
798 .getPresenceIconResourceId(info.status));
799 mPresenceView.setVisibility(VISIBLE);
800 }
801 } else {
802 mPhotoView.assignContactFromEmail(email, true /* lazyLookup */);
803 }
804
805 if (!photoSet) {
806 mPhotoView.setImageToDefault();
807 }
808 }
809
810
811 @Override
812 public boolean onMenuItemClick(MenuItem item) {
813 mPopup.dismiss();
814 return onClick(null, item.getItemId());
815 }
816
817 @Override
818 public void onClick(View v) {
819 onClick(v, v.getId());
820 }
821
822 /**
823 * Handles clicks on either views or menu items. View parameter can be null
824 * for menu item clicks.
825 */
826 public boolean onClick(View v, int id) {
Andy Huange9523532012-08-27 12:42:24 -0700827 if (mMessage == null) {
828 LogUtils.i(LOG_TAG, "ignoring message header tap on unbound view");
829 return false;
830 }
831
Mindy Pereira326c6602012-01-04 15:32:42 -0800832 boolean handled = true;
833
834 switch (id) {
835 case R.id.reply:
Andy Huang28b7aee2012-08-20 20:27:32 -0700836 ComposeActivity.reply(getContext(), getAccount(), mMessage);
Mindy Pereira326c6602012-01-04 15:32:42 -0800837 break;
838 case R.id.reply_all:
Andy Huang28b7aee2012-08-20 20:27:32 -0700839 ComposeActivity.replyAll(getContext(), getAccount(), mMessage);
Mindy Pereira326c6602012-01-04 15:32:42 -0800840 break;
841 case R.id.forward:
Andy Huang28b7aee2012-08-20 20:27:32 -0700842 ComposeActivity.forward(getContext(), getAccount(), mMessage);
Mindy Pereira326c6602012-01-04 15:32:42 -0800843 break;
844 case R.id.star: {
Andy Huangd8e249e2012-03-21 17:01:37 -0700845 final boolean newValue = !v.isSelected();
Mindy Pereira326c6602012-01-04 15:32:42 -0800846 v.setSelected(newValue);
Andy Huang839ada22012-07-20 15:48:40 -0700847 mMessage.star(newValue);
Mindy Pereira326c6602012-01-04 15:32:42 -0800848 break;
849 }
850 case R.id.edit_draft:
Andy Huang28b7aee2012-08-20 20:27:32 -0700851 ComposeActivity.editDraft(getContext(), getAccount(), mMessage);
Mindy Pereira326c6602012-01-04 15:32:42 -0800852 break;
853 case R.id.overflow: {
854 if (mPopup == null) {
855 mPopup = new PopupMenu(getContext(), v);
856 mPopup.getMenuInflater().inflate(R.menu.message_header_overflow_menu,
857 mPopup.getMenu());
858 mPopup.setOnMenuItemClickListener(this);
859 }
Andy Huang28b7aee2012-08-20 20:27:32 -0700860 final boolean defaultReplyAll = getAccount().settings.replyBehavior
861 == UIProvider.DefaultReplyBehavior.REPLY_ALL;
862 mPopup.getMenu().findItem(R.id.reply).setVisible(defaultReplyAll);
863 mPopup.getMenu().findItem(R.id.reply_all).setVisible(!defaultReplyAll);
Mindy Pereira326c6602012-01-04 15:32:42 -0800864
865 mPopup.show();
866 break;
867 }
868 case R.id.details_collapsed_content:
869 case R.id.details_expanded_content:
870 toggleMessageDetails(v);
871 break;
872 case R.id.upper_header:
873 toggleExpanded();
874 break;
875 case R.id.show_pictures:
876 handleShowImagePromptClick(v);
877 break;
878 default:
879 LogUtils.i(LOG_TAG, "unrecognized header tap: %d", id);
880 handled = false;
881 break;
882 }
883 return handled;
884 }
885
mindypf4fce122012-09-14 15:55:33 -0700886 public void setExpandable(boolean expandable) {
887 mExpandable = expandable;
888 }
889
Mindy Pereira326c6602012-01-04 15:32:42 -0800890 public void toggleExpanded() {
mindypf4fce122012-09-14 15:55:33 -0700891 if (!mExpandable) {
892 return;
893 }
Andy Huangc7543572012-04-03 15:34:29 -0700894 setExpanded(!isExpanded());
Mindy Pereira326c6602012-01-04 15:32:42 -0800895
mindypf69a05b2012-09-07 15:43:11 -0700896 // The snappy header will disappear; no reason to update text.
897 if (!mIsSnappy) {
898 mSenderNameView.setText(getHeaderTitle());
899 mSenderEmailView.setText(getHeaderSubtitle());
900 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800901
902 updateChildVisibility();
903
904 // Force-measure the new header height so we can set the spacer size and
mindypf69a05b2012-09-07 15:43:11 -0700905 // reveal the message div in one pass. Force-measuring makes it unnecessary to set
Mindy Pereira326c6602012-01-04 15:32:42 -0800906 // mSizeChanged.
Andy Huangc7543572012-04-03 15:34:29 -0700907 int h = measureHeight();
908 mMessageHeaderItem.setHeight(h);
Mindy Pereira326c6602012-01-04 15:32:42 -0800909 if (mCallbacks != null) {
Andy Huangc7543572012-04-03 15:34:29 -0700910 mCallbacks.setMessageExpanded(mMessageHeaderItem, h);
Mindy Pereira326c6602012-01-04 15:32:42 -0800911 }
912 }
913
914 private void toggleMessageDetails(View visibleDetailsView) {
mindyp1b3cc472012-09-27 11:32:59 -0700915 int heightBefore = measureHeight();
Andy Huangc7543572012-04-03 15:34:29 -0700916 final boolean detailsExpanded = (visibleDetailsView == mCollapsedDetailsView);
917 setMessageDetailsExpanded(detailsExpanded);
918 updateSpacerHeight();
mindyp1b3cc472012-09-27 11:32:59 -0700919 if (mCallbacks != null) {
920 mCallbacks.setMessageDetailsExpanded(mMessageHeaderItem, detailsExpanded, heightBefore);
921 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800922 }
923
924 private void setMessageDetailsExpanded(boolean expand) {
mindyp77a46052012-09-18 09:31:06 -0700925 if (mExpandMode == DEFAULT_MODE) {
926 if (expand) {
927 showExpandedDetails();
928 hideCollapsedDetails();
929 } else {
930 hideExpandedDetails();
931 showCollapsedDetails();
932 }
933 } else if (mExpandMode == POPUP_MODE) {
934 if (expand) {
935 showDetailsPopup();
936 } else {
937 hideDetailsPopup();
938 showCollapsedDetails();
939 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800940 }
Andy Huangc7543572012-04-03 15:34:29 -0700941 if (mMessageHeaderItem != null) {
942 mMessageHeaderItem.detailsExpanded = expand;
943 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800944 }
945
946 public void setMessageDetailsVisibility(int vis) {
947 if (vis == GONE) {
948 hideCollapsedDetails();
949 hideExpandedDetails();
Andrew Sapperstein3b08c512012-07-11 16:51:07 -0700950 hideSpamWarning();
Mindy Pereira326c6602012-01-04 15:32:42 -0800951 hideShowImagePrompt();
Andy Huanga71f1f32012-04-24 20:17:22 -0700952 hideInvite();
Mindy Pereira326c6602012-01-04 15:32:42 -0800953 } else {
Andy Huangc7543572012-04-03 15:34:29 -0700954 setMessageDetailsExpanded(mMessageHeaderItem.detailsExpanded);
Andrew Sapperstein3b08c512012-07-11 16:51:07 -0700955 if (mMessage.spamWarningString == null) {
956 hideSpamWarning();
957 } else {
958 showSpamWarning();
959 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800960 if (mShowImagePrompt) {
961 showImagePrompt();
Andy Huang3233bff2012-03-20 19:38:45 -0700962 } else {
963 hideShowImagePrompt();
Mindy Pereira326c6602012-01-04 15:32:42 -0800964 }
Andy Huanga71f1f32012-04-24 20:17:22 -0700965 if (mMessage.isFlaggedCalendarInvite()) {
966 showInvite();
967 } else {
968 hideInvite();
969 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800970 }
971 if (mBottomBorderView != null) {
972 mBottomBorderView.setVisibility(vis);
973 }
974 }
975
Mindy Pereira326c6602012-01-04 15:32:42 -0800976 public void hideMessageDetails() {
977 setMessageDetailsVisibility(GONE);
978 }
979
Mindy Pereira326c6602012-01-04 15:32:42 -0800980 private void hideCollapsedDetails() {
981 if (mCollapsedDetailsView != null) {
982 mCollapsedDetailsView.setVisibility(GONE);
983 }
984 }
985
986 private void hideExpandedDetails() {
987 if (mExpandedDetailsView != null) {
988 mExpandedDetailsView.setVisibility(GONE);
989 }
990 }
991
Andy Huanga71f1f32012-04-24 20:17:22 -0700992 private void hideInvite() {
993 if (mInviteView != null) {
994 mInviteView.setVisibility(GONE);
995 }
996 }
997
998 private void showInvite() {
999 if (mInviteView == null) {
1000 mInviteView = (MessageInviteView) mInflater.inflate(
1001 R.layout.conversation_message_invite, this, false);
1002 addView(mInviteView);
1003 }
1004 mInviteView.bind(mMessage);
1005 mInviteView.setVisibility(VISIBLE);
1006 }
1007
Mindy Pereira326c6602012-01-04 15:32:42 -08001008 private void hideShowImagePrompt() {
1009 if (mImagePromptView != null) {
1010 mImagePromptView.setVisibility(GONE);
1011 }
1012 }
1013
1014 private void showImagePrompt() {
1015 if (mImagePromptView == null) {
Andy Huang3233bff2012-03-20 19:38:45 -07001016 ViewGroup v = (ViewGroup) mInflater.inflate(R.layout.conversation_message_show_pics,
1017 this, false);
Mindy Pereira326c6602012-01-04 15:32:42 -08001018 addView(v);
1019 v.setOnClickListener(this);
1020 v.setTag(SHOW_IMAGE_PROMPT_ONCE);
1021
1022 mImagePromptView = v;
1023 }
1024 mImagePromptView.setVisibility(VISIBLE);
1025 }
1026
Andrew Sapperstein3b08c512012-07-11 16:51:07 -07001027 private void hideSpamWarning() {
1028 if (mSpamWarningView != null) {
1029 mSpamWarningView.setVisibility(GONE);
1030 }
1031 }
1032
1033 private void showSpamWarning() {
1034 if (mSpamWarningView == null) {
1035 mSpamWarningView = (SpamWarningView)
1036 mInflater.inflate(R.layout.conversation_message_spam_warning, this, false);
1037 addView(mSpamWarningView);
1038 }
1039
1040 mSpamWarningView.showSpamWarning(mMessage, mSender);
1041 }
1042
Mindy Pereira326c6602012-01-04 15:32:42 -08001043 private void handleShowImagePromptClick(View v) {
1044 Integer state = (Integer) v.getTag();
1045 if (state == null) {
1046 return;
1047 }
1048 switch (state) {
1049 case SHOW_IMAGE_PROMPT_ONCE:
1050 if (mCallbacks != null) {
Andy Huang3233bff2012-03-20 19:38:45 -07001051 mCallbacks.showExternalResources(mMessage);
Mindy Pereira326c6602012-01-04 15:32:42 -08001052 }
1053 ImageView descriptionViewIcon = (ImageView) v.findViewById(R.id.show_pictures_icon);
1054 descriptionViewIcon.setContentDescription(getResources().getString(
1055 R.string.always_show_images));
1056 TextView descriptionView = (TextView) v.findViewById(R.id.show_pictures_text);
1057 descriptionView.setText(R.string.always_show_images);
1058 v.setTag(SHOW_IMAGE_PROMPT_ALWAYS);
Andy Huangc7543572012-04-03 15:34:29 -07001059 // the new text's line count may differ, so update the spacer height
1060 updateSpacerHeight();
Mindy Pereira326c6602012-01-04 15:32:42 -08001061 break;
1062 case SHOW_IMAGE_PROMPT_ALWAYS:
Andy Huang3233bff2012-03-20 19:38:45 -07001063 mMessage.markAlwaysShowImages(getQueryHandler(), 0 /* token */, null /* cookie */);
1064
Mindy Pereira326c6602012-01-04 15:32:42 -08001065 mShowImagePrompt = false;
1066 v.setTag(null);
1067 v.setVisibility(GONE);
Andy Huangc7543572012-04-03 15:34:29 -07001068 updateSpacerHeight();
Mindy Pereira326c6602012-01-04 15:32:42 -08001069 Toast.makeText(getContext(), R.string.always_show_images_toast, Toast.LENGTH_SHORT)
1070 .show();
1071 break;
1072 }
1073 }
1074
Andy Huang3233bff2012-03-20 19:38:45 -07001075 private AsyncQueryHandler getQueryHandler() {
1076 if (mQueryHandler == null) {
1077 mQueryHandler = new AsyncQueryHandler(getContext().getContentResolver()) {};
1078 }
1079 return mQueryHandler;
1080 }
1081
Mindy Pereira326c6602012-01-04 15:32:42 -08001082 /**
1083 * Makes collapsed details visible. If necessary, will inflate details
1084 * layout and render using saved-off state (senders, timestamp, etc).
1085 */
1086 private void showCollapsedDetails() {
1087 if (mCollapsedDetailsView == null) {
Andy Huang9f369592012-04-05 20:10:12 -07001088 mCollapsedDetailsView = (ViewGroup) mInflater.inflate(
1089 R.layout.conversation_message_details_header, this, false);
1090 addView(mCollapsedDetailsView, indexOfChild(mUpperHeaderView) + 1);
Mindy Pereira326c6602012-01-04 15:32:42 -08001091 mCollapsedDetailsView.setOnClickListener(this);
Andy Huangf70fc402012-02-17 15:37:42 -08001092 }
1093 if (!mCollapsedDetailsValid) {
Andy Huang65fe28f2012-04-06 18:08:53 -07001094 if (mMessageHeaderItem.recipientSummaryText == null) {
1095 mMessageHeaderItem.recipientSummaryText = getRecipientSummaryText(getContext(),
Andy Huang28b7aee2012-08-20 20:27:32 -07001096 getAccount().name, mTo, mCc, mBcc, mAddressCache);
Andy Huang65fe28f2012-04-06 18:08:53 -07001097 }
1098 ((TextView) findViewById(R.id.recipients_summary))
1099 .setText(mMessageHeaderItem.recipientSummaryText);
Mindy Pereira326c6602012-01-04 15:32:42 -08001100
1101 ((TextView) findViewById(R.id.date_summary)).setText(mTimestampShort);
Andy Huangf70fc402012-02-17 15:37:42 -08001102
1103 mCollapsedDetailsValid = true;
Mindy Pereira326c6602012-01-04 15:32:42 -08001104 }
1105 mCollapsedDetailsView.setVisibility(VISIBLE);
1106 }
1107
1108 /**
1109 * Makes expanded details visible. If necessary, will inflate expanded
1110 * details layout and render using saved-off state (senders, timestamp,
1111 * etc).
1112 */
1113 private void showExpandedDetails() {
1114 // lazily create expanded details view
Paul Westbrook63f9b482012-09-20 13:06:51 -07001115 final boolean expandedViewCreated = ensureExpandedDetailsView();
1116 if (expandedViewCreated) {
1117 addView(mExpandedDetailsView, indexOfChild(mUpperHeaderView) + 1);
1118 }
mindyp77a46052012-09-18 09:31:06 -07001119 mExpandedDetailsView.setVisibility(VISIBLE);
1120 }
1121
Paul Westbrook63f9b482012-09-20 13:06:51 -07001122 private boolean ensureExpandedDetailsView() {
1123 boolean viewCreated = false;
Mindy Pereira326c6602012-01-04 15:32:42 -08001124 if (mExpandedDetailsView == null) {
mindyp77a46052012-09-18 09:31:06 -07001125 View v = mInflater.inflate(R.layout.conversation_message_details_header_expanded, null,
1126 false);
Mindy Pereira326c6602012-01-04 15:32:42 -08001127 v.setOnClickListener(this);
1128
Andy Huangf70fc402012-02-17 15:37:42 -08001129 mExpandedDetailsView = (ViewGroup) v;
Paul Westbrook63f9b482012-09-20 13:06:51 -07001130 viewCreated = true;
Andy Huangf70fc402012-02-17 15:37:42 -08001131 }
1132 if (!mExpandedDetailsValid) {
Andy Huang65fe28f2012-04-06 18:08:53 -07001133 if (mMessageHeaderItem.timestampLong == null) {
1134 mMessageHeaderItem.timestampLong = mDateBuilder.formatLongDateTime(mTimestampMs);
1135 }
mindyp77a46052012-09-18 09:31:06 -07001136 ((TextView) mExpandedDetailsView.findViewById(R.id.date_value))
1137 .setText(mMessageHeaderItem.timestampLong);
1138 renderEmailList(R.id.replyto_row, R.id.replyto_value, mReplyTo, false,
1139 mExpandedDetailsView);
Andrew Sappersteind5b369b2012-07-13 12:38:46 -07001140 if (mMessage.viaDomain != null) {
mindyp77a46052012-09-18 09:31:06 -07001141 renderEmailList(R.id.from_row, R.id.from_value, mFrom, true, mExpandedDetailsView);
Andrew Sappersteind5b369b2012-07-13 12:38:46 -07001142 }
mindyp77a46052012-09-18 09:31:06 -07001143 renderEmailList(R.id.to_row, R.id.to_value, mTo, false, mExpandedDetailsView);
1144 renderEmailList(R.id.cc_row, R.id.cc_value, mCc, false, mExpandedDetailsView);
1145 renderEmailList(R.id.bcc_row, R.id.bcc_value, mBcc, false, mExpandedDetailsView);
Mindy Pereira326c6602012-01-04 15:32:42 -08001146
Andy Huangf70fc402012-02-17 15:37:42 -08001147 mExpandedDetailsValid = true;
Mindy Pereira326c6602012-01-04 15:32:42 -08001148 }
Paul Westbrook63f9b482012-09-20 13:06:51 -07001149 return viewCreated;
mindyp77a46052012-09-18 09:31:06 -07001150 }
1151
1152 private void showDetailsPopup() {
1153 ensureExpandedDetailsView();
1154 if (mDetailsPopup == null) {
1155 AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
1156 mExpandedDetailsView.findViewById(R.id.details_expander)
1157 .setVisibility(View.GONE);
1158 builder.setView(mExpandedDetailsView)
1159 .setCancelable(true)
1160 .setTitle(getContext().getString(R.string.message_details_title));
1161 mDetailsPopup = builder.show();
1162 } else {
1163 mDetailsPopup.show();
1164 }
1165 }
1166
1167 private void hideDetailsPopup() {
1168 if (mDetailsPopup != null) {
1169 mDetailsPopup.hide();
1170 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001171 }
1172
Mindy Pereira326c6602012-01-04 15:32:42 -08001173 /**
1174 * Returns a short plaintext snippet generated from the given HTML message
1175 * body. Collapses whitespace, ignores '&lt;' and '&gt;' characters and
1176 * everything in between, and truncates the snippet to no more than 100
1177 * characters.
1178 *
1179 * @return Short plaintext snippet
1180 */
1181 @VisibleForTesting
1182 static String makeSnippet(final String messageBody) {
Andy Huange805ab42012-08-09 20:05:58 -07001183 if (TextUtils.isEmpty(messageBody)) {
1184 return null;
1185 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001186
Andy Huange805ab42012-08-09 20:05:58 -07001187 final StringBuilder snippet = new StringBuilder(MAX_SNIPPET_LENGTH);
1188
1189 final StringReader reader = new StringReader(messageBody);
Mindy Pereira326c6602012-01-04 15:32:42 -08001190 try {
1191 int c;
1192 while ((c = reader.read()) != -1 && snippet.length() < MAX_SNIPPET_LENGTH) {
1193 // Collapse whitespace.
1194 if (Character.isWhitespace(c)) {
1195 snippet.append(' ');
1196 do {
1197 c = reader.read();
1198 } while (Character.isWhitespace(c));
1199 if (c == -1) {
1200 break;
1201 }
1202 }
1203
1204 if (c == '<') {
1205 // Ignore everything up to and including the next '>'
1206 // character.
1207 while ((c = reader.read()) != -1) {
1208 if (c == '>') {
1209 break;
1210 }
1211 }
1212
1213 // If we reached the end of the message body, exit.
1214 if (c == -1) {
1215 break;
1216 }
1217 } else if (c == '&') {
1218 // Read HTML entity.
1219 StringBuilder sb = new StringBuilder();
1220
1221 while ((c = reader.read()) != -1) {
1222 if (c == ';') {
1223 break;
1224 }
1225 sb.append((char) c);
1226 }
1227
1228 String entity = sb.toString();
1229 if ("nbsp".equals(entity)) {
1230 snippet.append(' ');
1231 } else if ("lt".equals(entity)) {
1232 snippet.append('<');
1233 } else if ("gt".equals(entity)) {
1234 snippet.append('>');
1235 } else if ("amp".equals(entity)) {
1236 snippet.append('&');
1237 } else if ("quot".equals(entity)) {
1238 snippet.append('"');
1239 } else if ("apos".equals(entity) || "#39".equals(entity)) {
1240 snippet.append('\'');
1241 } else {
1242 // Unknown entity; just append the literal string.
1243 snippet.append('&').append(entity);
1244 if (c == ';') {
1245 snippet.append(';');
1246 }
1247 }
1248
1249 // If we reached the end of the message body, exit.
1250 if (c == -1) {
1251 break;
1252 }
1253 } else {
1254 // The current character is a non-whitespace character that
1255 // isn't inside some
1256 // HTML tag and is not part of an HTML entity.
1257 snippet.append((char) c);
1258 }
1259 }
1260 } catch (IOException e) {
1261 LogUtils.wtf(LOG_TAG, e, "Really? IOException while reading a freaking string?!? ");
1262 }
1263
1264 return snippet.toString();
1265 }
1266
1267 @Override
Mindy Pereira326c6602012-01-04 15:32:42 -08001268 protected void onLayout(boolean changed, int l, int t, int r, int b) {
1269 Timer perf = new Timer();
1270 perf.start(LAYOUT_TAG);
1271 super.onLayout(changed, l, t, r, b);
1272 perf.pause(LAYOUT_TAG);
1273 }
1274
1275 @Override
1276 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1277 Timer t = new Timer();
1278 if (Timer.ENABLE_TIMER && !mPreMeasuring) {
Andy Huang3233bff2012-03-20 19:38:45 -07001279 t.count("header measure id=" + mMessage.id);
Mindy Pereira326c6602012-01-04 15:32:42 -08001280 t.start(MEASURE_TAG);
1281 }
1282 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1283 if (!mPreMeasuring) {
1284 t.pause(MEASURE_TAG);
1285 }
1286 }
Andy Huangbb56a152012-02-24 18:26:47 -08001287
Mindy Pereira326c6602012-01-04 15:32:42 -08001288}