blob: f982669bb05b6bd21c1c6cbe9d8affb5e174a898 [file] [log] [blame]
Vikram Aggarwal5e5ac742011-12-19 08:14:16 -08001/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
Mindy Pereira6f92de62011-12-19 11:31:48 -08004 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
Vikram Aggarwal5e5ac742011-12-19 08:14:16 -08009 * http://www.apache.org/licenses/LICENSE-2.0
Mindy Pereira6f92de62011-12-19 11:31:48 -080010 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
Vikram Aggarwal5e5ac742011-12-19 08:14:16 -080017
Andy Huang30e2c242012-01-06 18:14:30 -080018package com.android.mail.browse;
Mindy Pereira6f92de62011-12-19 11:31:48 -080019
Mindy Pereira6f92de62011-12-19 11:31:48 -080020import com.google.common.annotations.VisibleForTesting;
21
Mindy Pereira6f92de62011-12-19 11:31:48 -080022import android.content.Context;
23import android.content.res.Resources;
Mindy Pereiraf9573c52011-12-22 14:02:49 -080024import android.database.Cursor;
Mindy Pereira6f92de62011-12-19 11:31:48 -080025import android.graphics.Bitmap;
26import android.graphics.BitmapFactory;
27import android.graphics.Canvas;
28import android.graphics.Color;
Mindy Pereira6f92de62011-12-19 11:31:48 -080029import android.graphics.Rect;
Mindy Pereira6f92de62011-12-19 11:31:48 -080030import android.graphics.Typeface;
31import android.graphics.drawable.Drawable;
32import android.text.Layout.Alignment;
33import android.text.Spannable;
34import android.text.SpannableStringBuilder;
35import android.text.StaticLayout;
36import android.text.TextPaint;
37import android.text.TextUtils;
38import android.text.TextUtils.TruncateAt;
39import android.text.format.DateUtils;
40import android.text.style.CharacterStyle;
41import android.text.style.ForegroundColorSpan;
42import android.text.style.StyleSpan;
Mindy Pereira4401c902012-01-17 15:21:14 -080043import android.text.util.Rfc822Token;
44import android.text.util.Rfc822Tokenizer;
Mindy Pereira6f92de62011-12-19 11:31:48 -080045import android.util.SparseArray;
Mindy Pereira6f92de62011-12-19 11:31:48 -080046import android.view.MotionEvent;
47import android.view.View;
Marc Blankf892f0a2012-01-30 13:04:10 -080048import android.widget.ListView;
Mindy Pereira6f92de62011-12-19 11:31:48 -080049
Andy Huang732600e2012-01-10 17:47:17 -080050import com.android.mail.R;
Andy Huang732600e2012-01-10 17:47:17 -080051import com.android.mail.browse.ConversationItemViewModel.SenderFragment;
52import com.android.mail.perf.Timer;
Mindy Pereira4401c902012-01-17 15:21:14 -080053import com.android.mail.providers.Address;
Andy Huang732600e2012-01-10 17:47:17 -080054import com.android.mail.providers.Conversation;
Marc Blankc8a99422012-01-19 14:27:47 -080055import com.android.mail.providers.UIProvider.ConversationColumns;
Vikram Aggarwal1ddcf0f2012-01-13 11:45:02 -080056import com.android.mail.ui.ConversationSelectionSet;
57import com.android.mail.ui.ViewMode;
Andy Huang732600e2012-01-10 17:47:17 -080058import com.android.mail.utils.Utils;
59
Mindy Pereira0944e5e2012-01-03 11:14:58 -080060public class ConversationItemView extends View {
Mindy Pereira6f92de62011-12-19 11:31:48 -080061 // Timer.
62 private static int sLayoutCount = 0;
63 private static Timer sTimer; // Create the sTimer here if you need to do perf analysis.
64 private static final int PERF_LAYOUT_ITERATIONS = 50;
65 private static final String PERF_TAG_LAYOUT = "CCHV.layout";
66 private static final String PERF_TAG_CALCULATE_TEXTS_BITMAPS = "CCHV.txtsbmps";
67 private static final String PERF_TAG_CALCULATE_SENDER_SUBJECT = "CCHV.sendersubj";
68 private static final String PERF_TAG_CALCULATE_LABELS = "CCHV.labels";
69 private static final String PERF_TAG_CALCULATE_COORDINATES = "CCHV.coordinates";
70
71 // Static bitmaps.
72 private static Bitmap CHECKMARK_OFF;
73 private static Bitmap CHECKMARK_ON;
74 private static Bitmap STAR_OFF;
75 private static Bitmap STAR_ON;
76 private static Bitmap ATTACHMENT;
77 private static Bitmap ONLY_TO_ME;
78 private static Bitmap TO_ME_AND_OTHERS;
79 private static Bitmap IMPORTANT_ONLY_TO_ME;
80 private static Bitmap IMPORTANT_TO_ME_AND_OTHERS;
81 private static Bitmap IMPORTANT_TO_OTHERS;
82 private static Bitmap DATE_BACKGROUND;
83
84 // Static colors.
85 private static int DEFAULT_TEXT_COLOR;
86 private static int ACTIVATED_TEXT_COLOR;
87 private static int LIGHT_TEXT_COLOR;
88 private static int DRAFT_TEXT_COLOR;
89 private static int SUBJECT_TEXT_COLOR_READ;
90 private static int SUBJECT_TEXT_COLOR_UNREAD;
91 private static int SNIPPET_TEXT_COLOR_READ;
92 private static int SNIPPET_TEXT_COLOR_UNREAD;
93 private static int SENDERS_TEXT_COLOR_READ;
94 private static int SENDERS_TEXT_COLOR_UNREAD;
95 private static int DATE_TEXT_COLOR_READ;
96 private static int DATE_TEXT_COLOR_UNREAD;
97 private static int DATE_BACKGROUND_PADDING_LEFT;
98 private static int TOUCH_SLOP;
99 private static int sDateBackgroundHeight;
100 private static int sStandardScaledDimen;
101 private static CharacterStyle sLightTextStyle;
102 private static CharacterStyle sNormalTextStyle;
103
104 // Static paints.
105 private static TextPaint sPaint = new TextPaint();
106 private static TextPaint sLabelsPaint = new TextPaint();
107
108 // Backgrounds for different states.
109 private final SparseArray<Drawable> mBackgrounds = new SparseArray<Drawable>();
110
111 // Dimensions and coordinates.
112 private int mViewWidth = -1;
113 private int mMode = -1;
114 private int mDateX;
115 private int mPaperclipX;
116 private int mLabelsXEnd;
117 private int mSendersWidth;
118
119 /** Whether we're running under test mode. */
120 private boolean mTesting = false;
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800121 /** Whether we are on a tablet device or not */
122 private final boolean mTabletDevice;
Mindy Pereira6f92de62011-12-19 11:31:48 -0800123
124 @VisibleForTesting
Mindy Pereira0944e5e2012-01-03 11:14:58 -0800125 ConversationItemViewCoordinates mCoordinates;
Mindy Pereira6f92de62011-12-19 11:31:48 -0800126
Mindy Pereira6f92de62011-12-19 11:31:48 -0800127 private final Context mContext;
Mindy Pereira6f92de62011-12-19 11:31:48 -0800128
Mindy Pereira6f92de62011-12-19 11:31:48 -0800129 private String mAccount;
Mindy Pereira0944e5e2012-01-03 11:14:58 -0800130 private ConversationItemViewModel mHeader;
Mindy Pereira6f92de62011-12-19 11:31:48 -0800131 private ViewMode mViewMode;
Mindy Pereirac1b239d2011-12-20 12:20:50 -0800132 private boolean mDownEvent;
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800133 private boolean mChecked = false;
Mindy Pereira6f92de62011-12-19 11:31:48 -0800134 private static int sFadedColor = -1;
135 private static int sFadedActivatedColor = -1;
Mindy Pereira11b0bbd2012-01-24 10:06:17 -0800136 private ConversationSelectionSet mSelectedConversationSet;
Mindy Pereira6f92de62011-12-19 11:31:48 -0800137
138 static {
139 sPaint.setAntiAlias(true);
140 sLabelsPaint.setAntiAlias(true);
141 }
142
143 /**
144 * This handler will be called when user toggle a star in a conversation
145 * header view. It can be used to update the state of other views to ensure
146 * UI consistency.
147 */
148 public static interface StarHandler {
149 public void toggleStar(boolean toggleOn, long conversationId, long maxMessageId);
150 }
151
Mindy Pereira0944e5e2012-01-03 11:14:58 -0800152 public ConversationItemView(Context context, String account) {
Mindy Pereira6f92de62011-12-19 11:31:48 -0800153 super(context);
154 mContext = context.getApplicationContext();
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800155 mTabletDevice = Utils.useTabletUI(mContext);
156
Mindy Pereira6f92de62011-12-19 11:31:48 -0800157 mAccount = account;
158 Resources res = mContext.getResources();
159
160 if (CHECKMARK_OFF == null) {
161 // Initialize static bitmaps.
162 CHECKMARK_OFF = BitmapFactory.decodeResource(res,
163 R.drawable.btn_check_off_normal_holo_light);
164 CHECKMARK_ON = BitmapFactory.decodeResource(res,
165 R.drawable.btn_check_on_normal_holo_light);
166 STAR_OFF = BitmapFactory.decodeResource(res,
167 R.drawable.btn_star_off_normal_email_holo_light);
168 STAR_ON = BitmapFactory.decodeResource(res,
169 R.drawable.btn_star_on_normal_email_holo_light);
170 ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double);
171 TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single);
172 IMPORTANT_ONLY_TO_ME = BitmapFactory.decodeResource(res,
173 R.drawable.ic_email_caret_double_important_unread);
174 IMPORTANT_TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res,
175 R.drawable.ic_email_caret_single_important_unread);
176 IMPORTANT_TO_OTHERS = BitmapFactory.decodeResource(res,
177 R.drawable.ic_email_caret_none_important_unread);
178 ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light);
Vikram Aggarwale9a81032012-02-22 13:15:35 -0800179 DATE_BACKGROUND = BitmapFactory.decodeResource(res, R.drawable.folder_bg_holo_light);
Mindy Pereira6f92de62011-12-19 11:31:48 -0800180
181 // Initialize colors.
182 DEFAULT_TEXT_COLOR = res.getColor(R.color.default_text_color);
183 ACTIVATED_TEXT_COLOR = res.getColor(android.R.color.white);
184 LIGHT_TEXT_COLOR = res.getColor(R.color.light_text_color);
185 DRAFT_TEXT_COLOR = res.getColor(R.color.drafts);
186 SUBJECT_TEXT_COLOR_READ = res.getColor(R.color.subject_text_color_read);
187 SUBJECT_TEXT_COLOR_UNREAD = res.getColor(R.color.subject_text_color_unread);
188 SNIPPET_TEXT_COLOR_READ = res.getColor(R.color.snippet_text_color_read);
189 SNIPPET_TEXT_COLOR_UNREAD = res.getColor(R.color.snippet_text_color_unread);
190 SENDERS_TEXT_COLOR_READ = res.getColor(R.color.senders_text_color_read);
191 SENDERS_TEXT_COLOR_UNREAD = res.getColor(R.color.senders_text_color_unread);
192 DATE_TEXT_COLOR_READ = res.getColor(R.color.date_text_color_read);
193 DATE_TEXT_COLOR_UNREAD = res.getColor(R.color.date_text_color_unread);
194 DATE_BACKGROUND_PADDING_LEFT = res
195 .getDimensionPixelSize(R.dimen.date_background_padding_left);
196 TOUCH_SLOP = res.getDimensionPixelSize(R.dimen.touch_slop);
197 sDateBackgroundHeight = res.getDimensionPixelSize(R.dimen.date_background_height);
198 sStandardScaledDimen = res.getDimensionPixelSize(R.dimen.standard_scaled_dimen);
199
200 // Initialize static color.
201 sNormalTextStyle = new StyleSpan(Typeface.NORMAL);
202 sLightTextStyle = new ForegroundColorSpan(LIGHT_TEXT_COLOR);
Mindy Pereira6f92de62011-12-19 11:31:48 -0800203 }
204 }
205
Mindy Pereiraf9573c52011-12-22 14:02:49 -0800206 public void bind(Cursor cursor, StarHandler starHandler, String account,
Mindy Pereira11b0bbd2012-01-24 10:06:17 -0800207 CharSequence displayedLabel, ViewMode viewMode, ConversationSelectionSet set) {
Mindy Pereiraf9573c52011-12-22 14:02:49 -0800208 mAccount = account;
209 mViewMode = viewMode;
Mindy Pereira0944e5e2012-01-03 11:14:58 -0800210 mHeader = ConversationItemViewModel.forCursor(account, cursor);
Mindy Pereira11b0bbd2012-01-24 10:06:17 -0800211 mSelectedConversationSet = set;
Mindy Pereiraf9573c52011-12-22 14:02:49 -0800212 setContentDescription(mHeader.getContentDescription(mContext));
213 requestLayout();
214 }
215
Andy Huang732600e2012-01-10 17:47:17 -0800216 public Conversation getConversation() {
217 return mHeader.conversation;
218 }
219
Mindy Pereira6f92de62011-12-19 11:31:48 -0800220 /**
221 * Sets the mode. Only used for testing.
222 */
223 @VisibleForTesting
224 void setMode(int mode) {
225 mMode = mode;
226 mTesting = true;
227 }
228
229 private static void startTimer(String tag) {
230 if (sTimer != null) {
231 sTimer.start(tag);
232 }
233 }
234
235 private static void pauseTimer(String tag) {
236 if (sTimer != null) {
237 sTimer.pause(tag);
238 }
239 }
240
241 @Override
242 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
243 startTimer(PERF_TAG_LAYOUT);
244
245 super.onLayout(changed, left, top, right, bottom);
246
247 int width = right - left;
248 if (width != mViewWidth) {
249 mViewWidth = width;
250 if (!mTesting) {
Mindy Pereira0944e5e2012-01-03 11:14:58 -0800251 mMode = ConversationItemViewCoordinates.getMode(mContext, mViewMode);
Mindy Pereira6f92de62011-12-19 11:31:48 -0800252 }
253 }
254 mHeader.viewWidth = mViewWidth;
255 Resources res = getResources();
256 mHeader.standardScaledDimen = res.getDimensionPixelOffset(R.dimen.standard_scaled_dimen);
257 if (mHeader.standardScaledDimen != sStandardScaledDimen) {
258 // Large Text has been toggle on/off. Update the static dimens.
259 sStandardScaledDimen = mHeader.standardScaledDimen;
Mindy Pereira0944e5e2012-01-03 11:14:58 -0800260 ConversationItemViewCoordinates.refreshConversationHeights(mContext);
Mindy Pereira6f92de62011-12-19 11:31:48 -0800261 sDateBackgroundHeight = res.getDimensionPixelSize(R.dimen.date_background_height);
262 }
Mindy Pereira0944e5e2012-01-03 11:14:58 -0800263 mCoordinates = ConversationItemViewCoordinates.forWidth(mContext, mViewWidth, mMode,
Mindy Pereira6f92de62011-12-19 11:31:48 -0800264 mHeader.standardScaledDimen);
265 calculateTextsAndBitmaps();
266 calculateCoordinates();
267 mHeader.validate(mContext);
268
269 pauseTimer(PERF_TAG_LAYOUT);
270 if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) {
271 sTimer.dumpResults();
272 sTimer = new Timer();
273 sLayoutCount = 0;
274 }
275 }
276
277 @Override
278 public void setBackgroundResource(int resourceId) {
279 Drawable drawable = mBackgrounds.get(resourceId);
280 if (drawable == null) {
281 drawable = getResources().getDrawable(resourceId);
282 mBackgrounds.put(resourceId, drawable);
283 }
284 if (getBackground() != drawable) {
285 super.setBackgroundDrawable(drawable);
286 }
287 }
288
289 private void calculateTextsAndBitmaps() {
290 startTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800291 if (mSelectedConversationSet != null) {
292 mChecked = mSelectedConversationSet.contains(mHeader.conversation);
293 }
Mindy Pereira6f92de62011-12-19 11:31:48 -0800294 // Update font color.
295 int fontColor = getFontColor(DEFAULT_TEXT_COLOR);
296 boolean fontChanged = false;
297 if (mHeader.fontColor != fontColor) {
298 fontChanged = true;
299 mHeader.fontColor = fontColor;
300 }
301
Marc Blank8d69d4e2012-01-25 12:04:28 -0800302 boolean isUnread = mHeader.unread;
Mindy Pereira6f92de62011-12-19 11:31:48 -0800303
304 final boolean checkboxEnabled = true;
305 if (mHeader.checkboxVisible != checkboxEnabled) {
306 mHeader.checkboxVisible = checkboxEnabled;
307 }
308
309 // Update background.
310 updateBackground(isUnread);
311
312 if (mHeader.isLayoutValid(mContext)) {
313 // Relayout subject if font color has changed.
314 if (fontChanged) {
315 createSubjectSpans(isUnread);
316 layoutSubject();
317 }
318 pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
319 return;
320 }
321
322 // Initialize label displayer.
323 startTimer(PERF_TAG_CALCULATE_LABELS);
Mindy Pereira6f92de62011-12-19 11:31:48 -0800324 pauseTimer(PERF_TAG_CALCULATE_LABELS);
325
326 // Star.
327 mHeader.starBitmap = mHeader.starred ? STAR_ON : STAR_OFF;
328
329 // Date.
Andy Huang732600e2012-01-10 17:47:17 -0800330 mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext,
331 mHeader.conversation.dateMs).toString();
Mindy Pereira6f92de62011-12-19 11:31:48 -0800332
333 // Paper clip icon.
334 mHeader.paperclip = null;
Andy Huang732600e2012-01-10 17:47:17 -0800335 if (mHeader.conversation.hasAttachments) {
Mindy Pereira6f92de62011-12-19 11:31:48 -0800336 mHeader.paperclip = ATTACHMENT;
337 }
338
339 // Personal level.
340 mHeader.personalLevelBitmap = null;
341
342 startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
343
344 // Subject.
345 createSubjectSpans(isUnread);
346
347 // Parse senders fragments.
348 parseSendersFragments(isUnread);
349
350 pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
351 pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
352 }
353
354 private void createSubjectSpans(boolean isUnread) {
Andy Huang732600e2012-01-10 17:47:17 -0800355 final String subject = filterTag(mHeader.conversation.subject);
356 final String snippet = mHeader.conversation.snippet;
Mindy Pereira6f92de62011-12-19 11:31:48 -0800357 int subjectColor = isUnread ? SUBJECT_TEXT_COLOR_UNREAD : SUBJECT_TEXT_COLOR_READ;
358 int snippetColor = isUnread ? SNIPPET_TEXT_COLOR_UNREAD : SNIPPET_TEXT_COLOR_READ;
359 mHeader.subjectText = new SpannableStringBuilder(mContext.getString(
360 R.string.subject_and_snippet, subject, snippet));
361 if (isUnread) {
362 mHeader.subjectText.setSpan(new StyleSpan(Typeface.BOLD), 0, subject.length(),
363 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
364 }
365 int fontColor = getFontColor(subjectColor);
366 mHeader.subjectText.setSpan(new ForegroundColorSpan(fontColor), 0,
367 subject.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
368 fontColor = getFontColor(snippetColor);
369 mHeader.subjectText.setSpan(new ForegroundColorSpan(fontColor), subject.length() + 1,
370 mHeader.subjectText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
371 }
372
373 private int getFontColor(int defaultColor) {
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800374 return isActivated() && mTabletDevice ? ACTIVATED_TEXT_COLOR
Mindy Pereira6f92de62011-12-19 11:31:48 -0800375 : defaultColor;
376 }
377
378 private void layoutSubject() {
379 sPaint.setTextSize(mCoordinates.subjectFontSize);
380 sPaint.setColor(mHeader.fontColor);
381 mHeader.subjectLayout = new StaticLayout(mHeader.subjectText, sPaint,
382 mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
383 if (mCoordinates.subjectLineCount < mHeader.subjectLayout.getLineCount()) {
384 int end = mHeader.subjectLayout.getLineEnd(mCoordinates.subjectLineCount - 1);
385 mHeader.subjectLayout = new StaticLayout(mHeader.subjectText.subSequence(0, end),
386 sPaint, mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
387 }
388 }
389
390 /**
391 * Parses senders text into small fragments.
392 */
393 private void parseSendersFragments(boolean isUnread) {
Mindy Pereira259e5222012-01-12 13:20:27 -0800394 if (TextUtils.isEmpty(mHeader.conversation.senders)) {
Mindy Pereiraf9573c52011-12-22 14:02:49 -0800395 return;
396 }
Mindy Pereira4401c902012-01-17 15:21:14 -0800397 mHeader.sendersText = formatSenders(mHeader.conversation.senders);
Mindy Pereira259e5222012-01-12 13:20:27 -0800398 mHeader.addSenderFragment(0, mHeader.sendersText.length(), sNormalTextStyle, true);
Mindy Pereira6f92de62011-12-19 11:31:48 -0800399 }
400
Mindy Pereira4401c902012-01-17 15:21:14 -0800401 private String formatSenders(String sendersString) {
402 String[] senders = TextUtils.split(sendersString, Address.ADDRESS_DELIMETER);
403 String[] namesOnly = new String[senders.length];
404 Rfc822Token[] senderTokens;
405 String display;
406 for (int i = 0; i < senders.length; i++) {
407 senderTokens = Rfc822Tokenizer.tokenize(senders[i]);
408 if (senderTokens != null && senderTokens.length > 0) {
409 display = senderTokens[0].getName();
410 if (TextUtils.isEmpty(display)) {
411 display = senderTokens[0].getAddress();
412 }
413 namesOnly[i] = display;
414 }
415 }
416 return TextUtils.join(Address.ADDRESS_DELIMETER + " ", namesOnly);
417 }
418
Mindy Pereira6f92de62011-12-19 11:31:48 -0800419 private boolean canFitFragment(int width, int line, int fixedWidth) {
420 if (line == mCoordinates.sendersLineCount) {
421 return width + fixedWidth <= mSendersWidth;
422 } else {
423 return width <= mSendersWidth;
424 }
425 }
426
427 private void calculateCoordinates() {
428 startTimer(PERF_TAG_CALCULATE_COORDINATES);
429
430 sPaint.setTextSize(mCoordinates.dateFontSize);
431 sPaint.setTypeface(Typeface.DEFAULT);
432 mDateX = mCoordinates.dateXEnd - (int) sPaint.measureText(mHeader.dateText);
433
434 mPaperclipX = mDateX - ATTACHMENT.getWidth();
435
436 int cellWidth = mContext.getResources().getDimensionPixelSize(R.dimen.label_cell_width);
437
Mindy Pereira0944e5e2012-01-03 11:14:58 -0800438 if (ConversationItemViewCoordinates.displayLabelsAboveDate(mMode)) {
Mindy Pereira6f92de62011-12-19 11:31:48 -0800439 mLabelsXEnd = mCoordinates.dateXEnd;
440 mSendersWidth = mCoordinates.sendersWidth;
441 } else {
442 if (mHeader.paperclip != null) {
443 mLabelsXEnd = mPaperclipX;
444 } else {
445 mLabelsXEnd = mDateX - cellWidth / 2;
446 }
447 mSendersWidth = mLabelsXEnd - mCoordinates.sendersX - 2 * cellWidth;
448 }
449
450 if (mHeader.isLayoutValid(mContext)) {
451 pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
452 return;
453 }
454
455 // Layout subject.
456 layoutSubject();
457
458 // First pass to calculate width of each fragment.
459 int totalWidth = 0;
460 int fixedWidth = 0;
461 sPaint.setTextSize(mCoordinates.sendersFontSize);
462 sPaint.setTypeface(Typeface.DEFAULT);
463 for (SenderFragment senderFragment : mHeader.senderFragments) {
464 CharacterStyle style = senderFragment.style;
465 int start = senderFragment.start;
466 int end = senderFragment.end;
467 style.updateDrawState(sPaint);
468 senderFragment.width = (int) sPaint.measureText(mHeader.sendersText, start, end);
469 boolean isFixed = senderFragment.isFixed;
470 if (isFixed) {
471 fixedWidth += senderFragment.width;
472 }
473 totalWidth += senderFragment.width;
474 }
475
476 // Second pass to layout each fragment.
477 int sendersY = mCoordinates.sendersY - mCoordinates.sendersAscent;
Mindy Pereira0944e5e2012-01-03 11:14:58 -0800478 if (!ConversationItemViewCoordinates.displaySendersInline(mMode)) {
Mindy Pereira6f92de62011-12-19 11:31:48 -0800479 sendersY += totalWidth <= mSendersWidth ? mCoordinates.sendersLineHeight / 2 : 0;
480 }
481 totalWidth = 0;
482 int currentLine = 1;
483 boolean ellipsize = false;
484 for (SenderFragment senderFragment : mHeader.senderFragments) {
485 CharacterStyle style = senderFragment.style;
486 int start = senderFragment.start;
487 int end = senderFragment.end;
488 int width = senderFragment.width;
489 boolean isFixed = senderFragment.isFixed;
490 style.updateDrawState(sPaint);
491
492 // No more width available, we'll only show fixed fragments.
493 if (ellipsize && !isFixed) {
494 senderFragment.shouldDisplay = false;
495 continue;
496 }
497
498 // New line and ellipsize text if needed.
499 senderFragment.ellipsizedText = null;
500 if (isFixed) {
501 fixedWidth -= width;
502 }
503 if (!canFitFragment(totalWidth + width, currentLine, fixedWidth)) {
504 // The text is too long, new line won't help. We have to
505 // ellipsize text.
506 if (totalWidth == 0) {
507 ellipsize = true;
508 } else {
509 // New line.
510 if (currentLine < mCoordinates.sendersLineCount) {
511 currentLine++;
512 sendersY += mCoordinates.sendersLineHeight;
513 totalWidth = 0;
514 // The text is still too long, we have to ellipsize
515 // text.
516 if (totalWidth + width > mSendersWidth) {
517 ellipsize = true;
518 }
519 } else {
520 ellipsize = true;
521 }
522 }
523
524 if (ellipsize) {
525 width = mSendersWidth - totalWidth;
526 // No more new line, we have to reserve width for fixed
527 // fragments.
528 if (currentLine == mCoordinates.sendersLineCount) {
529 width -= fixedWidth;
530 }
531 senderFragment.ellipsizedText = TextUtils.ellipsize(
532 mHeader.sendersText.substring(start, end), sPaint, width,
533 TruncateAt.END).toString();
534 width = (int) sPaint.measureText(senderFragment.ellipsizedText);
535 }
536 }
537 senderFragment.x = mCoordinates.sendersX + totalWidth;
538 senderFragment.y = sendersY;
539 senderFragment.shouldDisplay = true;
540 totalWidth += width;
541 }
542
543 pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
544 }
545
546 /**
547 * If the subject contains the tag of a mailing-list (text surrounded with
548 * []), return the subject with that tag ellipsized, e.g.
549 * "[android-gmail-team] Hello" -> "[andr...] Hello"
550 */
551 private String filterTag(String subject) {
552 String result = subject;
553 String formatString = getContext().getResources().getString(R.string.filtered_tag);
554 if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') {
555 int end = subject.indexOf(']');
556 if (end > 0) {
557 String tag = subject.substring(1, end);
558 result = String.format(formatString, Utils.ellipsize(tag, 7),
559 subject.substring(end + 1));
560 }
561 }
562 return result;
563 }
564
565 @Override
566 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
567 int width = measureWidth(widthMeasureSpec);
568 int height = measureHeight(heightMeasureSpec,
Mindy Pereira0944e5e2012-01-03 11:14:58 -0800569 ConversationItemViewCoordinates.getMode(mContext, mViewMode));
Mindy Pereira6f92de62011-12-19 11:31:48 -0800570 setMeasuredDimension(width, height);
571 }
572
573 /**
574 * Determine the width of this view.
575 *
576 * @param measureSpec A measureSpec packed into an int
577 * @return The width of the view, honoring constraints from measureSpec
578 */
579 private int measureWidth(int measureSpec) {
580 int result = 0;
581 int specMode = MeasureSpec.getMode(measureSpec);
582 int specSize = MeasureSpec.getSize(measureSpec);
583
584 if (specMode == MeasureSpec.EXACTLY) {
585 // We were told how big to be
586 result = specSize;
587 } else {
588 // Measure the text
589 result = mViewWidth;
590 if (specMode == MeasureSpec.AT_MOST) {
591 // Respect AT_MOST value if that was what is called for by
592 // measureSpec
593 result = Math.min(result, specSize);
594 }
595 }
596 return result;
597 }
598
599 /**
600 * Determine the height of this view.
601 *
602 * @param measureSpec A measureSpec packed into an int
603 * @param mode The current mode of this view
604 * @return The height of the view, honoring constraints from measureSpec
605 */
606 private int measureHeight(int measureSpec, int mode) {
607 int result = 0;
608 int specMode = MeasureSpec.getMode(measureSpec);
609 int specSize = MeasureSpec.getSize(measureSpec);
610
611 if (specMode == MeasureSpec.EXACTLY) {
612 // We were told how big to be
613 result = specSize;
614 } else {
615 // Measure the text
Mindy Pereira0944e5e2012-01-03 11:14:58 -0800616 result = ConversationItemViewCoordinates.getHeight(mContext, mode);
Mindy Pereira6f92de62011-12-19 11:31:48 -0800617 if (specMode == MeasureSpec.AT_MOST) {
618 // Respect AT_MOST value if that was what is called for by
619 // measureSpec
620 result = Math.min(result, specSize);
621 }
622 }
623 return result;
624 }
625
626 @Override
627 protected void onDraw(Canvas canvas) {
628 // Check mark.
629 if (mHeader.checkboxVisible) {
Mindy Pereirac1b239d2011-12-20 12:20:50 -0800630 Bitmap checkmark = mChecked ? CHECKMARK_ON : CHECKMARK_OFF;
Mindy Pereira6f92de62011-12-19 11:31:48 -0800631 canvas.drawBitmap(checkmark, mCoordinates.checkmarkX, mCoordinates.checkmarkY, sPaint);
632 }
633
634 // Personal Level.
635 if (mHeader.personalLevelBitmap != null) {
636 canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalLevelX,
637 mCoordinates.personalLevelY, sPaint);
638 }
639
640 // Senders.
641 sPaint.setTextSize(mCoordinates.sendersFontSize);
642 sPaint.setTypeface(Typeface.DEFAULT);
Marc Blank8d69d4e2012-01-25 12:04:28 -0800643 boolean isUnread = mHeader.unread;
Mindy Pereira6f92de62011-12-19 11:31:48 -0800644 int sendersColor = getFontColor(isUnread ? SENDERS_TEXT_COLOR_UNREAD
645 : SENDERS_TEXT_COLOR_READ);
646 sPaint.setColor(sendersColor);
647 for (SenderFragment fragment : mHeader.senderFragments) {
648 if (fragment.shouldDisplay) {
649 sPaint.setTypeface(Typeface.DEFAULT);
650 fragment.style.updateDrawState(sPaint);
651 if (fragment.ellipsizedText != null) {
652 canvas.drawText(fragment.ellipsizedText, fragment.x, fragment.y, sPaint);
653 } else {
654 canvas.drawText(mHeader.sendersText, fragment.start, fragment.end, fragment.x,
655 fragment.y, sPaint);
656 }
657 }
658 }
659
660 // Subject.
661 sPaint.setTextSize(mCoordinates.subjectFontSize);
662 sPaint.setTypeface(Typeface.DEFAULT);
663 sPaint.setColor(mHeader.fontColor);
664 canvas.save();
665 canvas.translate(mCoordinates.subjectX,
666 mCoordinates.subjectY + mHeader.subjectLayout.getTopPadding());
667 mHeader.subjectLayout.draw(canvas);
668 canvas.restore();
669
670 // Date background: shown when there is an attachment or a visible
671 // label.
672 if (!isActivated()
Andy Huang732600e2012-01-10 17:47:17 -0800673 && mHeader.conversation.hasAttachments
Mindy Pereira0944e5e2012-01-03 11:14:58 -0800674 && ConversationItemViewCoordinates.showAttachmentBackground(mMode)) {
Mindy Pereira6f92de62011-12-19 11:31:48 -0800675 mHeader.dateBackground = DATE_BACKGROUND;
Andy Huang732600e2012-01-10 17:47:17 -0800676 int leftOffset = (mHeader.conversation.hasAttachments ? mPaperclipX : mDateX)
Mindy Pereira6f92de62011-12-19 11:31:48 -0800677 - DATE_BACKGROUND_PADDING_LEFT;
678 int top = mCoordinates.labelsY;
679 Rect src = new Rect(0, 0, mHeader.dateBackground.getWidth(), mHeader.dateBackground
680 .getHeight());
681 Rect dst = new Rect(leftOffset, top, mViewWidth, top + sDateBackgroundHeight);
682 canvas.drawBitmap(mHeader.dateBackground, src, dst, sPaint);
683 } else {
684 mHeader.dateBackground = null;
685 }
686
687 // Date.
688 sPaint.setTextSize(mCoordinates.dateFontSize);
689 sPaint.setTypeface(Typeface.DEFAULT);
690 sPaint.setColor(isUnread ? DATE_TEXT_COLOR_UNREAD : DATE_TEXT_COLOR_READ);
691 drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateY - mCoordinates.dateAscent,
692 sPaint);
693
694 // Paper clip icon.
695 if (mHeader.paperclip != null) {
696 canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint);
697 }
698
699 if (mHeader.faded) {
700 int fadedColor = -1;
701 if (sFadedActivatedColor == -1) {
702 sFadedActivatedColor = mContext.getResources().getColor(
703 R.color.faded_activated_conversation_header);
704 }
705 fadedColor = sFadedActivatedColor;
706 int restoreState = canvas.save();
707 Rect bounds = canvas.getClipBounds();
708 canvas.clipRect(bounds.left, bounds.top, bounds.right
709 - mContext.getResources().getDimensionPixelSize(R.dimen.triangle_width),
710 bounds.bottom);
711 canvas.drawARGB(Color.alpha(fadedColor), Color.red(fadedColor),
712 Color.green(fadedColor), Color.blue(fadedColor));
713 canvas.restoreToCount(restoreState);
714 }
715
716 // Star.
717 canvas.drawBitmap(mHeader.starBitmap, mCoordinates.starX, mCoordinates.starY, sPaint);
718 }
719
720 private void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) {
721 canvas.drawText(s, 0, s.length(), x, y, paint);
722 }
723
724 private void updateBackground(boolean isUnread) {
725 if (isUnread) {
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800726 if (mTabletDevice && mViewMode.getMode() == ViewMode.CONVERSATION_LIST) {
Mindy Pereira9d59a9a2011-12-20 13:10:28 -0800727 if (mChecked) {
728 setBackgroundResource(R.drawable.list_conversation_wide_unread_selected_holo);
729 } else {
730 setBackgroundResource(R.drawable.conversation_wide_unread_selector);
731 }
Mindy Pereira6f92de62011-12-19 11:31:48 -0800732 } else {
Mindy Pereira9d59a9a2011-12-20 13:10:28 -0800733 if (mChecked) {
734 setCheckedActivatedBackground();
735 } else {
736 setBackgroundResource(R.drawable.conversation_unread_selector);
737 }
Mindy Pereira6f92de62011-12-19 11:31:48 -0800738 }
739 } else {
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800740 if (mTabletDevice && mViewMode.getMode() == ViewMode.CONVERSATION_LIST) {
Mindy Pereira9d59a9a2011-12-20 13:10:28 -0800741 if (mChecked) {
742 setBackgroundResource(R.drawable.list_conversation_wide_read_selected_holo);
743 } else {
744 setBackgroundResource(R.drawable.conversation_wide_read_selector);
745 }
Mindy Pereira6f92de62011-12-19 11:31:48 -0800746 } else {
Mindy Pereira9d59a9a2011-12-20 13:10:28 -0800747 if (mChecked) {
748 setCheckedActivatedBackground();
749 } else {
750 setBackgroundResource(R.drawable.conversation_read_selector);
751 }
Mindy Pereira6f92de62011-12-19 11:31:48 -0800752 }
753 }
754 }
Mindy Pereirac1b239d2011-12-20 12:20:50 -0800755
Mindy Pereira9d59a9a2011-12-20 13:10:28 -0800756 private void setCheckedActivatedBackground() {
Vikram Aggarwalfa131a22012-02-02 13:56:22 -0800757 if (isActivated() && mTabletDevice) {
Mindy Pereira9d59a9a2011-12-20 13:10:28 -0800758 setBackgroundResource(R.drawable.list_arrow_selected_holo);
759 } else {
760 setBackgroundResource(R.drawable.list_selected_holo);
761 }
762 }
763
Mindy Pereirac1b239d2011-12-20 12:20:50 -0800764 /**
765 * Toggle the check mark on this view and update the conversation
766 */
767 public void toggleCheckMark() {
768 mChecked = !mChecked;
Marc Blankf892f0a2012-01-30 13:04:10 -0800769 Conversation conv = mHeader.conversation;
770 // Set the list position of this item in the conversation
771 conv.position = mChecked ? ((ListView)getParent()).getPositionForView(this)
772 : Conversation.NO_POSITION;
Vikram Aggarwald247dc92012-02-10 15:49:01 -0800773 if (mSelectedConversationSet != null) {
774 mSelectedConversationSet.toggle(conv);
775 }
Mindy Pereirac1b239d2011-12-20 12:20:50 -0800776 // We update the background after the checked state has changed now that
777 // we have a selected background asset. Setting the background usually
778 // waits for a layout pass, but we don't need a full layout, just an
779 // update to the background.
780 requestLayout();
781 }
782
783 /**
784 * Toggle the star on this view and update the conversation.
785 */
786 private void toggleStar() {
787 mHeader.starred = !mHeader.starred;
788 mHeader.starBitmap = mHeader.starred ? STAR_ON : STAR_OFF;
789 postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX
790 + mHeader.starBitmap.getWidth(),
791 mCoordinates.starY + mHeader.starBitmap.getHeight());
Marc Blankc8a99422012-01-19 14:27:47 -0800792 // Generalize this...
793 mHeader.conversation.updateBoolean(mContext, ConversationColumns.STARRED, mHeader.starred);
Mindy Pereirac1b239d2011-12-20 12:20:50 -0800794 }
795
796 private boolean touchCheckmark(float x, float y) {
797 // Everything before senders and include a touch slop.
798 return mHeader.checkboxVisible && x < mCoordinates.sendersX + TOUCH_SLOP;
799 }
800
801 private boolean touchStar(float x, float y) {
802 // Everything after the star and include a touch slop.
803 return x > mCoordinates.starX - TOUCH_SLOP;
804 }
805
806 @Override
807 public boolean onTouchEvent(MotionEvent event) {
808 boolean handled = false;
809
810 int x = (int) event.getX();
811 int y = (int) event.getY();
812 switch (event.getAction()) {
813 case MotionEvent.ACTION_DOWN:
814 mDownEvent = true;
815 if (touchCheckmark(x, y) || touchStar(x, y)) {
816 handled = true;
817 }
818 break;
819
820 case MotionEvent.ACTION_CANCEL:
821 mDownEvent = false;
822 break;
823
824 case MotionEvent.ACTION_UP:
825 if (mDownEvent) {
826 if (touchCheckmark(x, y)) {
827 // Touch on the check mark
828 toggleCheckMark();
829 } else if (touchStar(x, y)) {
830 // Touch on the star
831 toggleStar();
832 }
833 handled = true;
834 }
835 break;
836 }
837
838 if (!handled) {
839 handled = super.onTouchEvent(event);
840 }
841
842 return handled;
843 }
Mindy Pereira6f92de62011-12-19 11:31:48 -0800844}