blob: 414d9c29d1b772f1b999950c50b49ed4152e1790 [file] [log] [blame]
/*
* Copyright (C) 2012 Google Inc.
* Licensed to 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.browse;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.support.v4.text.BidiFormatter;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.CharacterStyle;
import android.text.style.TextAppearanceSpan;
import com.android.emailcommon.mail.Address;
import com.android.mail.R;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.ConversationInfo;
import com.android.mail.providers.MessageInfo;
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.Maps;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
public class SendersView {
public static final int DEFAULT_FORMATTING = 0;
public static final int MERGED_FORMATTING = 1;
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).
// no need to listen for configuration changes.
private static String sSendersSplitToken;
public static String SENDERS_VERSION_SEPARATOR = "^**^";
public static Pattern SENDERS_VERSION_SEPARATOR_PATTERN = Pattern.compile("\\^\\*\\*\\^");
private static CharSequence sDraftSingularString;
private static CharSequence sDraftPluralString;
private static CharSequence sSendingString;
private static String sDraftCountFormatString;
private static CharacterStyle sDraftsStyleSpan;
private static CharacterStyle sSendingStyleSpan;
private static TextAppearanceSpan sUnreadStyleSpan;
private static CharacterStyle sReadStyleSpan;
private static String sMeString;
private static Locale sMeStringLocale;
private static String sMessageCountSpacerString;
public static CharSequence sElidedString;
private static BroadcastReceiver sConfigurationChangedReceiver;
private static TextAppearanceSpan sMessageInfoReadStyleSpan;
private static TextAppearanceSpan sMessageInfoUnreadStyleSpan;
private static BidiFormatter sBidiFormatter;
// We only want to have at most 2 Priority to length maps. This will handle the case where
// there is a widget installed on the launcher while the user is scrolling in the app
private static final int MAX_PRIORITY_LENGTH_MAP_LIST = 2;
// Cache of priority to length maps. We can't just use a single instance as it may be
// modified from different threads
private static final ObjectCache<Map<Integer, Integer>> PRIORITY_LENGTH_MAP_CACHE =
new ObjectCache<Map<Integer, Integer>>(
new ObjectCache.Callback<Map<Integer, Integer>>() {
@Override
public Map<Integer, Integer> newInstance() {
return Maps.newHashMap();
}
@Override
public void onObjectReleased(Map<Integer, Integer> object) {
object.clear();
}
}, MAX_PRIORITY_LENGTH_MAP_LIST);
public static Typeface getTypeface(boolean isUnread) {
return isUnread ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT;
}
private static synchronized void getSenderResources(
Context context, final boolean resourceCachingRequired) {
if (sConfigurationChangedReceiver == null && resourceCachingRequired) {
sConfigurationChangedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
sDraftSingularString = null;
getSenderResources(context, true);
}
};
context.registerReceiver(sConfigurationChangedReceiver, new IntentFilter(
Intent.ACTION_CONFIGURATION_CHANGED));
}
if (sDraftSingularString == null) {
Resources res = context.getResources();
sSendersSplitToken = res.getString(R.string.senders_split_token);
sElidedString = res.getString(R.string.senders_elided);
sDraftSingularString = res.getQuantityText(R.plurals.draft, 1);
sDraftPluralString = res.getQuantityText(R.plurals.draft, 2);
sDraftCountFormatString = res.getString(R.string.draft_count_format);
sMessageInfoUnreadStyleSpan = new TextAppearanceSpan(context,
R.style.MessageInfoUnreadTextAppearance);
sMessageInfoReadStyleSpan = new TextAppearanceSpan(context,
R.style.MessageInfoReadTextAppearance);
sDraftsStyleSpan = new TextAppearanceSpan(context, R.style.DraftTextAppearance);
sUnreadStyleSpan = new TextAppearanceSpan(context, R.style.SendersUnreadTextAppearance);
sSendingStyleSpan = new TextAppearanceSpan(context, R.style.SendingTextAppearance);
sReadStyleSpan = new TextAppearanceSpan(context, R.style.SendersReadTextAppearance);
sMessageCountSpacerString = res.getString(R.string.message_count_spacer);
sSendingString = res.getString(R.string.sending);
sBidiFormatter = BidiFormatter.getInstance();
}
}
public static SpannableStringBuilder createMessageInfo(Context context, Conversation conv,
final boolean resourceCachingRequired) {
SpannableStringBuilder messageInfo = new SpannableStringBuilder();
try {
ConversationInfo conversationInfo = conv.conversationInfo;
int sendingStatus = conv.sendingState;
boolean hasSenders = false;
// This covers the case where the sender is "me" and this is a draft
// message, which means this will only run once most of the time.
for (MessageInfo m : conversationInfo.messageInfos) {
if (!TextUtils.isEmpty(m.sender)) {
hasSenders = true;
break;
}
}
getSenderResources(context, resourceCachingRequired);
int count = conversationInfo.messageCount;
int draftCount = conversationInfo.draftCount;
boolean showSending = sendingStatus == UIProvider.ConversationSendingState.SENDING;
if (count > 1) {
messageInfo.append(count + "");
}
messageInfo.setSpan(CharacterStyle.wrap(
conv.read ? sMessageInfoReadStyleSpan : sMessageInfoUnreadStyleSpan),
0, messageInfo.length(), 0);
if (draftCount > 0) {
// If we are showing a message count or any draft text and there
// is at least 1 sender, prepend the sending state text with a
// comma.
if (hasSenders || count > 1) {
messageInfo.append(sSendersSplitToken);
}
SpannableStringBuilder draftString = new SpannableStringBuilder();
if (draftCount == 1) {
draftString.append(sDraftSingularString);
} else {
draftString.append(sDraftPluralString
+ String.format(sDraftCountFormatString, draftCount));
}
draftString.setSpan(CharacterStyle.wrap(sDraftsStyleSpan), 0,
draftString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
messageInfo.append(draftString);
}
if (showSending) {
// If we are showing a message count or any draft text, prepend
// the sending state text with a comma.
if (count > 1 || draftCount > 0) {
messageInfo.append(sSendersSplitToken);
}
SpannableStringBuilder sending = new SpannableStringBuilder();
sending.append(sSendingString);
sending.setSpan(sSendingStyleSpan, 0, sending.length(), 0);
messageInfo.append(sending);
}
// Prepend a space if we are showing other message info text.
if (count > 1 || (draftCount > 0 && hasSenders) || showSending) {
messageInfo.insert(0, sMessageCountSpacerString);
}
} finally {
if (!resourceCachingRequired) {
clearResourceCache();
}
}
return messageInfo;
}
public static void format(Context context, ConversationInfo conversationInfo,
String messageInfo, int maxChars, ArrayList<SpannableString> styledSenders,
ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails,
String account, final boolean resourceCachingRequired) {
try {
getSenderResources(context, resourceCachingRequired);
format(context, conversationInfo, messageInfo, maxChars, styledSenders,
displayableSenderNames, displayableSenderEmails, account,
sUnreadStyleSpan, sReadStyleSpan, resourceCachingRequired);
} finally {
if (!resourceCachingRequired) {
clearResourceCache();
}
}
}
public static void format(Context context, ConversationInfo conversationInfo,
String messageInfo, int maxChars, ArrayList<SpannableString> styledSenders,
ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails,
String account, final TextAppearanceSpan notificationUnreadStyleSpan,
final CharacterStyle notificationReadStyleSpan, final boolean resourceCachingRequired) {
try {
getSenderResources(context, resourceCachingRequired);
handlePriority(context, maxChars, messageInfo, conversationInfo, styledSenders,
displayableSenderNames, displayableSenderEmails, account,
notificationUnreadStyleSpan, notificationReadStyleSpan);
} finally {
if (!resourceCachingRequired) {
clearResourceCache();
}
}
}
public static void handlePriority(Context context, int maxChars, String messageInfoString,
ConversationInfo conversationInfo, ArrayList<SpannableString> styledSenders,
ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails,
String account, final TextAppearanceSpan unreadStyleSpan,
final CharacterStyle readStyleSpan) {
boolean shouldAddPhotos = displayableSenderEmails != null;
int maxPriorityToInclude = -1; // inclusive
int numCharsUsed = messageInfoString.length(); // draft, number drafts,
// count
int numSendersUsed = 0;
int numCharsToRemovePerWord = 0;
int maxFoundPriority = 0;
if (numCharsUsed > maxChars) {
numCharsToRemovePerWord = numCharsUsed - maxChars;
}
final Map<Integer, Integer> priorityToLength = PRIORITY_LENGTH_MAP_CACHE.get();
try {
priorityToLength.clear();
int senderLength;
for (MessageInfo info : conversationInfo.messageInfos) {
senderLength = !TextUtils.isEmpty(info.sender) ? info.sender.length() : 0;
priorityToLength.put(info.priority, senderLength);
maxFoundPriority = Math.max(maxFoundPriority, info.priority);
}
while (maxPriorityToInclude < maxFoundPriority) {
if (priorityToLength.containsKey(maxPriorityToInclude + 1)) {
int length = numCharsUsed + priorityToLength.get(maxPriorityToInclude + 1);
if (numCharsUsed > 0)
length += 2;
// We must show at least two senders if they exist. If we don't
// have space for both
// then we will truncate names.
if (length > maxChars && numSendersUsed >= 2) {
break;
}
numCharsUsed = length;
numSendersUsed++;
}
maxPriorityToInclude++;
}
} 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
MessageInfo currentMessage;
SpannableString spannableDisplay;
String nameString;
CharacterStyle style;
boolean appendedElided = false;
Map<String, Integer> displayHash = Maps.newHashMap();
String firstDisplayableSenderEmail = null;
String firstDisplayableSender = null;
for (int i = 0; i < conversationInfo.messageInfos.size(); i++) {
currentMessage = conversationInfo.messageInfos.get(i);
nameString = !TextUtils.isEmpty(currentMessage.sender) ? currentMessage.sender : "";
if (nameString.length() == 0) {
nameString = getMe(context);
}
if (numCharsToRemovePerWord != 0) {
nameString = nameString.substring(0,
Math.max(nameString.length() - numCharsToRemovePerWord, 0));
}
final int priority = currentMessage.priority;
style = !currentMessage.read ? getWrappedStyleSpan(unreadStyleSpan)
: getWrappedStyleSpan(readStyleSpan);
if (priority <= maxPriorityToInclude) {
spannableDisplay = new SpannableString(sBidiFormatter.unicodeWrap(nameString));
// Don't duplicate senders; leave the first instance, unless the
// current instance is also unread.
int oldPos = displayHash.containsKey(currentMessage.sender) ? displayHash
.get(currentMessage.sender) : DOES_NOT_EXIST;
// If this sender doesn't exist OR the current message is
// unread, add the sender.
if (oldPos == DOES_NOT_EXIST || !currentMessage.read) {
// If the sender entry already existed, and is right next to the
// current sender, remove the old entry.
if (oldPos != DOES_NOT_EXIST && i > 0 && oldPos == i - 1
&& oldPos < styledSenders.size()) {
// Remove the old one!
styledSenders.set(oldPos, null);
if (shouldAddPhotos && !TextUtils.isEmpty(currentMessage.senderEmail)) {
displayableSenderEmails.remove(currentMessage.senderEmail);
displayableSenderNames.remove(currentMessage.sender);
}
}
displayHash.put(currentMessage.sender, i);
spannableDisplay.setSpan(style, 0, spannableDisplay.length(), 0);
styledSenders.add(spannableDisplay);
}
} else {
if (!appendedElided) {
spannableDisplay = new SpannableString(sElidedString);
spannableDisplay.setSpan(style, 0, spannableDisplay.length(), 0);
appendedElided = true;
styledSenders.add(spannableDisplay);
}
}
if (shouldAddPhotos) {
String senderEmail = TextUtils.isEmpty(currentMessage.sender) ?
account :
TextUtils.isEmpty(currentMessage.senderEmail) ?
currentMessage.sender : currentMessage.senderEmail;
if (i == 0) {
// Always add the first sender!
firstDisplayableSenderEmail = senderEmail;
firstDisplayableSender = currentMessage.sender;
} else {
if (!Objects.equal(firstDisplayableSenderEmail, senderEmail)) {
int indexOf = displayableSenderEmails.indexOf(senderEmail);
if (indexOf > -1) {
displayableSenderEmails.remove(indexOf);
displayableSenderNames.remove(indexOf);
}
displayableSenderEmails.add(senderEmail);
displayableSenderNames.add(currentMessage.sender);
if (displayableSenderEmails.size() > DividedImageCanvas.MAX_DIVISIONS) {
displayableSenderEmails.remove(0);
displayableSenderNames.remove(0);
}
}
}
}
}
if (shouldAddPhotos && !TextUtils.isEmpty(firstDisplayableSenderEmail)) {
if (displayableSenderEmails.size() < DividedImageCanvas.MAX_DIVISIONS) {
displayableSenderEmails.add(0, firstDisplayableSenderEmail);
displayableSenderNames.add(0, firstDisplayableSender);
} else {
displayableSenderEmails.set(0, firstDisplayableSenderEmail);
displayableSenderNames.set(0, firstDisplayableSender);
}
}
}
private static CharacterStyle getWrappedStyleSpan(final CharacterStyle characterStyle) {
return CharacterStyle.wrap(characterStyle);
}
static String getMe(Context context) {
final Resources resources = context.getResources();
final Locale locale = resources.getConfiguration().locale;
if (sMeString == null || !locale.equals(sMeStringLocale)) {
sMeString = resources.getString(R.string.me_subject_pronun);
sMeStringLocale = locale;
}
return sMeString;
}
private static void clearResourceCache() {
sDraftSingularString = null;
}
}