ConversationItemViewModel.java revision 259df5b9e11908c8ef7c91483924891dd96b3c27
1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
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 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
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 */
17
18package com.android.mail.browse;
19
20import android.content.Context;
21import android.graphics.Bitmap;
22import android.text.SpannableString;
23import android.text.SpannableStringBuilder;
24import android.text.StaticLayout;
25import android.text.TextUtils;
26import android.text.format.DateUtils;
27import android.text.style.CharacterStyle;
28import android.util.LruCache;
29import android.util.Pair;
30
31import com.android.mail.R;
32import com.android.mail.providers.Conversation;
33import com.android.mail.providers.Folder;
34import com.android.mail.providers.MessageInfo;
35import com.android.mail.providers.UIProvider;
36import com.android.mail.utils.FolderUri;
37import com.google.common.annotations.VisibleForTesting;
38import com.google.common.base.Objects;
39import com.google.common.collect.Lists;
40
41import java.util.ArrayList;
42import java.util.List;
43
44/**
45 * This is the view model for the conversation header. It includes all the
46 * information needed to layout a conversation header view. Each view model is
47 * associated with a conversation and is cached to improve the relayout time.
48 */
49public class ConversationItemViewModel {
50    private static final int MAX_CACHE_SIZE = 100;
51
52    int fontColor;
53    @VisibleForTesting
54    static LruCache<Pair<String, Long>, ConversationItemViewModel> sConversationHeaderMap
55        = new LruCache<Pair<String, Long>, ConversationItemViewModel>(MAX_CACHE_SIZE);
56
57    /**
58     * The Folder associated with the cache of models.
59     */
60    private static Folder sCachedModelsFolder;
61
62    // The hashcode used to detect if the conversation has changed.
63    private int mDataHashCode;
64    private int mLayoutHashCode;
65
66    // Unread
67    boolean unread;
68
69    // Date
70    CharSequence dateText;
71
72    // Personal level
73    Bitmap personalLevelBitmap;
74
75    // Paperclip
76    Bitmap paperclip;
77
78    // Senders
79    String sendersText;
80
81    // A list of all the fragments that cover sendersText
82    final ArrayList<SenderFragment> senderFragments;
83
84    SpannableStringBuilder sendersDisplayText;
85    StaticLayout sendersDisplayLayout;
86
87    boolean hasDraftMessage;
88
89    // Attachment Previews overflow
90    String overflowText;
91
92    // View Width
93    public int viewWidth;
94
95    // Standard scaled dimen used to detect if the scale of text has changed.
96    @Deprecated
97    public int standardScaledDimen;
98
99    public long maxMessageId;
100
101    public int gadgetMode;
102
103    public Conversation conversation;
104
105    public ConversationItemView.ConversationItemFolderDisplayer folderDisplayer;
106
107    public boolean hasBeenForwarded;
108
109    public boolean hasBeenRepliedTo;
110
111    public boolean isInvite;
112
113    public ArrayList<SpannableString> styledSenders;
114
115    public SpannableStringBuilder styledSendersString;
116
117    public SpannableStringBuilder messageInfoString;
118
119    public int styledMessageInfoStringOffset;
120
121    private String mContentDescription;
122
123    /**
124     * Email address corresponding to the senders that will be displayed in the
125     * senders field.
126     */
127    public ArrayList<String> displayableSenderEmails;
128
129    /**
130     * Display names corresponding to the email address corresponding to the
131     * senders that will be displayed in the senders field.
132     */
133    public ArrayList<String> displayableSenderNames;
134
135    /**
136     * Returns the view model for a conversation. If the model doesn't exist for this conversation
137     * null is returned. Note: this should only be called from the UI thread.
138     *
139     * @param account the account contains this conversation
140     * @param conversationId the Id of this conversation
141     * @return the view model for this conversation, or null
142     */
143    @VisibleForTesting
144    static ConversationItemViewModel forConversationIdOrNull(
145            String account, long conversationId) {
146        final Pair<String, Long> key = new Pair<String, Long>(account, conversationId);
147        synchronized(sConversationHeaderMap) {
148            return sConversationHeaderMap.get(key);
149        }
150    }
151
152    static ConversationItemViewModel forConversation(String account, Conversation conv) {
153        ConversationItemViewModel header = ConversationItemViewModel.forConversationId(account,
154                conv.id);
155        header.conversation = conv;
156        header.unread = !conv.read;
157        header.hasBeenForwarded =
158                (conv.convFlags & UIProvider.ConversationFlags.FORWARDED)
159                == UIProvider.ConversationFlags.FORWARDED;
160        header.hasBeenRepliedTo =
161                (conv.convFlags & UIProvider.ConversationFlags.REPLIED)
162                == UIProvider.ConversationFlags.REPLIED;
163        header.isInvite =
164                (conv.convFlags & UIProvider.ConversationFlags.CALENDAR_INVITE)
165                == UIProvider.ConversationFlags.CALENDAR_INVITE;
166        return header;
167    }
168
169    /**
170     * Returns the view model for a conversation. If this is the first time
171     * call, a new view model will be returned. Note: this should only be called
172     * from the UI thread.
173     *
174     * @param account the account contains this conversation
175     * @param conversationId the Id of this conversation
176     * @param cursor the cursor to use in populating/ updating the model.
177     * @return the view model for this conversation
178     */
179    static ConversationItemViewModel forConversationId(String account, long conversationId) {
180        synchronized(sConversationHeaderMap) {
181            ConversationItemViewModel header =
182                    forConversationIdOrNull(account, conversationId);
183            if (header == null) {
184                final Pair<String, Long> key = new Pair<String, Long>(account, conversationId);
185                header = new ConversationItemViewModel();
186                sConversationHeaderMap.put(key, header);
187            }
188            return header;
189        }
190    }
191
192    public ConversationItemViewModel() {
193        senderFragments = Lists.newArrayList();
194    }
195
196    /**
197     * Adds a sender fragment.
198     *
199     * @param start the start position of this fragment
200     * @param end the start position of this fragment
201     * @param style the style of this fragment
202     * @param isFixed whether this fragment is fixed or not
203     */
204    void addSenderFragment(int start, int end, CharacterStyle style, boolean isFixed) {
205        SenderFragment senderFragment = new SenderFragment(start, end, sendersText, style, isFixed);
206        senderFragments.add(senderFragment);
207    }
208
209    /**
210     * Returns the hashcode to compare if the data in the header is valid.
211     */
212    private static int getHashCode(CharSequence dateText, Object convInfo,
213            List<Folder> rawFolders, boolean starred, boolean read, int priority,
214            int sendingState) {
215        if (dateText == null) {
216            return -1;
217        }
218        return Objects.hashCode(convInfo, dateText, rawFolders, starred, read, priority,
219                sendingState);
220    }
221
222    /**
223     * Returns the layout hashcode to compare to see if the layout state has changed.
224     */
225    private int getLayoutHashCode() {
226        return Objects.hashCode(mDataHashCode, viewWidth, standardScaledDimen, gadgetMode);
227    }
228
229    private Object getConvInfo() {
230        return conversation.conversationInfo != null ?
231                conversation.conversationInfo : conversation.getSnippet();
232    }
233
234    /**
235     * Marks this header as having valid data and layout.
236     */
237    void validate() {
238        mDataHashCode = getHashCode(dateText,
239                getConvInfo(), conversation.getRawFolders(), conversation.starred,
240                conversation.read, conversation.priority, conversation.sendingState);
241        mLayoutHashCode = getLayoutHashCode();
242    }
243
244    /**
245     * Returns if the data in this model is valid.
246     */
247    boolean isDataValid() {
248        return mDataHashCode == getHashCode(dateText,
249                getConvInfo(), conversation.getRawFolders(), conversation.starred,
250                conversation.read, conversation.priority, conversation.sendingState);
251    }
252
253    /**
254     * Returns if the layout in this model is valid.
255     */
256    boolean isLayoutValid() {
257        return isDataValid() && mLayoutHashCode == getLayoutHashCode();
258    }
259
260    /**
261     * Describes the style of a Senders fragment.
262     */
263    static class SenderFragment {
264        // Indices that determine which substring of mSendersText we are
265        // displaying.
266        int start;
267        int end;
268
269        // The style to apply to the TextPaint object.
270        CharacterStyle style;
271
272        // Width of the fragment.
273        int width;
274
275        // Ellipsized text.
276        String ellipsizedText;
277
278        // Whether the fragment is fixed or not.
279        boolean isFixed;
280
281        // Should the fragment be displayed or not.
282        boolean shouldDisplay;
283
284        SenderFragment(int start, int end, CharSequence sendersText, CharacterStyle style,
285                boolean isFixed) {
286            this.start = start;
287            this.end = end;
288            this.style = style;
289            this.isFixed = isFixed;
290        }
291    }
292
293
294    /**
295     * Reset the content description; enough content has changed that we need to
296     * regenerate it.
297     */
298    public void resetContentDescription() {
299        mContentDescription = null;
300    }
301
302    /**
303     * Get conversation information to use for accessibility.
304     */
305    public CharSequence getContentDescription(Context context) {
306        if (mContentDescription == null) {
307            // If any are unread, get the first unread sender.
308            // If all are unread, get the first sender.
309            // If all are read, get the last sender.
310            String sender = "";
311            if (conversation.conversationInfo != null) {
312                String lastSender = "";
313                int last = conversation.conversationInfo.messageInfos != null ?
314                        conversation.conversationInfo.messageInfos.size() - 1 : -1;
315                if (last != -1) {
316                    lastSender = conversation.conversationInfo.messageInfos.get(last).sender;
317                }
318                if (conversation.read) {
319                    sender = TextUtils.isEmpty(lastSender) ?
320                            SendersView.getMe(context) : lastSender;
321                } else {
322                    MessageInfo firstUnread = null;
323                    for (MessageInfo m : conversation.conversationInfo.messageInfos) {
324                        if (!m.read) {
325                            firstUnread = m;
326                            break;
327                        }
328                    }
329                    if (firstUnread != null) {
330                        sender = TextUtils.isEmpty(firstUnread.sender) ?
331                                SendersView.getMe(context) : firstUnread.sender;
332                    }
333                }
334                if (TextUtils.isEmpty(sender)) {
335                    // Just take the last sender
336                    sender = lastSender;
337                }
338            }
339            boolean isToday = DateUtils.isToday(conversation.dateMs);
340            String date = DateUtils.getRelativeTimeSpanString(context, conversation.dateMs)
341                    .toString();
342            String readString = context.getString(
343                    conversation.read ? R.string.read_string : R.string.unread_string);
344            int res = isToday ? R.string.content_description_today : R.string.content_description;
345            mContentDescription = context.getString(res, sender,
346                    conversation.subject, conversation.getSnippet(), date, readString);
347        }
348        return mContentDescription;
349    }
350
351    /**
352     * Clear cached header model objects when accessibility changes.
353     */
354
355    public static void onAccessibilityUpdated() {
356        sConversationHeaderMap.evictAll();
357    }
358
359    /**
360     * Clear cached header model objects when the folder changes.
361     */
362    public static void onFolderUpdated(Folder folder) {
363        final FolderUri old = sCachedModelsFolder != null
364                ? sCachedModelsFolder.folderUri : FolderUri.EMPTY;
365        final FolderUri newUri = folder != null ? folder.folderUri : FolderUri.EMPTY;
366        if (!old.equals(newUri)) {
367            sCachedModelsFolder = folder;
368            sConversationHeaderMap.evictAll();
369        }
370    }
371}