| /* |
| * 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(); |
| } |
| } |
| } |