blob: c1bc99f1e1a152b3b03cb2cd5e708e3ad0207267 [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
Andy Huang3233bff2012-03-20 19:38:45 -070019import android.content.AsyncQueryHandler;
Mindy Pereira326c6602012-01-04 15:32:42 -080020import android.content.Context;
Andy Huangb8331b42012-07-16 19:08:53 -070021import android.database.DataSetObserver;
Mindy Pereira326c6602012-01-04 15:32:42 -080022import android.graphics.Canvas;
23import 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;
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;
Andy Huang65fe28f2012-04-06 18:08:53 -070063import java.util.Map;
Mindy Pereira326c6602012-01-04 15:32:42 -080064
Mindy Pereira326c6602012-01-04 15:32:42 -080065public class MessageHeaderView extends LinearLayout implements OnClickListener,
Andy Huang31c38a82012-08-15 21:39:43 -070066 OnMenuItemClickListener, ConversationContainer.DetachListener {
Mindy Pereira326c6602012-01-04 15:32:42 -080067
68 /**
69 * Cap very long recipient lists during summary construction for efficiency.
70 */
71 private static final int SUMMARY_MAX_RECIPIENTS = 50;
72
73 private static final int MAX_SNIPPET_LENGTH = 100;
74
75 private static final int SHOW_IMAGE_PROMPT_ONCE = 1;
76 private static final int SHOW_IMAGE_PROMPT_ALWAYS = 2;
77
78 private static final String HEADER_INFLATE_TAG = "message header inflate";
79 private static final String HEADER_ADDVIEW_TAG = "message header addView";
80 private static final String HEADER_RENDER_TAG = "message header render";
81 private static final String PREMEASURE_TAG = "message header pre-measure";
82 private static final String LAYOUT_TAG = "message header layout";
83 private static final String MEASURE_TAG = "message header measure";
84
85 private static final String RECIPIENT_HEADING_DELIMITER = " ";
86
Paul Westbrookb334c902012-06-25 11:42:46 -070087 private static final String LOG_TAG = LogTag.getLogTag();
Mindy Pereira326c6602012-01-04 15:32:42 -080088
89 private MessageHeaderViewCallbacks mCallbacks;
Mindy Pereira326c6602012-01-04 15:32:42 -080090
Andy Huang9f369592012-04-05 20:10:12 -070091 private ViewGroup mUpperHeaderView;
Mindy Pereira326c6602012-01-04 15:32:42 -080092 private TextView mSenderNameView;
93 private TextView mSenderEmailView;
94 private QuickContactBadge mPhotoView;
95 private ImageView mStarView;
96 private ViewGroup mTitleContainerView;
97 private ViewGroup mCollapsedDetailsView;
98 private ViewGroup mExpandedDetailsView;
Andrew Sapperstein3b08c512012-07-11 16:51:07 -070099 private SpamWarningView mSpamWarningView;
Mindy Pereira326c6602012-01-04 15:32:42 -0800100 private ViewGroup mImagePromptView;
Andy Huanga71f1f32012-04-24 20:17:22 -0700101 private MessageInviteView mInviteView;
Mindy Pereira326c6602012-01-04 15:32:42 -0800102 private View mBottomBorderView;
103 private ImageView mPresenceView;
Andy Huang65fe28f2012-04-06 18:08:53 -0700104 private View mPhotoSpacerView;
105 private View mForwardButton;
106 private View mOverflowButton;
107 private View mDraftIcon;
108 private View mEditDraftButton;
109 private TextView mUpperDateView;
110 private View mReplyButton;
111 private View mReplyAllButton;
112 private View mAttachmentIcon;
Mindy Pereira326c6602012-01-04 15:32:42 -0800113
114 // temporary fields to reference raw data between initial render and details
115 // expansion
Andrew Sappersteind5b369b2012-07-13 12:38:46 -0700116 private String[] mFrom;
Mindy Pereira326c6602012-01-04 15:32:42 -0800117 private String[] mTo;
118 private String[] mCc;
119 private String[] mBcc;
120 private String[] mReplyTo;
121 private long mTimestampMs;
122 private FormattedDateBuilder mDateBuilder;
123
124 private boolean mIsDraft = false;
125
126 private boolean mIsSending;
127
Mindy Pereira326c6602012-01-04 15:32:42 -0800128 /**
129 * The snappy header has special visibility rules (i.e. no details header,
130 * even though it has an expanded appearance)
131 */
132 private boolean mIsSnappy;
133
134 private String mSnippet;
135
136 private Address mSender;
137
138 private ContactInfoSource mContactInfoSource;
139
140 private boolean mPreMeasuring;
141
Andy Huang28b7aee2012-08-20 20:27:32 -0700142 private ConversationAccountController mAccountController;
Mindy Pereira326c6602012-01-04 15:32:42 -0800143
Andy Huang65fe28f2012-04-06 18:08:53 -0700144 private Map<String, Address> mAddressCache;
145
Mindy Pereira326c6602012-01-04 15:32:42 -0800146 private boolean mShowImagePrompt;
147
Mindy Pereira326c6602012-01-04 15:32:42 -0800148 private int mDrawTranslateY;
149
Mindy Pereira326c6602012-01-04 15:32:42 -0800150 private CharSequence mTimestampShort;
151
152 /**
153 * Take the initial visibility of the star view to mean its collapsed
154 * visibility. Star is always visible when expanded, but sometimes, like on
155 * phones, there isn't enough room to warrant showing star when collapsed.
156 */
157 private int mCollapsedStarVis;
158
159 /**
160 * Take the initial right margin of the header title container to mean its
161 * right margin when collapsed. There's currently no need for additional
162 * margin when expanded, but if that need ever arises, title_container can
163 * simply tack on some extra right padding.
164 */
165 private int mTitleContainerCollapsedMarginRight;
166
Mindy Pereira326c6602012-01-04 15:32:42 -0800167 private PopupMenu mPopup;
168
Andy Huangc7543572012-04-03 15:34:29 -0700169 private MessageHeaderItem mMessageHeaderItem;
Andy Huang839ada22012-07-20 15:48:40 -0700170 private ConversationMessage mMessage;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800171
Andy Huangf70fc402012-02-17 15:37:42 -0800172 private boolean mCollapsedDetailsValid;
173 private boolean mExpandedDetailsValid;
174
Andy Huang88fc42e2012-03-08 15:02:43 -0800175 private final LayoutInflater mInflater;
176
Andy Huang3233bff2012-03-20 19:38:45 -0700177 private AsyncQueryHandler mQueryHandler;
178
Andy Huang59e0b182012-08-14 14:32:23 -0700179 private boolean mObservingContactInfo;
180
Andy Huangb8331b42012-07-16 19:08:53 -0700181 private final DataSetObserver mContactInfoObserver = new DataSetObserver() {
182 @Override
183 public void onChanged() {
184 updateContactInfo();
185 }
186 };
187
Andy Huang3233bff2012-03-20 19:38:45 -0700188 public interface MessageHeaderViewCallbacks {
Andy Huangc7543572012-04-03 15:34:29 -0700189 void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeight);
Andy Huang3233bff2012-03-20 19:38:45 -0700190
Andy Huangc7543572012-04-03 15:34:29 -0700191 void setMessageExpanded(MessageHeaderItem item, int newSpacerHeight);
Andy Huang3233bff2012-03-20 19:38:45 -0700192
193 void showExternalResources(Message msg);
194 }
195
Mindy Pereira326c6602012-01-04 15:32:42 -0800196 public MessageHeaderView(Context context) {
197 this(context, null);
198 }
199
200 public MessageHeaderView(Context context, AttributeSet attrs) {
201 this(context, attrs, -1);
202 }
203
204 public MessageHeaderView(Context context, AttributeSet attrs, int defStyle) {
205 super(context, attrs, defStyle);
Andy Huang88fc42e2012-03-08 15:02:43 -0800206
207 mInflater = LayoutInflater.from(context);
Mindy Pereira326c6602012-01-04 15:32:42 -0800208 }
209
210 @Override
211 protected void onFinishInflate() {
212 super.onFinishInflate();
Andy Huang9f369592012-04-05 20:10:12 -0700213 mUpperHeaderView = (ViewGroup) findViewById(R.id.upper_header);
Mindy Pereira326c6602012-01-04 15:32:42 -0800214 mSenderNameView = (TextView) findViewById(R.id.sender_name);
215 mSenderEmailView = (TextView) findViewById(R.id.sender_email);
216 mPhotoView = (QuickContactBadge) findViewById(R.id.photo);
Andy Huang65fe28f2012-04-06 18:08:53 -0700217 mPhotoSpacerView = findViewById(R.id.photo_spacer);
218 mReplyButton = findViewById(R.id.reply);
219 mReplyAllButton = findViewById(R.id.reply_all);
220 mForwardButton = findViewById(R.id.forward);
Mindy Pereira326c6602012-01-04 15:32:42 -0800221 mStarView = (ImageView) findViewById(R.id.star);
222 mPresenceView = (ImageView) findViewById(R.id.presence);
223 mTitleContainerView = (ViewGroup) findViewById(R.id.title_container);
Andy Huang65fe28f2012-04-06 18:08:53 -0700224 mOverflowButton = findViewById(R.id.overflow);
225 mDraftIcon = findViewById(R.id.draft);
226 mEditDraftButton = findViewById(R.id.edit_draft);
227 mUpperDateView = (TextView) findViewById(R.id.upper_date);
228 mAttachmentIcon = findViewById(R.id.attachment);
Mindy Pereira326c6602012-01-04 15:32:42 -0800229
230 mCollapsedStarVis = mStarView.getVisibility();
231 mTitleContainerCollapsedMarginRight = ((MarginLayoutParams) mTitleContainerView
232 .getLayoutParams()).rightMargin;
233
Andy Huang9f369592012-04-05 20:10:12 -0700234 mBottomBorderView = findViewById(R.id.details_bottom_border);
235
Mindy Pereira326c6602012-01-04 15:32:42 -0800236 setExpanded(true);
237
238 registerMessageClickTargets(R.id.reply, R.id.reply_all, R.id.forward, R.id.star,
239 R.id.edit_draft, R.id.overflow, R.id.upper_header);
240 }
241
242 private void registerMessageClickTargets(int... ids) {
243 for (int id : ids) {
244 View v = findViewById(id);
245 if (v != null) {
246 v.setOnClickListener(this);
247 }
248 }
249 }
250
Mindy Pereira326c6602012-01-04 15:32:42 -0800251 /**
252 * Associate the header with a contact info source for later contact
253 * presence/photo lookup.
254 */
255 public void setContactInfoSource(ContactInfoSource contactInfoSource) {
256 mContactInfoSource = contactInfoSource;
257 }
258
259 public void setCallbacks(MessageHeaderViewCallbacks callbacks) {
260 mCallbacks = callbacks;
261 }
262
263 /**
264 * Find the header view corresponding to a message with given local ID.
265 *
266 * @param parent the view parent to search within
267 * @param localMessageId local message ID
268 * @return a header view or null
269 */
270 public static MessageHeaderView find(ViewGroup parent, long localMessageId) {
271 return (MessageHeaderView) parent.findViewWithTag(localMessageId);
272 }
273
Mindy Pereira326c6602012-01-04 15:32:42 -0800274 public boolean isExpanded() {
Andy Huangc7543572012-04-03 15:34:29 -0700275 // (let's just arbitrarily say that unbound views are expanded by default)
276 return mMessageHeaderItem == null || mMessageHeaderItem.isExpanded();
Mindy Pereira326c6602012-01-04 15:32:42 -0800277 }
278
Mindy Pereira326c6602012-01-04 15:32:42 -0800279 public void setSnappy(boolean snappy) {
280 mIsSnappy = snappy;
281 hideMessageDetails();
282 if (snappy) {
283 setBackgroundDrawable(null);
284 } else {
285 setBackgroundColor(android.R.color.white);
286 }
287 }
288
Andy Huangb8331b42012-07-16 19:08:53 -0700289 @Override
290 public void onDetachedFromParent() {
291 unbind();
292 }
293
Mindy Pereira326c6602012-01-04 15:32:42 -0800294 /**
295 * Headers that are unbound will not match any rendered header (matches()
296 * will return false). Unbinding is not guaranteed to *hide* the view's old
297 * data, though. To re-bind this header to message data, call render() or
298 * renderUpperHeaderFrom().
299 */
300 public void unbind() {
Andy Huangc7543572012-04-03 15:34:29 -0700301 mMessageHeaderItem = null;
Andy Huang3233bff2012-03-20 19:38:45 -0700302 mMessage = null;
Andy Huangb8331b42012-07-16 19:08:53 -0700303
Andy Huang59e0b182012-08-14 14:32:23 -0700304 if (mObservingContactInfo) {
305 mContactInfoSource.unregisterObserver(mContactInfoObserver);
306 mObservingContactInfo = false;
307 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800308 }
309
Andy Huang28b7aee2012-08-20 20:27:32 -0700310 public void initialize(FormattedDateBuilder dateBuilder,
311 ConversationAccountController accountController,
Andy Huang65fe28f2012-04-06 18:08:53 -0700312 Map<String, Address> addressCache) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800313 mDateBuilder = dateBuilder;
Andy Huang28b7aee2012-08-20 20:27:32 -0700314 mAccountController = accountController;
Andy Huang65fe28f2012-04-06 18:08:53 -0700315 mAddressCache = addressCache;
Mindy Pereira326c6602012-01-04 15:32:42 -0800316 }
317
Andy Huang28b7aee2012-08-20 20:27:32 -0700318 private Account getAccount() {
319 return mAccountController.getAccount();
320 }
321
322 public void bind(MessageHeaderItem headerItem, boolean measureOnly) {
Andy Huang59e0b182012-08-14 14:32:23 -0700323 if (mMessageHeaderItem != null && mMessageHeaderItem == headerItem) {
324 return;
325 }
326
Mindy Pereira326c6602012-01-04 15:32:42 -0800327 Timer t = new Timer();
328 t.start(HEADER_RENDER_TAG);
329
Andy Huangf70fc402012-02-17 15:37:42 -0800330 mCollapsedDetailsValid = false;
331 mExpandedDetailsValid = false;
332
Andy Huangc7543572012-04-03 15:34:29 -0700333 mMessageHeaderItem = headerItem;
334 mMessage = headerItem.message;
335 mShowImagePrompt = mMessage.shouldShowImagePrompt();
Andy Huangc7543572012-04-03 15:34:29 -0700336 setExpanded(headerItem.isExpanded());
Mindy Pereira326c6602012-01-04 15:32:42 -0800337
Andy Huangf70fc402012-02-17 15:37:42 -0800338 mTimestampMs = mMessage.dateReceivedMs;
Andy Huang65fe28f2012-04-06 18:08:53 -0700339 mTimestampShort = headerItem.timestampShort;
340 if (mTimestampShort == null) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800341 mTimestampShort = mDateBuilder.formatShortDate(mTimestampMs);
Andy Huang65fe28f2012-04-06 18:08:53 -0700342 headerItem.timestampShort = mTimestampShort;
Mindy Pereira326c6602012-01-04 15:32:42 -0800343 }
344
Andrew Sappersteind5b369b2012-07-13 12:38:46 -0700345 mFrom = mMessage.getFromAddresses();
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700346 mTo = mMessage.getToAddresses();
347 mCc = mMessage.getCcAddresses();
348 mBcc = mMessage.getBccAddresses();
349 mReplyTo = mMessage.getReplyToAddresses();
Mindy Pereira326c6602012-01-04 15:32:42 -0800350
351 /**
352 * Turns draft mode on or off. Draft mode hides message operations other
353 * than "edit", hides contact photo, hides presence, and changes the
354 * sender name to "Draft".
355 */
Andy Huangf70fc402012-02-17 15:37:42 -0800356 mIsDraft = mMessage.draftType != UIProvider.DraftType.NOT_A_DRAFT;
357 mIsSending = isInOutbox();
Mindy Pereira326c6602012-01-04 15:32:42 -0800358
Mindy Pereira326c6602012-01-04 15:32:42 -0800359 // If this was a sent message AND:
360 // 1. the account has a custom from, the cursor will populate the
361 // selected custom from as the fromAddress when a message is sent but
362 // not yet synced.
363 // 2. the account has no custom froms, fromAddress will be empty, and we
364 // can safely fall back and show the account name as sender since it's
365 // the only possible fromAddress.
Andy Huangf70fc402012-02-17 15:37:42 -0800366 String from = mMessage.from;
Mindy Pereira326c6602012-01-04 15:32:42 -0800367 if (TextUtils.isEmpty(from)) {
Andy Huang28b7aee2012-08-20 20:27:32 -0700368 from = getAccount().name;
Mindy Pereira326c6602012-01-04 15:32:42 -0800369 }
Andy Huang65fe28f2012-04-06 18:08:53 -0700370 mSender = getAddress(from);
Mindy Pereira326c6602012-01-04 15:32:42 -0800371
Andrew Sapperstein3b08c512012-07-11 16:51:07 -0700372 updateChildVisibility();
373
374 if (mIsDraft || isInOutbox()) {
375 mSnippet = makeSnippet(mMessage.snippet);
376 } else {
377 mSnippet = mMessage.snippet;
378 }
379
Mindy Pereira326c6602012-01-04 15:32:42 -0800380 mSenderNameView.setText(getHeaderTitle());
381 mSenderEmailView.setText(getHeaderSubtitle());
382
Andy Huang65fe28f2012-04-06 18:08:53 -0700383 if (mUpperDateView != null) {
384 mUpperDateView.setText(mTimestampShort);
Mindy Pereira326c6602012-01-04 15:32:42 -0800385 }
386
Andy Huangd8e249e2012-03-21 17:01:37 -0700387 mStarView.setSelected(mMessage.starred);
Mindy Pereira326c6602012-01-04 15:32:42 -0800388 mStarView.setContentDescription(getResources().getString(
389 mStarView.isSelected() ? R.string.remove_star : R.string.add_star));
390
Andy Huang59e0b182012-08-14 14:32:23 -0700391 if (measureOnly) {
392 // avoid leaving any state around that would interfere with future regular bind() calls
393 unbind();
394 } else {
Andy Huangb8331b42012-07-16 19:08:53 -0700395 updateContactInfo();
Andy Huang59e0b182012-08-14 14:32:23 -0700396 if (!mObservingContactInfo) {
397 mContactInfoSource.registerObserver(mContactInfoObserver);
398 mObservingContactInfo = true;
399 }
Andy Huangb8331b42012-07-16 19:08:53 -0700400 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800401
402 t.pause(HEADER_RENDER_TAG);
Mindy Pereira326c6602012-01-04 15:32:42 -0800403 }
404
Andy Huang65fe28f2012-04-06 18:08:53 -0700405 private Address getAddress(String emailStr) {
406 return getAddress(mAddressCache, emailStr);
407 }
408
409 private static Address getAddress(Map<String, Address> cache, String emailStr) {
410 Address addr = null;
411 if (cache != null) {
412 addr = cache.get(emailStr);
413 }
414 if (addr == null) {
415 addr = Address.getEmailAddress(emailStr);
416 if (cache != null) {
417 cache.put(emailStr, addr);
418 }
419 }
420 return addr;
421 }
422
Andy Huangf70fc402012-02-17 15:37:42 -0800423 private boolean isInOutbox() {
Mindy Pereira326c6602012-01-04 15:32:42 -0800424 // TODO: what should this read? Folder info?
425 return false;
426 }
427
Andy Huangc7543572012-04-03 15:34:29 -0700428 private void updateSpacerHeight() {
429 final int h = measureHeight();
430
431 mMessageHeaderItem.setHeight(h);
432 if (mCallbacks != null) {
433 mCallbacks.setMessageSpacerHeight(mMessageHeaderItem, h);
434 }
435 }
436
437 private int measureHeight() {
438 ViewGroup parent = (ViewGroup) getParent();
Mindy Pereira326c6602012-01-04 15:32:42 -0800439 if (parent == null) {
Andy Huangc7543572012-04-03 15:34:29 -0700440 LogUtils.e(LOG_TAG, new Error(), "Unable to measure height of detached header");
Mindy Pereira326c6602012-01-04 15:32:42 -0800441 return getHeight();
442 }
443 mPreMeasuring = true;
Andy Huangc7543572012-04-03 15:34:29 -0700444 final int h = Utils.measureViewHeight(this, parent);
Mindy Pereira326c6602012-01-04 15:32:42 -0800445 mPreMeasuring = false;
446 return h;
447 }
448
449 private CharSequence getHeaderTitle() {
450 CharSequence title;
451
452 if (mIsDraft) {
453 title = getResources().getQuantityText(R.plurals.draft, 1);
454 } else if (mIsSending) {
455 title = getResources().getString(R.string.sending);
456 } else {
457 title = getSenderName(mSender);
458 }
459
460 return title;
461 }
462
463 private CharSequence getHeaderSubtitle() {
464 CharSequence sub;
465 if (mIsSending) {
466 sub = null;
467 } else {
Andrew Sappersteind5b369b2012-07-13 12:38:46 -0700468 if (isExpanded()) {
469 if (mMessage.viaDomain != null) {
470 sub = getResources().getString(
471 R.string.via_domain, mMessage.viaDomain);
472 } else {
473 sub = getSenderAddress(mSender);
474 }
475 } else {
476 sub = mSnippet;
477 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800478 }
479 return sub;
480 }
481
482 /**
483 * Return the name, if known, or just the address.
484 */
485 private static CharSequence getSenderName(Address sender) {
Andy Huang65fe28f2012-04-06 18:08:53 -0700486 final String displayName = sender.getName();
487 return TextUtils.isEmpty(displayName) ? sender.getAddress() : displayName;
Mindy Pereira326c6602012-01-04 15:32:42 -0800488 }
489
490 /**
491 * Return the address, if a name is present, or null if not.
492 */
493 private static CharSequence getSenderAddress(Address sender) {
494 String displayName = sender == null ? "" : sender.getName();
495 return TextUtils.isEmpty(displayName) ? null : sender.getAddress();
496 }
497
Andy Huang65fe28f2012-04-06 18:08:53 -0700498 private void setChildVisibility(int visibility, View... children) {
499 for (View v : children) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800500 if (v != null) {
501 v.setVisibility(visibility);
502 }
503 }
504 }
505
506 private void setExpanded(final boolean expanded) {
507 // use View's 'activated' flag to store expanded state
508 // child view state lists can use this to toggle drawables
509 setActivated(expanded);
Andy Huangc7543572012-04-03 15:34:29 -0700510 if (mMessageHeaderItem != null) {
511 mMessageHeaderItem.setExpanded(expanded);
512 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800513 }
514
Andy Huang28b7aee2012-08-20 20:27:32 -0700515 // update internal Account- or Settings-related state
516 public void onAccountChanged() {
517 updateChildVisibility();
518 }
519
Mindy Pereira326c6602012-01-04 15:32:42 -0800520 /**
521 * Update the visibility of the many child views based on expanded/collapsed
522 * and draft/normal state.
523 */
524 private void updateChildVisibility() {
Mindy Pereira326c6602012-01-04 15:32:42 -0800525 // Too bad this can't be done with an XML state list...
526
Andy Huangc7543572012-04-03 15:34:29 -0700527 if (isExpanded()) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800528 int normalVis, draftVis;
529
530 setMessageDetailsVisibility((mIsSnappy) ? GONE : VISIBLE);
531
532 if (mIsDraft) {
533 normalVis = GONE;
534 draftVis = VISIBLE;
535 } else {
536 normalVis = VISIBLE;
537 draftVis = GONE;
538 }
539
540 setReplyOrReplyAllVisible();
Andy Huang65fe28f2012-04-06 18:08:53 -0700541 setChildVisibility(normalVis, mPhotoView, mPhotoSpacerView, mForwardButton,
542 mSenderEmailView, mOverflowButton);
543 setChildVisibility(draftVis, mDraftIcon, mEditDraftButton);
544 setChildVisibility(GONE, mAttachmentIcon, mUpperDateView);
545 setChildVisibility(VISIBLE, mStarView);
Mindy Pereira326c6602012-01-04 15:32:42 -0800546
547 setChildMarginRight(mTitleContainerView, 0);
548
549 } else {
550
551 setMessageDetailsVisibility(GONE);
Andy Huang65fe28f2012-04-06 18:08:53 -0700552 setChildVisibility(VISIBLE, mSenderEmailView, mUpperDateView);
Mindy Pereira326c6602012-01-04 15:32:42 -0800553
Andy Huang65fe28f2012-04-06 18:08:53 -0700554 setChildVisibility(GONE, mEditDraftButton, mReplyButton, mReplyAllButton,
555 mForwardButton);
556 setChildVisibility(GONE, mOverflowButton);
Mindy Pereira326c6602012-01-04 15:32:42 -0800557
Andy Huang88fc42e2012-03-08 15:02:43 -0800558 setChildVisibility(mMessage.hasAttachments ? VISIBLE : GONE,
Andy Huang65fe28f2012-04-06 18:08:53 -0700559 mAttachmentIcon);
Mindy Pereira326c6602012-01-04 15:32:42 -0800560
Andy Huang65fe28f2012-04-06 18:08:53 -0700561 setChildVisibility(mCollapsedStarVis, mStarView);
Mindy Pereira326c6602012-01-04 15:32:42 -0800562
563 setChildMarginRight(mTitleContainerView, mTitleContainerCollapsedMarginRight);
564
565 if (mIsDraft) {
566
Andy Huang65fe28f2012-04-06 18:08:53 -0700567 setChildVisibility(VISIBLE, mDraftIcon);
568 setChildVisibility(GONE, mPhotoView, mPhotoSpacerView);
Mindy Pereira326c6602012-01-04 15:32:42 -0800569
570 } else {
571
Andy Huang65fe28f2012-04-06 18:08:53 -0700572 setChildVisibility(GONE, mDraftIcon);
573 setChildVisibility(VISIBLE, mPhotoView, mPhotoSpacerView);
Mindy Pereira326c6602012-01-04 15:32:42 -0800574
575 }
576 }
577
578 }
579
580 /**
581 * If an overflow menu is present in this header's layout, set the
582 * visibility of "Reply" and "Reply All" actions based on a user preference.
583 * Only one of those actions will be visible when an overflow is present. If
584 * no overflow is present (e.g. big phone or tablet), it's assumed we have
585 * plenty of screen real estate and can show both.
586 */
587 private void setReplyOrReplyAllVisible() {
588 if (mIsDraft) {
Andy Huang65fe28f2012-04-06 18:08:53 -0700589 setChildVisibility(GONE, mReplyButton, mReplyAllButton);
Mindy Pereira326c6602012-01-04 15:32:42 -0800590 return;
Andy Huang65fe28f2012-04-06 18:08:53 -0700591 } else if (mOverflowButton == null) {
592 setChildVisibility(VISIBLE, mReplyButton, mReplyAllButton);
Mindy Pereira326c6602012-01-04 15:32:42 -0800593 return;
594 }
595
Andy Huang28b7aee2012-08-20 20:27:32 -0700596 final boolean defaultReplyAll = getAccount().settings.replyBehavior
597 == UIProvider.DefaultReplyBehavior.REPLY_ALL;
598 setChildVisibility(defaultReplyAll ? GONE : VISIBLE, mReplyButton);
599 setChildVisibility(defaultReplyAll ? VISIBLE : GONE, mReplyAllButton);
Mindy Pereira326c6602012-01-04 15:32:42 -0800600 }
601
602 private static void setChildMarginRight(View childView, int marginRight) {
603 MarginLayoutParams mlp = (MarginLayoutParams) childView.getLayoutParams();
604 mlp.rightMargin = marginRight;
605 childView.setLayoutParams(mlp);
606 }
607
Andrew Sappersteind5b369b2012-07-13 12:38:46 -0700608 private void renderEmailList(int rowRes, int valueRes, String[] emails, boolean showViaDomain) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800609 if (emails == null || emails.length == 0) {
610 return;
611 }
612 String[] formattedEmails = new String[emails.length];
613 for (int i = 0; i < emails.length; i++) {
Andy Huang65fe28f2012-04-06 18:08:53 -0700614 Address e = getAddress(emails[i]);
Mindy Pereira326c6602012-01-04 15:32:42 -0800615 String name = e.getName();
616 String addr = e.getAddress();
617 if (name == null || name.length() == 0) {
618 formattedEmails[i] = addr;
619 } else {
Andrew Sappersteind5b369b2012-07-13 12:38:46 -0700620 // The one downside to having the showViaDomain here is that
621 // if the sender does not have a name, it will not show the via info
622 if (showViaDomain) {
623 formattedEmails[i] = getResources().getString(
624 R.string.address_display_format_with_via_domain,
625 name, addr, mMessage.viaDomain);
626 } else {
627 formattedEmails[i] = getResources().getString(R.string.address_display_format,
628 name, addr);
629 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800630 }
631 }
632 ((TextView) findViewById(valueRes)).setText(TextUtils.join("\n", formattedEmails));
633 findViewById(rowRes).setVisibility(VISIBLE);
634 }
635
Mindy Pereira326c6602012-01-04 15:32:42 -0800636 public void setTranslateY(int offsetY) {
637 if (mDrawTranslateY != offsetY) {
638 mDrawTranslateY = offsetY;
639 invalidate();
640 }
641 }
642
643 /**
644 * Utility class to build a list of recipient lists.
645 */
646 private static class RecipientListsBuilder {
647 private final Context mContext;
648 private final String mMe;
649 private final SpannableStringBuilder mBuilder = new SpannableStringBuilder();
650 private final CharSequence mComma;
Andy Huang65fe28f2012-04-06 18:08:53 -0700651 private final Map<String, Address> mAddressCache;
Mindy Pereira326c6602012-01-04 15:32:42 -0800652
653 int mRecipientCount = 0;
654 boolean mFirst = true;
655
Andy Huang65fe28f2012-04-06 18:08:53 -0700656 public RecipientListsBuilder(Context context, String me,
657 Map<String, Address> addressCache) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800658 mContext = context;
659 mMe = me;
660 mComma = mContext.getText(R.string.enumeration_comma);
Andy Huang65fe28f2012-04-06 18:08:53 -0700661 mAddressCache = addressCache;
Mindy Pereira326c6602012-01-04 15:32:42 -0800662 }
663
664 public void append(String[] recipients, int headingRes) {
665 int addLimit = SUMMARY_MAX_RECIPIENTS - mRecipientCount;
666 CharSequence recipientList = getSummaryTextForHeading(headingRes, recipients, addLimit);
667 if (recipientList != null) {
668 // duplicate TextUtils.join() logic to minimize temporary
669 // allocations, and because we need to support spans
670 if (mFirst) {
671 mFirst = false;
672 } else {
673 mBuilder.append(RECIPIENT_HEADING_DELIMITER);
674 }
675 mBuilder.append(recipientList);
676 mRecipientCount += Math.min(addLimit, recipients.length);
677 }
678 }
679
680 private CharSequence getSummaryTextForHeading(int headingStrRes, String[] rawAddrs,
681 int maxToCopy) {
682 if (rawAddrs == null || rawAddrs.length == 0 || maxToCopy == 0) {
683 return null;
684 }
685
686 SpannableStringBuilder ssb = new SpannableStringBuilder(
687 mContext.getString(headingStrRes));
688 ssb.setSpan(new StyleSpan(Typeface.BOLD), 0, ssb.length(),
689 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
690 ssb.append(' ');
691
692 final int len = Math.min(maxToCopy, rawAddrs.length);
693 boolean first = true;
694 for (int i = 0; i < len; i++) {
Andy Huang65fe28f2012-04-06 18:08:53 -0700695 Address email = getAddress(mAddressCache, rawAddrs[i]);
Mindy Pereira326c6602012-01-04 15:32:42 -0800696 String name = (mMe.equals(email.getAddress())) ? mContext.getString(R.string.me)
697 : email.getSimplifiedName();
698
699 // duplicate TextUtils.join() logic to minimize temporary
700 // allocations, and because we need to support spans
701 if (first) {
702 first = false;
703 } else {
704 ssb.append(mComma);
705 }
706 ssb.append(name);
707 }
708
709 return ssb;
710 }
711
712 public CharSequence build() {
713 return mBuilder;
714 }
715 }
716
717 @VisibleForTesting
718 static CharSequence getRecipientSummaryText(Context context, String me, String[] to,
Andy Huang65fe28f2012-04-06 18:08:53 -0700719 String[] cc, String[] bcc, Map<String, Address> addressCache) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800720
Andy Huang65fe28f2012-04-06 18:08:53 -0700721 RecipientListsBuilder builder = new RecipientListsBuilder(context, me, addressCache);
Mindy Pereira326c6602012-01-04 15:32:42 -0800722
723 builder.append(to, R.string.to_heading);
724 builder.append(cc, R.string.cc_heading);
725 builder.append(bcc, R.string.bcc_heading);
726
727 return builder.build();
728 }
729
Andy Huang31c38a82012-08-15 21:39:43 -0700730 private void updateContactInfo() {
Mindy Pereira326c6602012-01-04 15:32:42 -0800731
732 mPresenceView.setImageDrawable(null);
733 mPresenceView.setVisibility(GONE);
734 if (mContactInfoSource == null || mSender == null) {
735 mPhotoView.setImageToDefault();
736 mPhotoView.setContentDescription(getResources().getString(
737 R.string.contact_info_string_default));
738 return;
739 }
740
741 // Set the photo to either a found Bitmap or the default
742 // and ensure either the contact URI or email is set so the click
743 // handling works
744 String contentDesc = getResources().getString(R.string.contact_info_string,
745 !TextUtils.isEmpty(mSender.getName()) ? mSender.getName() : mSender.getAddress());
746 mPhotoView.setContentDescription(contentDesc);
747 boolean photoSet = false;
748 String email = mSender.getAddress();
749 ContactInfo info = mContactInfoSource.getContactInfo(email);
750 if (info != null) {
751 mPhotoView.assignContactUri(info.contactUri);
752 if (info.photo != null) {
753 mPhotoView.setImageBitmap(info.photo);
754 contentDesc = String.format(contentDesc, mSender.getName());
755 photoSet = true;
756 }
757 if (!mIsDraft && info.status != null) {
758 mPresenceView.setImageResource(ContactsContract.StatusUpdates
759 .getPresenceIconResourceId(info.status));
760 mPresenceView.setVisibility(VISIBLE);
761 }
762 } else {
763 mPhotoView.assignContactFromEmail(email, true /* lazyLookup */);
764 }
765
766 if (!photoSet) {
767 mPhotoView.setImageToDefault();
768 }
769 }
770
771
772 @Override
773 public boolean onMenuItemClick(MenuItem item) {
774 mPopup.dismiss();
775 return onClick(null, item.getItemId());
776 }
777
778 @Override
779 public void onClick(View v) {
780 onClick(v, v.getId());
781 }
782
783 /**
784 * Handles clicks on either views or menu items. View parameter can be null
785 * for menu item clicks.
786 */
787 public boolean onClick(View v, int id) {
788 boolean handled = true;
789
790 switch (id) {
791 case R.id.reply:
Andy Huang28b7aee2012-08-20 20:27:32 -0700792 ComposeActivity.reply(getContext(), getAccount(), mMessage);
Mindy Pereira326c6602012-01-04 15:32:42 -0800793 break;
794 case R.id.reply_all:
Andy Huang28b7aee2012-08-20 20:27:32 -0700795 ComposeActivity.replyAll(getContext(), getAccount(), mMessage);
Mindy Pereira326c6602012-01-04 15:32:42 -0800796 break;
797 case R.id.forward:
Andy Huang28b7aee2012-08-20 20:27:32 -0700798 ComposeActivity.forward(getContext(), getAccount(), mMessage);
Mindy Pereira326c6602012-01-04 15:32:42 -0800799 break;
800 case R.id.star: {
Andy Huangd8e249e2012-03-21 17:01:37 -0700801 final boolean newValue = !v.isSelected();
Mindy Pereira326c6602012-01-04 15:32:42 -0800802 v.setSelected(newValue);
Andy Huang839ada22012-07-20 15:48:40 -0700803 mMessage.star(newValue);
Mindy Pereira326c6602012-01-04 15:32:42 -0800804 break;
805 }
806 case R.id.edit_draft:
Andy Huang28b7aee2012-08-20 20:27:32 -0700807 ComposeActivity.editDraft(getContext(), getAccount(), mMessage);
Mindy Pereira326c6602012-01-04 15:32:42 -0800808 break;
809 case R.id.overflow: {
810 if (mPopup == null) {
811 mPopup = new PopupMenu(getContext(), v);
812 mPopup.getMenuInflater().inflate(R.menu.message_header_overflow_menu,
813 mPopup.getMenu());
814 mPopup.setOnMenuItemClickListener(this);
815 }
Andy Huang28b7aee2012-08-20 20:27:32 -0700816 final boolean defaultReplyAll = getAccount().settings.replyBehavior
817 == UIProvider.DefaultReplyBehavior.REPLY_ALL;
818 mPopup.getMenu().findItem(R.id.reply).setVisible(defaultReplyAll);
819 mPopup.getMenu().findItem(R.id.reply_all).setVisible(!defaultReplyAll);
Mindy Pereira326c6602012-01-04 15:32:42 -0800820
821 mPopup.show();
822 break;
823 }
824 case R.id.details_collapsed_content:
825 case R.id.details_expanded_content:
826 toggleMessageDetails(v);
827 break;
828 case R.id.upper_header:
829 toggleExpanded();
830 break;
831 case R.id.show_pictures:
832 handleShowImagePromptClick(v);
833 break;
834 default:
835 LogUtils.i(LOG_TAG, "unrecognized header tap: %d", id);
836 handled = false;
837 break;
838 }
839 return handled;
840 }
841
842 public void toggleExpanded() {
843 if (mIsSnappy) {
844 // In addition to making the snappy header disappear, this will
845 // propagate the change to the normal header. It should only be
846 // possible to collapse an expanded snappy header; collapsed snappy
847 // headers should never exist.
848
849 // TODO: make this work right. the scroll position jumps and the
850 // snappy header doesn't re-appear bound to a subsequent message.
851 // mCallbacks.setMessageExpanded(mLocalMessageId, mServerMessageId,
852 // false);
853 // setVisibility(GONE);
854 // unbind();
855 return;
856 }
857
Andy Huangc7543572012-04-03 15:34:29 -0700858 setExpanded(!isExpanded());
Mindy Pereira326c6602012-01-04 15:32:42 -0800859
860 mSenderNameView.setText(getHeaderTitle());
861 mSenderEmailView.setText(getHeaderSubtitle());
862
863 updateChildVisibility();
864
865 // Force-measure the new header height so we can set the spacer size and
866 // reveal the message
867 // div in one pass. Force-measuring makes it unnecessary to set
868 // mSizeChanged.
Andy Huangc7543572012-04-03 15:34:29 -0700869 int h = measureHeight();
870 mMessageHeaderItem.setHeight(h);
Mindy Pereira326c6602012-01-04 15:32:42 -0800871 if (mCallbacks != null) {
Andy Huangc7543572012-04-03 15:34:29 -0700872 mCallbacks.setMessageExpanded(mMessageHeaderItem, h);
Mindy Pereira326c6602012-01-04 15:32:42 -0800873 }
874 }
875
876 private void toggleMessageDetails(View visibleDetailsView) {
Andy Huangc7543572012-04-03 15:34:29 -0700877 final boolean detailsExpanded = (visibleDetailsView == mCollapsedDetailsView);
878 setMessageDetailsExpanded(detailsExpanded);
879 updateSpacerHeight();
Mindy Pereira326c6602012-01-04 15:32:42 -0800880 }
881
882 private void setMessageDetailsExpanded(boolean expand) {
883 if (expand) {
884 showExpandedDetails();
885 hideCollapsedDetails();
886 } else {
887 hideExpandedDetails();
888 showCollapsedDetails();
889 }
Andy Huangc7543572012-04-03 15:34:29 -0700890 if (mMessageHeaderItem != null) {
891 mMessageHeaderItem.detailsExpanded = expand;
892 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800893 }
894
895 public void setMessageDetailsVisibility(int vis) {
896 if (vis == GONE) {
897 hideCollapsedDetails();
898 hideExpandedDetails();
Andrew Sapperstein3b08c512012-07-11 16:51:07 -0700899 hideSpamWarning();
Mindy Pereira326c6602012-01-04 15:32:42 -0800900 hideShowImagePrompt();
Andy Huanga71f1f32012-04-24 20:17:22 -0700901 hideInvite();
Mindy Pereira326c6602012-01-04 15:32:42 -0800902 } else {
Andy Huangc7543572012-04-03 15:34:29 -0700903 setMessageDetailsExpanded(mMessageHeaderItem.detailsExpanded);
Andrew Sapperstein3b08c512012-07-11 16:51:07 -0700904 if (mMessage.spamWarningString == null) {
905 hideSpamWarning();
906 } else {
907 showSpamWarning();
908 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800909 if (mShowImagePrompt) {
910 showImagePrompt();
Andy Huang3233bff2012-03-20 19:38:45 -0700911 } else {
912 hideShowImagePrompt();
Mindy Pereira326c6602012-01-04 15:32:42 -0800913 }
Andy Huanga71f1f32012-04-24 20:17:22 -0700914 if (mMessage.isFlaggedCalendarInvite()) {
915 showInvite();
916 } else {
917 hideInvite();
918 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800919 }
920 if (mBottomBorderView != null) {
921 mBottomBorderView.setVisibility(vis);
922 }
923 }
924
Mindy Pereira326c6602012-01-04 15:32:42 -0800925 public void hideMessageDetails() {
926 setMessageDetailsVisibility(GONE);
927 }
928
Mindy Pereira326c6602012-01-04 15:32:42 -0800929 private void hideCollapsedDetails() {
930 if (mCollapsedDetailsView != null) {
931 mCollapsedDetailsView.setVisibility(GONE);
932 }
933 }
934
935 private void hideExpandedDetails() {
936 if (mExpandedDetailsView != null) {
937 mExpandedDetailsView.setVisibility(GONE);
938 }
939 }
940
Andy Huanga71f1f32012-04-24 20:17:22 -0700941 private void hideInvite() {
942 if (mInviteView != null) {
943 mInviteView.setVisibility(GONE);
944 }
945 }
946
947 private void showInvite() {
948 if (mInviteView == null) {
949 mInviteView = (MessageInviteView) mInflater.inflate(
950 R.layout.conversation_message_invite, this, false);
951 addView(mInviteView);
952 }
953 mInviteView.bind(mMessage);
954 mInviteView.setVisibility(VISIBLE);
955 }
956
Mindy Pereira326c6602012-01-04 15:32:42 -0800957 private void hideShowImagePrompt() {
958 if (mImagePromptView != null) {
959 mImagePromptView.setVisibility(GONE);
960 }
961 }
962
963 private void showImagePrompt() {
964 if (mImagePromptView == null) {
Andy Huang3233bff2012-03-20 19:38:45 -0700965 ViewGroup v = (ViewGroup) mInflater.inflate(R.layout.conversation_message_show_pics,
966 this, false);
Mindy Pereira326c6602012-01-04 15:32:42 -0800967 addView(v);
968 v.setOnClickListener(this);
969 v.setTag(SHOW_IMAGE_PROMPT_ONCE);
970
971 mImagePromptView = v;
972 }
973 mImagePromptView.setVisibility(VISIBLE);
974 }
975
Andrew Sapperstein3b08c512012-07-11 16:51:07 -0700976 private void hideSpamWarning() {
977 if (mSpamWarningView != null) {
978 mSpamWarningView.setVisibility(GONE);
979 }
980 }
981
982 private void showSpamWarning() {
983 if (mSpamWarningView == null) {
984 mSpamWarningView = (SpamWarningView)
985 mInflater.inflate(R.layout.conversation_message_spam_warning, this, false);
986 addView(mSpamWarningView);
987 }
988
989 mSpamWarningView.showSpamWarning(mMessage, mSender);
990 }
991
Mindy Pereira326c6602012-01-04 15:32:42 -0800992 private void handleShowImagePromptClick(View v) {
993 Integer state = (Integer) v.getTag();
994 if (state == null) {
995 return;
996 }
997 switch (state) {
998 case SHOW_IMAGE_PROMPT_ONCE:
999 if (mCallbacks != null) {
Andy Huang3233bff2012-03-20 19:38:45 -07001000 mCallbacks.showExternalResources(mMessage);
Mindy Pereira326c6602012-01-04 15:32:42 -08001001 }
1002 ImageView descriptionViewIcon = (ImageView) v.findViewById(R.id.show_pictures_icon);
1003 descriptionViewIcon.setContentDescription(getResources().getString(
1004 R.string.always_show_images));
1005 TextView descriptionView = (TextView) v.findViewById(R.id.show_pictures_text);
1006 descriptionView.setText(R.string.always_show_images);
1007 v.setTag(SHOW_IMAGE_PROMPT_ALWAYS);
Andy Huangc7543572012-04-03 15:34:29 -07001008 // the new text's line count may differ, so update the spacer height
1009 updateSpacerHeight();
Mindy Pereira326c6602012-01-04 15:32:42 -08001010 break;
1011 case SHOW_IMAGE_PROMPT_ALWAYS:
Andy Huang3233bff2012-03-20 19:38:45 -07001012 mMessage.markAlwaysShowImages(getQueryHandler(), 0 /* token */, null /* cookie */);
1013
Mindy Pereira326c6602012-01-04 15:32:42 -08001014 mShowImagePrompt = false;
1015 v.setTag(null);
1016 v.setVisibility(GONE);
Andy Huangc7543572012-04-03 15:34:29 -07001017 updateSpacerHeight();
Mindy Pereira326c6602012-01-04 15:32:42 -08001018 Toast.makeText(getContext(), R.string.always_show_images_toast, Toast.LENGTH_SHORT)
1019 .show();
1020 break;
1021 }
1022 }
1023
Andy Huang3233bff2012-03-20 19:38:45 -07001024 private AsyncQueryHandler getQueryHandler() {
1025 if (mQueryHandler == null) {
1026 mQueryHandler = new AsyncQueryHandler(getContext().getContentResolver()) {};
1027 }
1028 return mQueryHandler;
1029 }
1030
Mindy Pereira326c6602012-01-04 15:32:42 -08001031 /**
1032 * Makes collapsed details visible. If necessary, will inflate details
1033 * layout and render using saved-off state (senders, timestamp, etc).
1034 */
1035 private void showCollapsedDetails() {
1036 if (mCollapsedDetailsView == null) {
Andy Huang9f369592012-04-05 20:10:12 -07001037 mCollapsedDetailsView = (ViewGroup) mInflater.inflate(
1038 R.layout.conversation_message_details_header, this, false);
1039 addView(mCollapsedDetailsView, indexOfChild(mUpperHeaderView) + 1);
Mindy Pereira326c6602012-01-04 15:32:42 -08001040 mCollapsedDetailsView.setOnClickListener(this);
Andy Huangf70fc402012-02-17 15:37:42 -08001041 }
1042 if (!mCollapsedDetailsValid) {
Andy Huang65fe28f2012-04-06 18:08:53 -07001043 if (mMessageHeaderItem.recipientSummaryText == null) {
1044 mMessageHeaderItem.recipientSummaryText = getRecipientSummaryText(getContext(),
Andy Huang28b7aee2012-08-20 20:27:32 -07001045 getAccount().name, mTo, mCc, mBcc, mAddressCache);
Andy Huang65fe28f2012-04-06 18:08:53 -07001046 }
1047 ((TextView) findViewById(R.id.recipients_summary))
1048 .setText(mMessageHeaderItem.recipientSummaryText);
Mindy Pereira326c6602012-01-04 15:32:42 -08001049
1050 ((TextView) findViewById(R.id.date_summary)).setText(mTimestampShort);
Andy Huangf70fc402012-02-17 15:37:42 -08001051
1052 mCollapsedDetailsValid = true;
Mindy Pereira326c6602012-01-04 15:32:42 -08001053 }
1054 mCollapsedDetailsView.setVisibility(VISIBLE);
1055 }
1056
1057 /**
1058 * Makes expanded details visible. If necessary, will inflate expanded
1059 * details layout and render using saved-off state (senders, timestamp,
1060 * etc).
1061 */
1062 private void showExpandedDetails() {
1063 // lazily create expanded details view
1064 if (mExpandedDetailsView == null) {
Andy Huang3233bff2012-03-20 19:38:45 -07001065 View v = mInflater.inflate(R.layout.conversation_message_details_header_expanded,
1066 this, false);
Andy Huang9f369592012-04-05 20:10:12 -07001067 addView(v, indexOfChild(mUpperHeaderView) + 1);
Mindy Pereira326c6602012-01-04 15:32:42 -08001068 v.setOnClickListener(this);
1069
Andy Huangf70fc402012-02-17 15:37:42 -08001070 mExpandedDetailsView = (ViewGroup) v;
1071 }
1072 if (!mExpandedDetailsValid) {
Andy Huang65fe28f2012-04-06 18:08:53 -07001073 if (mMessageHeaderItem.timestampLong == null) {
1074 mMessageHeaderItem.timestampLong = mDateBuilder.formatLongDateTime(mTimestampMs);
1075 }
1076 ((TextView) findViewById(R.id.date_value)).setText(mMessageHeaderItem.timestampLong);
Andrew Sappersteind5b369b2012-07-13 12:38:46 -07001077 renderEmailList(R.id.replyto_row, R.id.replyto_value, mReplyTo, false);
1078 if (mMessage.viaDomain != null) {
1079 renderEmailList(R.id.from_row, R.id.from_value, mFrom, true);
1080 }
1081 renderEmailList(R.id.to_row, R.id.to_value, mTo, false);
1082 renderEmailList(R.id.cc_row, R.id.cc_value, mCc, false);
1083 renderEmailList(R.id.bcc_row, R.id.bcc_value, mBcc, false);
Mindy Pereira326c6602012-01-04 15:32:42 -08001084
Andy Huangf70fc402012-02-17 15:37:42 -08001085 mExpandedDetailsValid = true;
Mindy Pereira326c6602012-01-04 15:32:42 -08001086 }
1087 mExpandedDetailsView.setVisibility(VISIBLE);
1088 }
1089
Mindy Pereira326c6602012-01-04 15:32:42 -08001090 /**
1091 * Returns a short plaintext snippet generated from the given HTML message
1092 * body. Collapses whitespace, ignores '&lt;' and '&gt;' characters and
1093 * everything in between, and truncates the snippet to no more than 100
1094 * characters.
1095 *
1096 * @return Short plaintext snippet
1097 */
1098 @VisibleForTesting
1099 static String makeSnippet(final String messageBody) {
Andy Huange805ab42012-08-09 20:05:58 -07001100 if (TextUtils.isEmpty(messageBody)) {
1101 return null;
1102 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001103
Andy Huange805ab42012-08-09 20:05:58 -07001104 final StringBuilder snippet = new StringBuilder(MAX_SNIPPET_LENGTH);
1105
1106 final StringReader reader = new StringReader(messageBody);
Mindy Pereira326c6602012-01-04 15:32:42 -08001107 try {
1108 int c;
1109 while ((c = reader.read()) != -1 && snippet.length() < MAX_SNIPPET_LENGTH) {
1110 // Collapse whitespace.
1111 if (Character.isWhitespace(c)) {
1112 snippet.append(' ');
1113 do {
1114 c = reader.read();
1115 } while (Character.isWhitespace(c));
1116 if (c == -1) {
1117 break;
1118 }
1119 }
1120
1121 if (c == '<') {
1122 // Ignore everything up to and including the next '>'
1123 // character.
1124 while ((c = reader.read()) != -1) {
1125 if (c == '>') {
1126 break;
1127 }
1128 }
1129
1130 // If we reached the end of the message body, exit.
1131 if (c == -1) {
1132 break;
1133 }
1134 } else if (c == '&') {
1135 // Read HTML entity.
1136 StringBuilder sb = new StringBuilder();
1137
1138 while ((c = reader.read()) != -1) {
1139 if (c == ';') {
1140 break;
1141 }
1142 sb.append((char) c);
1143 }
1144
1145 String entity = sb.toString();
1146 if ("nbsp".equals(entity)) {
1147 snippet.append(' ');
1148 } else if ("lt".equals(entity)) {
1149 snippet.append('<');
1150 } else if ("gt".equals(entity)) {
1151 snippet.append('>');
1152 } else if ("amp".equals(entity)) {
1153 snippet.append('&');
1154 } else if ("quot".equals(entity)) {
1155 snippet.append('"');
1156 } else if ("apos".equals(entity) || "#39".equals(entity)) {
1157 snippet.append('\'');
1158 } else {
1159 // Unknown entity; just append the literal string.
1160 snippet.append('&').append(entity);
1161 if (c == ';') {
1162 snippet.append(';');
1163 }
1164 }
1165
1166 // If we reached the end of the message body, exit.
1167 if (c == -1) {
1168 break;
1169 }
1170 } else {
1171 // The current character is a non-whitespace character that
1172 // isn't inside some
1173 // HTML tag and is not part of an HTML entity.
1174 snippet.append((char) c);
1175 }
1176 }
1177 } catch (IOException e) {
1178 LogUtils.wtf(LOG_TAG, e, "Really? IOException while reading a freaking string?!? ");
1179 }
1180
1181 return snippet.toString();
1182 }
1183
1184 @Override
1185 public void dispatchDraw(Canvas canvas) {
1186 boolean transform = mIsSnappy && (mDrawTranslateY != 0);
1187 int saved = -1;
1188 if (transform) {
1189 saved = canvas.save();
1190 canvas.translate(0, mDrawTranslateY);
1191 }
1192 super.dispatchDraw(canvas);
1193 if (transform) {
1194 canvas.restoreToCount(saved);
1195 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001196 }
1197
1198 @Override
1199 protected void onLayout(boolean changed, int l, int t, int r, int b) {
1200 Timer perf = new Timer();
1201 perf.start(LAYOUT_TAG);
1202 super.onLayout(changed, l, t, r, b);
1203 perf.pause(LAYOUT_TAG);
1204 }
1205
1206 @Override
1207 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1208 Timer t = new Timer();
1209 if (Timer.ENABLE_TIMER && !mPreMeasuring) {
Andy Huang3233bff2012-03-20 19:38:45 -07001210 t.count("header measure id=" + mMessage.id);
Mindy Pereira326c6602012-01-04 15:32:42 -08001211 t.start(MEASURE_TAG);
1212 }
1213 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1214 if (!mPreMeasuring) {
1215 t.pause(MEASURE_TAG);
1216 }
1217 }
Andy Huangbb56a152012-02-24 18:26:47 -08001218
Mindy Pereira326c6602012-01-04 15:32:42 -08001219}