Choose thread icon more carefully
b/17298161
Choosing the avatar to draw as the thread icon follows this algorithm:
1) Prefer the sender of the first unread message.
2) If all messages are read, prefer the last sender that is not the
current account.
3) If all messages were sent from the current account (e.g. user is
emailing themselves), use the last sender (aka current account).
In the process of doing this work the last remaining dependency on
DividedImageCanvas was broken, so it could be removed at this time.
Tests confirming the new behavior have been added to SendersFormattingTests.
Change-Id: If8e066de8cb98f2f95b019a88a2fdadc2f9f5090
diff --git a/src/com/android/mail/browse/ConversationItemView.java b/src/com/android/mail/browse/ConversationItemView.java
index 632075f..028c3e1 100644
--- a/src/com/android/mail/browse/ConversationItemView.java
+++ b/src/com/android/mail/browse/ConversationItemView.java
@@ -67,6 +67,7 @@
import com.android.mail.bitmap.CheckableContactFlipDrawable;
import com.android.mail.bitmap.ContactDrawable;
import com.android.mail.perf.Timer;
+import com.android.mail.providers.Account;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
import com.android.mail.providers.UIProvider;
@@ -77,7 +78,6 @@
import com.android.mail.ui.ControllableActivity;
import com.android.mail.ui.ConversationCheckedSet;
import com.android.mail.ui.ConversationSetObserver;
-import com.android.mail.ui.DividedImageCanvas.InvalidateCallback;
import com.android.mail.ui.FolderDisplayer;
import com.android.mail.ui.SwipeableItemView;
import com.android.mail.ui.SwipeableListView;
@@ -89,12 +89,11 @@
import com.android.mail.utils.ViewUtils;
import com.google.common.annotations.VisibleForTesting;
-import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class ConversationItemView extends View
- implements SwipeableItemView, ToggleableItem, InvalidateCallback, ConversationSetObserver,
+ implements SwipeableItemView, ToggleableItem, ConversationSetObserver,
BadgeSpan.BadgeSpanDimensions {
// Timer.
@@ -184,7 +183,7 @@
private final Context mContext;
- public ConversationItemViewModel mHeader;
+ private ConversationItemViewModel mHeader;
private boolean mDownEvent;
private boolean mChecked = false;
private ConversationCheckedSet mCheckedConversationSet;
@@ -194,7 +193,7 @@
private boolean mDividerEnabled;
private AnimatedAdapter mAdapter;
private float mAnimatedHeightFraction = 1.0f;
- private final String mAccount;
+ private final Account mAccount;
private ControllableActivity mActivity;
private final TextView mSendersTextView;
private final TextView mSubjectTextView;
@@ -512,7 +511,7 @@
}
}
- public ConversationItemView(Context context, String account) {
+ public ConversationItemView(Context context, Account account) {
super(context);
Utils.traceBeginSection("CIVC constructor");
setClickable(true);
@@ -640,8 +639,8 @@
final boolean swipeEnabled, final boolean importanceMarkersEnabled,
final boolean showChevronsEnabled, final AnimatedAdapter adapter) {
Utils.traceBeginSection("CIVC.bind");
- bind(ConversationItemViewModel.forConversation(mAccount, conversation), activity,
- null /* conversationItemAreaClickListener */,
+ bind(ConversationItemViewModel.forConversation(mAccount.getEmailAddress(), conversation),
+ activity, null /* conversationItemAreaClickListener */,
set, folder, checkboxOrSenderImage, swipeEnabled, importanceMarkersEnabled,
showChevronsEnabled, adapter, -1 /* backgroundOverrideResId */,
null /* photoBitmap */, false /* useFullMargins */, true /* mDividerEnabled */);
@@ -680,8 +679,7 @@
final boolean newlyBound = header.conversation.id != mHeader.conversation.id;
// If this was previously bound to a different conversation, remove any contact photo
// manager requests.
- if (newlyBound || (mHeader.displayableNames != null && !mHeader
- .displayableNames.equals(header.displayableNames))) {
+ if (newlyBound || (!mHeader.displayableNames.equals(header.displayableNames))) {
mSendersImageView.getContactDrawable().unbind();
}
@@ -951,18 +949,20 @@
.createMessageInfo(context, mHeader.conversation, true);
int maxChars = ConversationItemViewCoordinates.getSendersLength(context,
mCoordinates.getMode(), mHeader.conversation.hasAttachments);
- mHeader.displayableEmails = new ArrayList<String>();
- mHeader.displayableNames = new ArrayList<String>();
- mHeader.styledNames = new ArrayList<SpannableString>();
+
+ mHeader.mSenderAvatarModel.clear();
+ mHeader.displayableNames.clear();
+ mHeader.styledNames.clear();
SendersView.format(context, mHeader.conversation.conversationInfo,
mHeader.messageInfoString.toString(), maxChars, mHeader.styledNames,
- mHeader.displayableNames, mHeader.displayableEmails, mAccount,
- mDisplayedFolder.shouldShowRecipients(), true);
+ mHeader.displayableNames, mHeader.mSenderAvatarModel,
+ mAccount.getEmailAddress(), mDisplayedFolder.shouldShowRecipients(), true);
- if (mHeader.displayableEmails.isEmpty() && mHeader.hasDraftMessage) {
- mHeader.displayableEmails.add(mAccount);
- mHeader.displayableNames.add(mAccount);
+ if (mHeader.mSenderAvatarModel.isNotPopulated() && mHeader.hasDraftMessage) {
+ mHeader.mSenderAvatarModel.populate(mAccount.getDisplayName(),
+ mAccount.getEmailAddress());
+ mHeader.displayableNames.add(mAccount.getDisplayName());
}
// If we have displayable senders, load their thumbnails
@@ -996,8 +996,7 @@
// is immutable.
private void loadImages() {
if (mGadgetMode != ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO
- || mHeader.displayableEmails == null
- || mHeader.displayableEmails.isEmpty()) {
+ || mHeader.mSenderAvatarModel.isNotPopulated()) {
return;
}
if (mCoordinates.contactImagesWidth <= 0 || mCoordinates.contactImagesHeight <= 0) {
@@ -1015,7 +1014,8 @@
final ContactDrawable drawable = mSendersImageView.getContactDrawable();
drawable.setDecodeDimensions(mCoordinates.contactImagesWidth,
mCoordinates.contactImagesHeight);
- drawable.bind(mHeader.displayableNames.get(0), mHeader.displayableEmails.get(0));
+ drawable.bind(mHeader.mSenderAvatarModel.getName(),
+ mHeader.mSenderAvatarModel.getEmailAddress());
Utils.traceEndSection();
}
@@ -1935,7 +1935,7 @@
return sScrollSlop;
}
- public String getAccount() {
- return mAccount;
+ public String getAccountEmailAddress() {
+ return mAccount.getEmailAddress();
}
}
diff --git a/src/com/android/mail/browse/ConversationItemViewModel.java b/src/com/android/mail/browse/ConversationItemViewModel.java
index cc30d85..84cf59a 100644
--- a/src/com/android/mail/browse/ConversationItemViewModel.java
+++ b/src/com/android/mail/browse/ConversationItemViewModel.java
@@ -24,7 +24,6 @@
import android.text.StaticLayout;
import android.text.TextUtils;
import android.text.format.DateUtils;
-import android.text.style.CharacterStyle;
import android.util.LruCache;
import android.util.Pair;
@@ -119,21 +118,20 @@
private String mContentDescription;
/**
- * Email addresses corresponding to the senders/recipients that will be displayed on the top
- * line; used to generate the conversation icon.
+ * The email address and name of the sender whose avatar will be drawn as a conversation icon.
*/
- public ArrayList<String> displayableEmails;
+ public final SenderAvatarModel mSenderAvatarModel = new SenderAvatarModel();
/**
* Display names corresponding to the email address for the senders/recipients that will be
* displayed on the top line.
*/
- public ArrayList<String> displayableNames;
+ public final ArrayList<String> displayableNames = new ArrayList<>();
/**
* A styled version of the {@link #displayableNames} to be displayed on the top line.
*/
- public ArrayList<SpannableString> styledNames;
+ public final ArrayList<SpannableString> styledNames = new ArrayList<>();
/**
* Returns the view model for a conversation. If the model doesn't exist for this conversation
@@ -318,4 +316,45 @@
sConversationHeaderMap.evictAll();
}
}
+
+ /**
+ * This mutable model stores the name and email address of the sender for whom an avatar will
+ * be drawn as the conversation icon.
+ */
+ public static final class SenderAvatarModel {
+ private String mEmailAddress;
+ private String mName;
+
+ public String getEmailAddress() {
+ return mEmailAddress;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Removes the name and email address of the participant of this avatar.
+ */
+ public void clear() {
+ populate(null, null);
+ }
+
+ /**
+ * @param name the name of the participant of this avatar
+ * @param emailAddress the email address of the participant of this avatar
+ */
+ public void populate(String name, String emailAddress) {
+ mName = name;
+ mEmailAddress = emailAddress;
+ }
+
+ /**
+ * @return <tt>true</tt> if this model does not yet contain enough data to produce an
+ * avatar image; <tt>false</tt> otherwise
+ */
+ public boolean isNotPopulated() {
+ return mEmailAddress == null;
+ }
+ }
}
\ No newline at end of file
diff --git a/src/com/android/mail/browse/SendersView.java b/src/com/android/mail/browse/SendersView.java
index 3f05ab5..734a59a 100644
--- a/src/com/android/mail/browse/SendersView.java
+++ b/src/com/android/mail/browse/SendersView.java
@@ -36,15 +36,19 @@
import com.android.mail.providers.ConversationInfo;
import com.android.mail.providers.ParticipantInfo;
import com.android.mail.providers.UIProvider;
-import com.android.mail.ui.DividedImageCanvas;
import com.android.mail.utils.ObjectCache;
import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
public class SendersView {
+ /** The maximum number of senders to display for a given conversation */
+ private static final int MAX_SENDER_COUNT = 4;
+
private static final Integer DOES_NOT_EXIST = -5;
// FIXME(ath): make all of these statics instance variables, and have callers hold onto this
// instance as long as appropriate (e.g. activity lifetime).
@@ -220,12 +224,13 @@
public static void format(Context context, ConversationInfo conversationInfo,
String messageInfo, int maxChars, ArrayList<SpannableString> styledSenders,
- ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails,
+ ArrayList<String> displayableSenderNames,
+ ConversationItemViewModel.SenderAvatarModel senderAvatarModel,
String account, final boolean showToHeader, final boolean resourceCachingRequired) {
try {
getSenderResources(context, resourceCachingRequired);
format(context, conversationInfo, messageInfo, maxChars, styledSenders,
- displayableSenderNames, displayableSenderEmails, account,
+ displayableSenderNames, senderAvatarModel, account,
sUnreadStyleSpan, sReadStyleSpan, showToHeader, resourceCachingRequired);
} finally {
if (!resourceCachingRequired) {
@@ -236,14 +241,15 @@
public static void format(Context context, ConversationInfo conversationInfo,
String messageInfo, int maxChars, ArrayList<SpannableString> styledSenders,
- ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails,
+ ArrayList<String> displayableSenderNames,
+ ConversationItemViewModel.SenderAvatarModel senderAvatarModel,
String account, final TextAppearanceSpan notificationUnreadStyleSpan,
final CharacterStyle notificationReadStyleSpan, final boolean showToHeader,
final boolean resourceCachingRequired) {
try {
getSenderResources(context, resourceCachingRequired);
handlePriority(maxChars, messageInfo, conversationInfo, styledSenders,
- displayableSenderNames, displayableSenderEmails, account,
+ displayableSenderNames, senderAvatarModel, account,
notificationUnreadStyleSpan, notificationReadStyleSpan, showToHeader);
} finally {
if (!resourceCachingRequired) {
@@ -254,10 +260,12 @@
private static void handlePriority(int maxChars, String messageInfoString,
ConversationInfo conversationInfo, ArrayList<SpannableString> styledSenders,
- ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails,
+ ArrayList<String> displayableSenderNames,
+ ConversationItemViewModel.SenderAvatarModel senderAvatarModel,
String account, final TextAppearanceSpan unreadStyleSpan,
final CharacterStyle readStyleSpan, final boolean showToHeader) {
- boolean shouldAddPhotos = displayableSenderEmails != null;
+ final boolean shouldSelectSenders = displayableSenderNames != null;
+ final boolean shouldSelectAvatar = senderAvatarModel != null;
int maxPriorityToInclude = -1; // inclusive
int numCharsUsed = messageInfoString.length(); // draft, number drafts,
// count
@@ -297,18 +305,15 @@
} finally {
PRIORITY_LENGTH_MAP_CACHE.release(priorityToLength);
}
- // We want to include this entry if
- // 1) The onlyShowUnread flags is not set
- // 2) The above flag is set, and the message is unread
- ParticipantInfo currentParticipant;
+
SpannableString spannableDisplay;
- CharacterStyle style;
boolean appendedElided = false;
- Map<String, Integer> displayHash = Maps.newHashMap();
- String firstDisplayableSenderEmail = null;
- String firstDisplayableSender = null;
+ final Map<String, Integer> displayHash = Maps.newHashMap();
+ final List<String> senderEmails = Lists.newArrayListWithExpectedSize(MAX_SENDER_COUNT);
+ String firstSenderEmail = null;
+ String firstSenderName = null;
for (int i = 0; i < conversationInfo.participantInfos.size(); i++) {
- currentParticipant = conversationInfo.participantInfos.get(i);
+ final ParticipantInfo currentParticipant = conversationInfo.participantInfos.get(i);
final String currentEmail = currentParticipant.email;
final String currentName = currentParticipant.name;
@@ -323,8 +328,8 @@
}
final int priority = currentParticipant.priority;
- style = CharacterStyle.wrap(currentParticipant.readConversation ? readStyleSpan :
- unreadStyleSpan);
+ final CharacterStyle style = CharacterStyle.wrap(currentParticipant.readConversation ?
+ readStyleSpan : unreadStyleSpan);
if (priority <= maxPriorityToInclude) {
spannableDisplay = new SpannableString(sBidiFormatter.unicodeWrap(nameString));
// Don't duplicate senders; leave the first instance, unless the
@@ -340,8 +345,8 @@
&& oldPos < styledSenders.size()) {
// Remove the old one!
styledSenders.set(oldPos, null);
- if (shouldAddPhotos && !TextUtils.isEmpty(currentEmail)) {
- displayableSenderEmails.remove(currentEmail);
+ if (shouldSelectSenders && !TextUtils.isEmpty(currentEmail)) {
+ senderEmails.remove(currentEmail);
displayableSenderNames.remove(currentName);
}
}
@@ -357,38 +362,67 @@
styledSenders.add(spannableDisplay);
}
}
- if (shouldAddPhotos) {
- String senderEmail = TextUtils.isEmpty(currentName) ?
- account :
- TextUtils.isEmpty(currentEmail) ? currentName : currentEmail;
+
+ final String senderEmail = TextUtils.isEmpty(currentName) ? account :
+ TextUtils.isEmpty(currentEmail) ? currentName : currentEmail;
+
+ if (shouldSelectSenders) {
if (i == 0) {
// Always add the first sender!
- firstDisplayableSenderEmail = senderEmail;
- firstDisplayableSender = currentName;
+ firstSenderEmail = senderEmail;
+ firstSenderName = currentName;
} else {
- if (!Objects.equal(firstDisplayableSenderEmail, senderEmail)) {
- int indexOf = displayableSenderEmails.indexOf(senderEmail);
+ if (!Objects.equal(firstSenderEmail, senderEmail)) {
+ int indexOf = senderEmails.indexOf(senderEmail);
if (indexOf > -1) {
- displayableSenderEmails.remove(indexOf);
+ senderEmails.remove(indexOf);
displayableSenderNames.remove(indexOf);
}
- displayableSenderEmails.add(senderEmail);
+ senderEmails.add(senderEmail);
displayableSenderNames.add(currentName);
- if (displayableSenderEmails.size() > DividedImageCanvas.MAX_DIVISIONS) {
- displayableSenderEmails.remove(0);
+ if (senderEmails.size() > MAX_SENDER_COUNT) {
+ senderEmails.remove(0);
displayableSenderNames.remove(0);
}
}
}
}
+
+ // if the corresponding message from this participant is unread and no sender avatar
+ // is yet chosen, choose this one
+ if (shouldSelectAvatar && senderAvatarModel.isNotPopulated() &&
+ !currentParticipant.readConversation) {
+ senderAvatarModel.populate(currentName, senderEmail);
+ }
}
- if (shouldAddPhotos && !TextUtils.isEmpty(firstDisplayableSenderEmail)) {
- if (displayableSenderEmails.size() < DividedImageCanvas.MAX_DIVISIONS) {
- displayableSenderEmails.add(0, firstDisplayableSenderEmail);
- displayableSenderNames.add(0, firstDisplayableSender);
+
+ // always add the first sender to the display
+ if (shouldSelectSenders && !TextUtils.isEmpty(firstSenderEmail)) {
+ if (displayableSenderNames.size() < MAX_SENDER_COUNT) {
+ displayableSenderNames.add(0, firstSenderName);
} else {
- displayableSenderEmails.set(0, firstDisplayableSenderEmail);
- displayableSenderNames.set(0, firstDisplayableSender);
+ displayableSenderNames.set(0, firstSenderName);
+ }
+ }
+
+ // if all messages in the thread were read, we must search for an appropriate avatar
+ if (shouldSelectAvatar && senderAvatarModel.isNotPopulated() &&
+ !conversationInfo.participantInfos.isEmpty()) {
+
+ // search for the last sender that is not the current account
+ for (int i = conversationInfo.participantInfos.size() - 1; i >= 0; i--) {
+ final ParticipantInfo participant = conversationInfo.participantInfos.get(i);
+ if (!TextUtils.isEmpty(participant.name)) {
+ senderAvatarModel.populate(participant.name, participant.email);
+ break;
+ }
+ }
+
+ // if we still don't have an avatar, the account is emailing itself; use the last sender
+ if (senderAvatarModel.isNotPopulated()) {
+ final int lastIndex = conversationInfo.participantInfos.size() - 1;
+ final ParticipantInfo lastSender = conversationInfo.participantInfos.get(lastIndex);
+ senderAvatarModel.populate(lastSender.name, lastSender.email);
}
}
}
diff --git a/src/com/android/mail/browse/SwipeableConversationItemView.java b/src/com/android/mail/browse/SwipeableConversationItemView.java
index a08c028..866653a 100644
--- a/src/com/android/mail/browse/SwipeableConversationItemView.java
+++ b/src/com/android/mail/browse/SwipeableConversationItemView.java
@@ -23,6 +23,7 @@
import android.widget.FrameLayout;
import android.widget.ListView;
+import com.android.mail.providers.Account;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
import com.android.mail.providers.UIProvider;
@@ -34,7 +35,7 @@
private final ConversationItemView mConversationItemView;
- public SwipeableConversationItemView(Context context, String account) {
+ public SwipeableConversationItemView(Context context, Account account) {
super(context);
mConversationItemView = new ConversationItemView(context, account);
addView(mConversationItemView);
diff --git a/src/com/android/mail/ui/AnimatedAdapter.java b/src/com/android/mail/ui/AnimatedAdapter.java
index 30aa36c..b2fec88 100644
--- a/src/com/android/mail/ui/AnimatedAdapter.java
+++ b/src/com/android/mail/ui/AnimatedAdapter.java
@@ -371,7 +371,7 @@
public View createConversationItemView(SwipeableConversationItemView view, Context context,
Conversation conv) {
if (view == null) {
- view = new SwipeableConversationItemView(context, mAccount.getEmailAddress());
+ view = new SwipeableConversationItemView(context, mAccount);
}
view.bind(conv, mActivity, mBatchConversations, mFolder, getCheckboxSetting(),
mSwipeEnabled, mImportanceMarkersEnabled, mShowChevronsEnabled, this);
@@ -764,7 +764,7 @@
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
- return new SwipeableConversationItemView(context, mAccount.getEmailAddress());
+ return new SwipeableConversationItemView(context, mAccount);
}
@Override
diff --git a/src/com/android/mail/ui/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java
index a70eecd..af75205 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -132,6 +132,9 @@
*/
private final int LOAD_WAIT_UNTIL_VISIBLE = 2;
+ // Default scroll distance when the user tries to scroll with up/down
+ private final int DEFAULT_VERTICAL_SCROLL_DISTANCE_PX = 50;
+
// Keyboard navigation
private KeyboardNavigationController mNavigationController;
// Since we manually control navigation for most of the conversation view due to problems
@@ -1212,24 +1215,25 @@
// We manually handle up/down navigation through the overlay items because the
// system's default isn't optimal for two-pane landscape since it's not a real list.
- final View next = mConversationContainer.getNextOverlayView(
- mOriginalKeyedView, isDown);
+ final View next = mConversationContainer.getNextOverlayView(mOriginalKeyedView,
+ isDown);
if (next != null) {
- if (isActionUp) {
- // Make sure that v is in view
- final int[] coords = new int[2];
- next.getLocationOnScreen(coords);
- final int bottom = coords[1] + next.getHeight();
- if (bottom > mMaxScreenHeight) {
- mWebView.scrollBy(0, bottom - mMaxScreenHeight);
- } else if (coords[1] < mTopOfVisibleScreen) {
- mWebView.scrollBy(0, coords[1] - mTopOfVisibleScreen);
+ focusAndScrollToView(next);
+ } else if (!isActionUp) {
+ // Scroll in the direction of the arrow if next view isn't found.
+ final int currentY = mWebView.getScrollY();
+ if (isUp && currentY > 0) {
+ mWebView.scrollBy(0,
+ -Math.min(currentY, DEFAULT_VERTICAL_SCROLL_DISTANCE_PX));
+ } else if (isDown) {
+ final int webviewEnd = (int) (mWebView.getContentHeight() *
+ mWebView.getScale());
+ final int currentEnd = currentY + mWebView.getHeight();
+ if (currentEnd < webviewEnd) {
+ mWebView.scrollBy(0, Math.min(webviewEnd - currentEnd,
+ DEFAULT_VERTICAL_SCROLL_DISTANCE_PX));
}
- next.requestFocus();
}
- } else {
- // If the next view cannot be reached, let's scroll in the direction of the key.
- mTopmostOverlay.requestFocus();
}
return true;
}
@@ -1252,6 +1256,19 @@
return false;
}
+ private void focusAndScrollToView(View v) {
+ // Make sure that v is in view
+ final int[] coords = new int[2];
+ v.getLocationOnScreen(coords);
+ final int bottom = coords[1] + v.getHeight();
+ if (bottom > mMaxScreenHeight) {
+ mWebView.scrollBy(0, bottom - mMaxScreenHeight);
+ } else if (coords[1] < mTopOfVisibleScreen) {
+ mWebView.scrollBy(0, coords[1] - mTopOfVisibleScreen);
+ }
+ v.requestFocus();
+ }
+
public class ConversationWebViewClient extends AbstractConversationWebViewClient {
public ConversationWebViewClient(Account account) {
super(account);
diff --git a/src/com/android/mail/ui/DividedImageCanvas.java b/src/com/android/mail/ui/DividedImageCanvas.java
deleted file mode 100644
index 274327e..0000000
--- a/src/com/android/mail/ui/DividedImageCanvas.java
+++ /dev/null
@@ -1,463 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.mail.ui;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Matrix;
-import android.graphics.Paint;
-import android.graphics.PorterDuff.Mode;
-import android.graphics.PorterDuffXfermode;
-import android.graphics.Rect;
-
-import com.android.mail.R;
-import com.android.mail.utils.Utils;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-/**
- * DividedImageCanvas creates a canvas that can display into a minimum of 1
- * and maximum of 4 images. As images are added, they
- * are laid out according to the following algorithm:
- * 1 Image: Draw the bitmap filling the entire canvas.
- * 2 Images: Draw 2 bitmaps split vertically down the middle.
- * 3 Images: Draw 3 bitmaps: the first takes up all vertical space; the 2nd and 3rd are stacked in
- * the second vertical position.
- * 4 Images: Divide the Canvas into 4 equal quadrants and draws 1 bitmap in each.
- */
-public class DividedImageCanvas implements ImageCanvas {
- public static final int MAX_DIVISIONS = 4;
-
- private final Map<String, Integer> mDivisionMap = Maps
- .newHashMapWithExpectedSize(MAX_DIVISIONS);
- private Bitmap mDividedBitmap;
- private Canvas mCanvas;
- private int mWidth;
- private int mHeight;
-
- private final Context mContext;
- private final InvalidateCallback mCallback;
- private final ArrayList<Bitmap> mDivisionImages = new ArrayList<Bitmap>(MAX_DIVISIONS);
-
- /**
- * Ignore any request to draw final output when not yet ready. This prevents partially drawn
- * canvases from appearing.
- */
- private boolean mBitmapValid = false;
-
- private int mGeneration;
-
- private static final Paint sPaint = new Paint();
- private static final Paint sClearPaint = new Paint();
- private static final Rect sSrc = new Rect();
- private static final Rect sDest = new Rect();
-
- private static int sDividerLineWidth = -1;
- private static int sDividerColor;
-
- static {
- sClearPaint.setXfermode(new PorterDuffXfermode(Mode.CLEAR));
- }
-
- public DividedImageCanvas(Context context, InvalidateCallback callback) {
- mContext = context;
- mCallback = callback;
- setupDividerLines();
- }
-
- /**
- * Get application context for this object.
- */
- public Context getContext() {
- return mContext;
- }
-
- @Override
- public String toString() {
- final StringBuilder sb = new StringBuilder("{");
- sb.append(super.toString());
- sb.append(" mDivisionMap=");
- sb.append(mDivisionMap);
- sb.append(" mDivisionImages=");
- sb.append(mDivisionImages);
- sb.append(" mWidth=");
- sb.append(mWidth);
- sb.append(" mHeight=");
- sb.append(mHeight);
- sb.append("}");
- return sb.toString();
- }
-
- /**
- * Set the id associated with each quadrant. The quadrants are laid out:
- * TopLeft, TopRight, Bottom Left, Bottom Right
- * @param keys
- */
- public void setDivisionIds(List<Object> keys) {
- if (keys.size() > MAX_DIVISIONS) {
- throw new IllegalArgumentException("too many divisionIds: " + keys);
- }
-
- boolean needClear = getDivisionCount() != keys.size();
- if (!needClear) {
- for (int i = 0; i < keys.size(); i++) {
- String divisionId = transformKeyToDivisionId(keys.get(i));
- // different item or different place
- if (!mDivisionMap.containsKey(divisionId) || mDivisionMap.get(divisionId) != i) {
- needClear = true;
- break;
- }
- }
- }
-
- if (needClear) {
- mDivisionMap.clear();
- mDivisionImages.clear();
- int i = 0;
- for (Object key : keys) {
- String divisionId = transformKeyToDivisionId(key);
- mDivisionMap.put(divisionId, i);
- mDivisionImages.add(null);
- i++;
- }
- }
- }
-
- private void draw(Bitmap b, int left, int top, int right, int bottom) {
- if (b != null) {
- // Some times we load taller images compared to the destination rect on the canvas
- int srcTop = 0;
- int srcBottom = b.getHeight();
- int destHeight = bottom - top;
- if (b.getHeight() > bottom - top) {
- srcTop = b.getHeight() / 2 - destHeight/2;
- srcBottom = b.getHeight() / 2 + destHeight/2;
- }
-
-// todo:markwei do not scale very small bitmaps
- // l t r b
- sSrc.set(0, srcTop, b.getWidth(), srcBottom);
- sDest.set(left, top, right, bottom);
- mCanvas.drawRect(sDest, sClearPaint);
- mCanvas.drawBitmap(b, sSrc, sDest, sPaint);
- } else {
- // clear
- mCanvas.drawRect(left, top, right, bottom, sClearPaint);
- }
- }
-
- /**
- * Get the desired dimensions and scale for the bitmap to be placed in the
- * location corresponding to id. Caller must allocate the Dimensions object.
- * @param key
- * @param outDim a {@link ImageCanvas.Dimensions} object to write results into
- */
- @Override
- public void getDesiredDimensions(Object key, Dimensions outDim) {
- Utils.traceBeginSection("get desired dimensions");
- int w = 0, h = 0;
- float scale = 0;
- final Integer pos = mDivisionMap.get(transformKeyToDivisionId(key));
- if (pos != null && pos >= 0) {
- final int size = mDivisionMap.size();
- switch (size) {
- case 0:
- break;
- case 1:
- w = mWidth;
- h = mHeight;
- scale = Dimensions.SCALE_ONE;
- break;
- case 2:
- w = mWidth / 2;
- h = mHeight;
- scale = Dimensions.SCALE_HALF;
- break;
- case 3:
- switch (pos) {
- case 0:
- w = mWidth / 2;
- h = mHeight;
- scale = Dimensions.SCALE_HALF;
- break;
- default:
- w = mWidth / 2;
- h = mHeight / 2;
- scale = Dimensions.SCALE_QUARTER;
- }
- break;
- case 4:
- w = mWidth / 2;
- h = mHeight / 2;
- scale = Dimensions.SCALE_QUARTER;
- break;
- }
- }
- outDim.width = w;
- outDim.height = h;
- outDim.scale = scale;
- Utils.traceEndSection();
- }
-
- @Override
- public void drawImage(Bitmap b, Object key) {
- addDivisionImage(b, key);
- }
-
- /**
- * Add a bitmap to this view in the quadrant matching its id.
- * @param b Bitmap
- * @param key Id to look for that was previously set in setDivisionIds.
- */
- public void addDivisionImage(Bitmap b, Object key) {
- if (b != null) {
- addOrClearDivisionImage(b, key);
- }
- }
-
- public void clearDivisionImage(Object key) {
- addOrClearDivisionImage(null, key);
- }
- private void addOrClearDivisionImage(Bitmap b, Object key) {
- Utils.traceBeginSection("add or clear division image");
- final Integer pos = mDivisionMap.get(transformKeyToDivisionId(key));
- if (pos != null && pos >= 0) {
- mDivisionImages.set(pos, b);
- boolean complete = false;
- final int width = mWidth;
- final int height = mHeight;
- // Different layouts depending on count.
- final int size = mDivisionMap.size();
- switch (size) {
- case 0:
- // Do nothing.
- break;
- case 1:
- // Draw the bitmap filling the entire canvas.
- draw(mDivisionImages.get(0), 0, 0, width, height);
- complete = true;
- break;
- case 2:
- // Draw 2 bitmaps split vertically down the middle
- switch (pos) {
- case 0:
- draw(mDivisionImages.get(0), 0, 0, width / 2, height);
- break;
- case 1:
- draw(mDivisionImages.get(1), width / 2, 0, width, height);
- break;
- }
- complete = mDivisionImages.get(0) != null && mDivisionImages.get(1) != null
- || isPartialBitmapComplete();
- if (complete) {
- // Draw dividers
- drawVerticalDivider(width, height);
- }
- break;
- case 3:
- // Draw 3 bitmaps: the first takes up all vertical
- // space, the 2nd and 3rd are stacked in the second vertical
- // position.
- switch (pos) {
- case 0:
- draw(mDivisionImages.get(0), 0, 0, width / 2, height);
- break;
- case 1:
- draw(mDivisionImages.get(1), width / 2, 0, width, height / 2);
- break;
- case 2:
- draw(mDivisionImages.get(2), width / 2, height / 2, width, height);
- break;
- }
- complete = mDivisionImages.get(0) != null && mDivisionImages.get(1) != null
- && mDivisionImages.get(2) != null || isPartialBitmapComplete();
- if (complete) {
- // Draw dividers
- drawVerticalDivider(width, height);
- drawHorizontalDivider(width / 2, height / 2, width, height / 2);
- }
- break;
- default:
- // Draw all 4 bitmaps in a grid
- switch (pos) {
- case 0:
- draw(mDivisionImages.get(0), 0, 0, width / 2, height / 2);
- break;
- case 1:
- draw(mDivisionImages.get(1), width / 2, 0, width, height / 2);
- break;
- case 2:
- draw(mDivisionImages.get(2), 0, height / 2, width / 2, height);
- break;
- case 3:
- draw(mDivisionImages.get(3), width / 2, height / 2, width, height);
- break;
- }
- complete = mDivisionImages.get(0) != null && mDivisionImages.get(1) != null
- && mDivisionImages.get(2) != null && mDivisionImages.get(3) != null
- || isPartialBitmapComplete();
- if (complete) {
- // Draw dividers
- drawVerticalDivider(width, height);
- drawHorizontalDivider(0, height / 2, width, height / 2);
- }
- break;
- }
- // Create the new image bitmap.
- if (complete) {
- mBitmapValid = true;
- mCallback.invalidate();
- }
- }
- Utils.traceEndSection();
- }
-
- public boolean hasImageFor(Object key) {
- final Integer pos = mDivisionMap.get(transformKeyToDivisionId(key));
- return pos != null && mDivisionImages.get(pos) != null;
- }
-
- private void setupDividerLines() {
- if (sDividerLineWidth == -1) {
- Resources res = getContext().getResources();
- sDividerLineWidth = res
- .getDimensionPixelSize(R.dimen.tile_divider_width);
- sDividerColor = res.getColor(R.color.tile_divider_color);
- }
- }
-
- private static void setupPaint() {
- sPaint.setStrokeWidth(sDividerLineWidth);
- sPaint.setColor(sDividerColor);
- }
-
- protected void drawVerticalDivider(int width, int height) {
- int x1 = width / 2, y1 = 0, x2 = width/2, y2 = height;
- setupPaint();
- mCanvas.drawLine(x1, y1, x2, y2, sPaint);
- }
-
- protected void drawHorizontalDivider(int x1, int y1, int x2, int y2) {
- setupPaint();
- mCanvas.drawLine(x1, y1, x2, y2, sPaint);
- }
-
- protected boolean isPartialBitmapComplete() {
- return false;
- }
-
- protected String transformKeyToDivisionId(Object key) {
- return key.toString();
- }
-
- /**
- * Draw the contents of the DividedImageCanvas to the supplied canvas.
- */
- public void draw(Canvas canvas) {
- if (mDividedBitmap != null && mBitmapValid) {
- canvas.drawBitmap(mDividedBitmap, 0, 0, null);
- }
- }
-
- /**
- * Draw the contents of the DividedImageCanvas to the supplied canvas.
- */
- public void draw(final Canvas canvas, final Matrix matrix) {
- if (mDividedBitmap != null && mBitmapValid) {
- canvas.drawBitmap(mDividedBitmap, matrix, null);
- }
- }
-
- @Override
- public void reset() {
- if (mCanvas != null && mDividedBitmap != null) {
- mBitmapValid = false;
- }
- mDivisionMap.clear();
- mDivisionImages.clear();
- mGeneration++;
- }
-
- @Override
- public int getGeneration() {
- return mGeneration;
- }
-
- /**
- * Set the width and height of the canvas.
- * @param width
- * @param height
- */
- public void setDimensions(int width, int height) {
- Utils.traceBeginSection("set dimensions");
- if (mWidth == width && mHeight == height) {
- Utils.traceEndSection();
- return;
- }
-
- mWidth = width;
- mHeight = height;
-
- mDividedBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- mCanvas = new Canvas(mDividedBitmap);
-
- for (int i = 0; i < getDivisionCount(); i++) {
- mDivisionImages.set(i, null);
- }
- mBitmapValid = false;
- Utils.traceEndSection();
- }
-
- /**
- * Get the resulting canvas width.
- */
- public int getWidth() {
- return mWidth;
- }
-
- /**
- * Get the resulting canvas height.
- */
- public int getHeight() {
- return mHeight;
- }
-
- /**
- * The class that will provided the canvas to which the DividedImageCanvas
- * should render its contents must implement this interface.
- */
- public interface InvalidateCallback {
- public void invalidate();
- }
-
- public int getDivisionCount() {
- return mDivisionMap.size();
- }
-
- /**
- * Get the division ids currently associated with this DivisionImageCanvas.
- */
- public ArrayList<String> getDivisionIds() {
- return Lists.newArrayList(mDivisionMap.keySet());
- }
-}
diff --git a/tests/src/com/android/mail/browse/SendersFormattingTests.java b/tests/src/com/android/mail/browse/SendersFormattingTests.java
index ecec4df..c55e03b 100644
--- a/tests/src/com/android/mail/browse/SendersFormattingTests.java
+++ b/tests/src/com/android/mail/browse/SendersFormattingTests.java
@@ -112,4 +112,98 @@
assertEquals(before.firstUnreadSnippet, after.firstUnreadSnippet);
assertEquals(before.lastSnippet, after.lastSnippet);
}
+
+ public void testSenderAvatarIsSenderOfFirstUnreadMessage() {
+ final ConversationInfo conv = createConversationInfo();
+ conv.addParticipant(new ParticipantInfo("a", "a@a.com", 0, true));
+ conv.addParticipant(new ParticipantInfo("b", "b@b.com", 0, false));
+ conv.addParticipant(new ParticipantInfo("c", "c@c.com", 0, false));
+
+ final ArrayList<SpannableString> styledSenders = Lists.newArrayList();
+ final ArrayList<String> displayableSenderNames = Lists.newArrayList();
+ final ConversationItemViewModel.SenderAvatarModel senderAvatarModel =
+ new ConversationItemViewModel.SenderAvatarModel();
+
+ SendersView.format(getContext(), conv, "", 100, styledSenders, displayableSenderNames,
+ senderAvatarModel, null, false, false);
+
+ assertEquals("b@b.com", senderAvatarModel.getEmailAddress());
+ assertEquals("b", senderAvatarModel.getName());
+ }
+
+ public void testSenderAvatarIsLastSenderIfAllMessagesAreRead() {
+ final ConversationInfo conv = createConversationInfo();
+ conv.addParticipant(new ParticipantInfo("a", "a@a.com", 0, true));
+ conv.addParticipant(new ParticipantInfo("b", "b@b.com", 0, true));
+ conv.addParticipant(new ParticipantInfo("c", "c@c.com", 0, true));
+
+ final ArrayList<SpannableString> styledSenders = Lists.newArrayList();
+ final ArrayList<String> displayableSenderNames = Lists.newArrayList();
+ final ConversationItemViewModel.SenderAvatarModel senderAvatarModel =
+ new ConversationItemViewModel.SenderAvatarModel();
+
+ SendersView.format(getContext(), conv, "", 100, styledSenders, displayableSenderNames,
+ senderAvatarModel, null, false, false);
+
+ assertEquals("c@c.com", senderAvatarModel.getEmailAddress());
+ assertEquals("c", senderAvatarModel.getName());
+ }
+
+ public void testSenderAvatarIsLastSenderThatIsNotTheCurrentAccountIfAllMessagesAreRead() {
+ final ConversationInfo conv = createConversationInfo();
+ conv.addParticipant(new ParticipantInfo("a", "a@a.com", 0, true));
+ conv.addParticipant(new ParticipantInfo("b", "b@b.com", 0, true));
+ // empty name indicates it is the current account
+ conv.addParticipant(new ParticipantInfo("", "c@c.com", 0, true));
+
+ final ArrayList<SpannableString> styledSenders = Lists.newArrayList();
+ final ArrayList<String> displayableSenderNames = Lists.newArrayList();
+ final ConversationItemViewModel.SenderAvatarModel senderAvatarModel =
+ new ConversationItemViewModel.SenderAvatarModel();
+
+ SendersView.format(getContext(), conv, "", 100, styledSenders, displayableSenderNames,
+ senderAvatarModel, null, false, false);
+
+ assertEquals("b@b.com", senderAvatarModel.getEmailAddress());
+ assertEquals("b", senderAvatarModel.getName());
+ }
+
+ public void testSenderAvatarIsCurrentAccountIfAllSendersAreCurrentAccount() {
+ final ConversationInfo conv = createConversationInfo();
+ // empty name indicates it is the current account
+ conv.addParticipant(new ParticipantInfo("", "a@a.com", 0, true));
+
+ final ArrayList<SpannableString> styledSenders = Lists.newArrayList();
+ final ArrayList<String> displayableSenderNames = Lists.newArrayList();
+ final ConversationItemViewModel.SenderAvatarModel senderAvatarModel =
+ new ConversationItemViewModel.SenderAvatarModel();
+
+ SendersView.format(getContext(), conv, "", 100, styledSenders, displayableSenderNames,
+ senderAvatarModel, null, false, false);
+
+ assertEquals("a@a.com", senderAvatarModel.getEmailAddress());
+ assertEquals("", senderAvatarModel.getName());
+ }
+
+ /**
+ * Two senders in a thread should be kept distinct if they have unique email addresses, even if
+ * they happen to share the same name.
+ */
+ public void testSenderNamesWhenNamesMatchButEmailAddressesDiffer() {
+ final ConversationInfo conv = createConversationInfo();
+ conv.addParticipant(new ParticipantInfo("Andrew", "aholmes@awesome.com", 0, true));
+ conv.addParticipant(new ParticipantInfo("Andrew", "ajohnson@wicked.com", 0, true));
+
+ final ArrayList<SpannableString> styledSenders = Lists.newArrayList();
+ final ArrayList<String> displayableSenderNames = Lists.newArrayList();
+ final ConversationItemViewModel.SenderAvatarModel senderAvatarModel =
+ new ConversationItemViewModel.SenderAvatarModel();
+
+ SendersView.format(getContext(), conv, "", 100, styledSenders, displayableSenderNames,
+ senderAvatarModel, null, false, false);
+
+ assertEquals(2, displayableSenderNames.size());
+ assertEquals("Andrew", displayableSenderNames.get(0));
+ assertEquals("Andrew", displayableSenderNames.get(1));
+ }
}
\ No newline at end of file