ConversationItemViewModel.java revision 4f2224c70d4d07df85b325fa3faf78218f92aae6
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 com.google.common.annotations.VisibleForTesting;
21import com.google.common.collect.Lists;
22
23import android.content.Context;
24import android.database.Cursor;
25import android.graphics.Bitmap;
26import android.text.SpannableString;
27import android.text.SpannableStringBuilder;
28import android.text.StaticLayout;
29import android.text.TextUtils;
30import android.text.style.CharacterStyle;
31import android.util.LruCache;
32import android.util.Pair;
33
34import com.android.mail.R;
35import com.android.mail.providers.Conversation;
36import com.android.mail.providers.Folder;
37import com.android.mail.providers.UIProvider;
38
39import java.util.ArrayList;
40
41/**
42 * This is the view model for the conversation header. It includes all the
43 * information needed to layout a conversation header view. Each view model is
44 * associated with a conversation and is cached to improve the relayout time.
45 */
46public class ConversationItemViewModel {
47    private static final int MAX_CACHE_SIZE = 100;
48
49    boolean faded = false;
50    int fontColor;
51    @VisibleForTesting
52    static LruCache<Pair<String, Long>, ConversationItemViewModel> sConversationHeaderMap
53        = new LruCache<Pair<String, Long>, ConversationItemViewModel>(MAX_CACHE_SIZE);
54
55    // The hashcode used to detect if the conversation has changed.
56    private int mDataHashCode;
57    private int mLayoutHashCode;
58
59    // Unread
60    boolean unread;
61
62    // Date
63    String dateText;
64    Bitmap dateBackground;
65
66    // Personal level
67    Bitmap personalLevelBitmap;
68
69    // Paperclip
70    Bitmap paperclip;
71
72    // Senders
73    String sendersText;
74
75    // A list of all the fragments that cover sendersText
76    final ArrayList<SenderFragment> senderFragments;
77
78    SpannableStringBuilder sendersDisplayText;
79    StaticLayout sendersDisplayLayout;
80
81    boolean hasDraftMessage;
82
83    // Subject
84    SpannableStringBuilder subjectText;
85
86    StaticLayout subjectLayout;
87
88    // View Width
89    public int viewWidth;
90
91    // Standard scaled dimen used to detect if the scale of text has changed.
92    public int standardScaledDimen;
93
94    public String fromSnippetInstructions;
95
96    public long maxMessageId;
97
98    public boolean checkboxVisible;
99
100    public Conversation conversation;
101
102    public ConversationItemView.ConversationItemFolderDisplayer folderDisplayer;
103
104    public ArrayList<Folder> rawFolders;
105
106    public int personalLevel;
107
108    public int priority;
109
110    public boolean hasBeenForwarded;
111
112    public boolean hasBeenRepliedTo;
113
114    public boolean isInvite;
115
116    public StaticLayout subjectLayoutActivated;
117
118    public SpannableStringBuilder subjectTextActivated;
119
120    public SpannableString[] styledSenders;
121
122    public SpannableStringBuilder styledSendersString;
123
124    /**
125     * Returns the view model for a conversation. If the model doesn't exist for this conversation
126     * null is returned. Note: this should only be called from the UI thread.
127     *
128     * @param account the account contains this conversation
129     * @param conversationId the Id of this conversation
130     * @return the view model for this conversation, or null
131     */
132    @VisibleForTesting
133    static ConversationItemViewModel forConversationIdOrNull(
134            String account, long conversationId) {
135        final Pair<String, Long> key = new Pair<String, Long>(account, conversationId);
136        synchronized(sConversationHeaderMap) {
137            return sConversationHeaderMap.get(key);
138        }
139    }
140
141    static ConversationItemViewModel forCursor(String account, Cursor cursor) {
142        return forConversation(account, new Conversation(cursor));
143    }
144
145    static ConversationItemViewModel forConversation(String account, Conversation conv) {
146        ConversationItemViewModel header = ConversationItemViewModel.forConversationId(account,
147                conv.id);
148        if (conv != null) {
149            header.faded = false;
150            header.checkboxVisible = true;
151            header.conversation = conv;
152            header.unread = !conv.read;
153            header.rawFolders = conv.getRawFolders();
154            header.personalLevel = conv.personalLevel;
155            header.priority = conv.priority;
156            header.hasBeenForwarded =
157                    (conv.convFlags & UIProvider.ConversationFlags.FORWARDED)
158                    == UIProvider.ConversationFlags.FORWARDED;
159            header.hasBeenRepliedTo =
160                    (conv.convFlags & UIProvider.ConversationFlags.REPLIED)
161                    == UIProvider.ConversationFlags.REPLIED;
162            header.isInvite =
163                    (conv.convFlags & UIProvider.ConversationFlags.CALENDAR_INVITE)
164                    == UIProvider.ConversationFlags.CALENDAR_INVITE;
165        }
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     * Clears all the current sender fragments.
211     */
212    void clearSenderFragments() {
213        senderFragments.clear();
214    }
215
216    /**
217     * Returns the hashcode to compare if the data in the header is valid.
218     */
219    private static int getHashCode(Context context, String dateText, Object convInfo,
220            String rawFolders, boolean starred) {
221        if (dateText == null) {
222            return -1;
223        }
224        if (TextUtils.isEmpty(rawFolders)) {
225            rawFolders = "";
226        }
227        return convInfo.hashCode() ^ dateText.hashCode() ^ rawFolders.hashCode()
228                ^ (starred ? 1 : 0);
229    }
230
231    /**
232     * Returns the layout hashcode to compare to see if the layout state has changed.
233     */
234    private int getLayoutHashCode() {
235        return mDataHashCode ^ viewWidth ^ standardScaledDimen
236                ^ Boolean.valueOf(checkboxVisible).hashCode();
237    }
238
239    private Object getConvInfo() {
240        return conversation.conversationInfo != null ?
241                conversation.conversationInfo :
242                    TextUtils.isEmpty(fromSnippetInstructions) ? "" : fromSnippetInstructions;
243    }
244
245    /**
246     * Marks this header as having valid data and layout.
247     */
248    void validate(Context context) {
249        mDataHashCode = getHashCode(context, dateText, getConvInfo(),
250                conversation.getRawFoldersString(), conversation.starred);
251        mLayoutHashCode = getLayoutHashCode();
252    }
253
254    /**
255     * Returns if the data in this model is valid.
256     */
257    boolean isDataValid(Context context) {
258        return mDataHashCode == getHashCode(context, dateText, getConvInfo(),
259                conversation.getRawFoldersString(), conversation.starred);
260    }
261
262    /**
263     * Returns if the layout in this model is valid.
264     */
265    boolean isLayoutValid(Context context) {
266        return isDataValid(context) && mLayoutHashCode == getLayoutHashCode();
267    }
268
269    /**
270     * Describes the style of a Senders fragment.
271     */
272    static class SenderFragment {
273        // Indices that determine which substring of mSendersText we are
274        // displaying.
275        int start;
276        int end;
277
278        // The style to apply to the TextPaint object.
279        CharacterStyle style;
280
281        // Width of the fragment.
282        int width;
283
284        // Ellipsized text.
285        String ellipsizedText;
286
287        // Whether the fragment is fixed or not.
288        boolean isFixed;
289
290        // Should the fragment be displayed or not.
291        boolean shouldDisplay;
292
293        SenderFragment(int start, int end, CharSequence sendersText, CharacterStyle style,
294                boolean isFixed) {
295            this.start = start;
296            this.end = end;
297            this.style = style;
298            this.isFixed = isFixed;
299        }
300    }
301
302    /**
303     * Get conversation information to use for accessibility.
304     */
305    public CharSequence getContentDescription(Context context) {
306        return context.getString(R.string.content_description, conversation.subject,
307                conversation.getSnippet());
308    }
309}