blob: a7a5533d473f5a285a05888ad7058e3c63343a5d [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.Context;
import android.graphics.Bitmap;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
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;
import com.android.mail.R;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
import com.android.mail.providers.MessageInfo;
import com.android.mail.providers.UIProvider;
import com.android.mail.utils.FolderUri;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.List;
/**
* This is the view model for the conversation header. It includes all the
* information needed to layout a conversation header view. Each view model is
* associated with a conversation and is cached to improve the relayout time.
*/
public class ConversationItemViewModel {
private static final int MAX_CACHE_SIZE = 100;
int fontColor;
@VisibleForTesting
static LruCache<Pair<String, Long>, ConversationItemViewModel> sConversationHeaderMap
= new LruCache<Pair<String, Long>, ConversationItemViewModel>(MAX_CACHE_SIZE);
/**
* The Folder associated with the cache of models.
*/
private static Folder sCachedModelsFolder;
// The hashcode used to detect if the conversation has changed.
private int mDataHashCode;
private int mLayoutHashCode;
// Unread
public boolean unread;
// Date
CharSequence dateText;
public CharSequence dateOverrideText;
// Personal level
Bitmap personalLevelBitmap;
public Bitmap infoIcon;
// Paperclip
Bitmap paperclip;
/** If <code>true</code>, we will not apply any formatting to {@link #sendersText}. */
public boolean preserveSendersText = false;
// Senders
public String sendersText;
// A list of all the fragments that cover sendersText
final ArrayList<SenderFragment> senderFragments;
SpannableStringBuilder sendersDisplayText;
StaticLayout sendersDisplayLayout;
boolean hasDraftMessage;
// Attachment Previews overflow
String overflowText;
// View Width
public int viewWidth;
// Standard scaled dimen used to detect if the scale of text has changed.
@Deprecated
public int standardScaledDimen;
public long maxMessageId;
public int gadgetMode;
public Conversation conversation;
public ConversationItemView.ConversationItemFolderDisplayer folderDisplayer;
public boolean hasBeenForwarded;
public boolean hasBeenRepliedTo;
public boolean isInvite;
public ArrayList<SpannableString> styledSenders;
public SpannableStringBuilder styledSendersString;
public SpannableStringBuilder messageInfoString;
public int styledMessageInfoStringOffset;
private String mContentDescription;
/**
* Email address corresponding to the senders that will be displayed in the
* senders field.
*/
public ArrayList<String> displayableSenderEmails;
/**
* Display names corresponding to the email address corresponding to the
* senders that will be displayed in the senders field.
*/
public ArrayList<String> displayableSenderNames;
/**
* Returns the view model for a conversation. If the model doesn't exist for this conversation
* null is returned. Note: this should only be called from the UI thread.
*
* @param account the account contains this conversation
* @param conversationId the Id of this conversation
* @return the view model for this conversation, or null
*/
@VisibleForTesting
static ConversationItemViewModel forConversationIdOrNull(
String account, long conversationId) {
final Pair<String, Long> key = new Pair<String, Long>(account, conversationId);
synchronized(sConversationHeaderMap) {
return sConversationHeaderMap.get(key);
}
}
static ConversationItemViewModel forConversation(String account, Conversation conv) {
ConversationItemViewModel header = ConversationItemViewModel.forConversationId(account,
conv.id);
header.conversation = conv;
header.unread = !conv.read;
header.hasBeenForwarded =
(conv.convFlags & UIProvider.ConversationFlags.FORWARDED)
== UIProvider.ConversationFlags.FORWARDED;
header.hasBeenRepliedTo =
(conv.convFlags & UIProvider.ConversationFlags.REPLIED)
== UIProvider.ConversationFlags.REPLIED;
header.isInvite =
(conv.convFlags & UIProvider.ConversationFlags.CALENDAR_INVITE)
== UIProvider.ConversationFlags.CALENDAR_INVITE;
return header;
}
/**
* Returns the view model for a conversation. If this is the first time
* call, a new view model will be returned. Note: this should only be called
* from the UI thread.
*
* @param account the account contains this conversation
* @param conversationId the Id of this conversation
* @param cursor the cursor to use in populating/ updating the model.
* @return the view model for this conversation
*/
static ConversationItemViewModel forConversationId(String account, long conversationId) {
synchronized(sConversationHeaderMap) {
ConversationItemViewModel header =
forConversationIdOrNull(account, conversationId);
if (header == null) {
final Pair<String, Long> key = new Pair<String, Long>(account, conversationId);
header = new ConversationItemViewModel();
sConversationHeaderMap.put(key, header);
}
return header;
}
}
public ConversationItemViewModel() {
senderFragments = Lists.newArrayList();
}
/**
* Adds a sender fragment.
*
* @param start the start position of this fragment
* @param end the start position of this fragment
* @param style the style of this fragment
* @param isFixed whether this fragment is fixed or not
*/
void addSenderFragment(int start, int end, CharacterStyle style, boolean isFixed) {
SenderFragment senderFragment = new SenderFragment(start, end, sendersText, style, isFixed);
senderFragments.add(senderFragment);
}
/**
* Returns the hashcode to compare if the data in the header is valid.
*/
private static int getHashCode(CharSequence dateText, Object convInfo,
List<Folder> rawFolders, boolean starred, boolean read, int priority,
int sendingState) {
if (dateText == null) {
return -1;
}
return Objects.hashCode(convInfo, dateText, rawFolders, starred, read, priority,
sendingState);
}
/**
* Returns the layout hashcode to compare to see if the layout state has changed.
*/
private int getLayoutHashCode() {
return Objects.hashCode(mDataHashCode, viewWidth, standardScaledDimen, gadgetMode);
}
/**
* Marks this header as having valid data and layout.
*/
void validate() {
mDataHashCode = getHashCode(dateText,
conversation.conversationInfo, conversation.getRawFolders(), conversation.starred,
conversation.read, conversation.priority, conversation.sendingState);
mLayoutHashCode = getLayoutHashCode();
}
/**
* Returns if the data in this model is valid.
*/
boolean isDataValid() {
return mDataHashCode == getHashCode(dateText,
conversation.conversationInfo, conversation.getRawFolders(), conversation.starred,
conversation.read, conversation.priority, conversation.sendingState);
}
/**
* Returns if the layout in this model is valid.
*/
boolean isLayoutValid() {
return isDataValid() && mLayoutHashCode == getLayoutHashCode();
}
/**
* Describes the style of a Senders fragment.
*/
static class SenderFragment {
// Indices that determine which substring of mSendersText we are
// displaying.
int start;
int end;
// The style to apply to the TextPaint object.
CharacterStyle style;
// Width of the fragment.
int width;
// Ellipsized text.
String ellipsizedText;
// Whether the fragment is fixed or not.
boolean isFixed;
// Should the fragment be displayed or not.
boolean shouldDisplay;
SenderFragment(int start, int end, CharSequence sendersText, CharacterStyle style,
boolean isFixed) {
this.start = start;
this.end = end;
this.style = style;
this.isFixed = isFixed;
}
}
/**
* Reset the content description; enough content has changed that we need to
* regenerate it.
*/
public void resetContentDescription() {
mContentDescription = null;
}
/**
* Get conversation information to use for accessibility.
*/
public CharSequence getContentDescription(Context context) {
if (mContentDescription == null) {
// If any are unread, get the first unread sender.
// If all are unread, get the first sender.
// If all are read, get the last sender.
String sender = "";
String lastSender = "";
int last = conversation.conversationInfo.messageInfos != null ?
conversation.conversationInfo.messageInfos.size() - 1 : -1;
if (last != -1) {
lastSender = conversation.conversationInfo.messageInfos.get(last).sender;
}
if (conversation.read) {
sender = TextUtils.isEmpty(lastSender) ?
SendersView.getMe(context) : lastSender;
} else {
MessageInfo firstUnread = null;
for (MessageInfo m : conversation.conversationInfo.messageInfos) {
if (!m.read) {
firstUnread = m;
break;
}
}
if (firstUnread != null) {
sender = TextUtils.isEmpty(firstUnread.sender) ?
SendersView.getMe(context) : firstUnread.sender;
}
}
if (TextUtils.isEmpty(sender)) {
// Just take the last sender
sender = lastSender;
}
boolean isToday = DateUtils.isToday(conversation.dateMs);
String date = DateUtils.getRelativeTimeSpanString(context, conversation.dateMs)
.toString();
String readString = context.getString(
conversation.read ? R.string.read_string : R.string.unread_string);
int res = isToday ? R.string.content_description_today : R.string.content_description;
mContentDescription = context.getString(res, sender,
conversation.subject, conversation.getSnippet(), date, readString);
}
return mContentDescription;
}
/**
* Clear cached header model objects when accessibility changes.
*/
public static void onAccessibilityUpdated() {
sConversationHeaderMap.evictAll();
}
/**
* Clear cached header model objects when the folder changes.
*/
public static void onFolderUpdated(Folder folder) {
final FolderUri old = sCachedModelsFolder != null
? sCachedModelsFolder.folderUri : FolderUri.EMPTY;
final FolderUri newUri = folder != null ? folder.folderUri : FolderUri.EMPTY;
if (!old.equals(newUri)) {
sCachedModelsFolder = folder;
sConversationHeaderMap.evictAll();
}
}
}