ConversationItemViewModel.java revision 12fe37aa24c313fd8192dede0770dadf0ec23359
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.base.Objects;
22import com.google.common.collect.Lists;
23
24import android.content.Context;
25import android.database.Cursor;
26import android.graphics.Bitmap;
27import android.net.Uri;
28import android.text.SpannableString;
29import android.text.SpannableStringBuilder;
30import android.text.StaticLayout;
31import android.text.TextUtils;
32import android.text.style.CharacterStyle;
33import android.util.LruCache;
34import android.util.Pair;
35
36import com.android.mail.R;
37import com.android.mail.providers.Conversation;
38import com.android.mail.providers.Folder;
39import com.android.mail.providers.UIProvider;
40
41import java.util.ArrayList;
42
43/**
44 * This is the view model for the conversation header. It includes all the
45 * information needed to layout a conversation header view. Each view model is
46 * associated with a conversation and is cached to improve the relayout time.
47 */
48public class ConversationItemViewModel {
49    private static final int MAX_CACHE_SIZE = 100;
50
51    boolean faded = false;
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    String dateText;
71    Bitmap dateBackground;
72
73    // Personal level
74    Bitmap personalLevelBitmap;
75
76    // Paperclip
77    Bitmap paperclip;
78
79    // Senders
80    String sendersText;
81
82    // A list of all the fragments that cover sendersText
83    final ArrayList<SenderFragment> senderFragments;
84
85    SpannableStringBuilder sendersDisplayText;
86    StaticLayout sendersDisplayLayout;
87
88    boolean hasDraftMessage;
89
90    // Subject
91    SpannableStringBuilder subjectText;
92
93    StaticLayout subjectLayout;
94
95    // View Width
96    public int viewWidth;
97
98    // Standard scaled dimen used to detect if the scale of text has changed.
99    public int standardScaledDimen;
100
101    public String fromSnippetInstructions;
102
103    public long maxMessageId;
104
105    public boolean checkboxVisible;
106
107    public Conversation conversation;
108
109    public ConversationItemView.ConversationItemFolderDisplayer folderDisplayer;
110
111    public boolean hasBeenForwarded;
112
113    public boolean hasBeenRepliedTo;
114
115    public boolean isInvite;
116
117    public StaticLayout subjectLayoutActivated;
118
119    public SpannableStringBuilder subjectTextActivated;
120
121    public SpannableString[] styledSenders;
122
123    public SpannableStringBuilder styledSendersString;
124
125    public SpannableStringBuilder messageInfoString;
126
127    public int styledMessageInfoStringOffset;
128
129    /**
130     * Returns the view model for a conversation. If the model doesn't exist for this conversation
131     * null is returned. Note: this should only be called from the UI thread.
132     *
133     * @param account the account contains this conversation
134     * @param conversationId the Id of this conversation
135     * @return the view model for this conversation, or null
136     */
137    @VisibleForTesting
138    static ConversationItemViewModel forConversationIdOrNull(
139            String account, long conversationId) {
140        final Pair<String, Long> key = new Pair<String, Long>(account, conversationId);
141        synchronized(sConversationHeaderMap) {
142            return sConversationHeaderMap.get(key);
143        }
144    }
145
146    static ConversationItemViewModel forCursor(String account, Cursor cursor) {
147        return forConversation(account, new Conversation(cursor));
148    }
149
150    static ConversationItemViewModel forConversation(String account, Conversation conv) {
151        ConversationItemViewModel header = ConversationItemViewModel.forConversationId(account,
152                conv.id);
153        if (conv != null) {
154            header.faded = false;
155            header.checkboxVisible = true;
156            header.conversation = conv;
157            header.unread = !conv.read;
158            header.hasBeenForwarded =
159                    (conv.convFlags & UIProvider.ConversationFlags.FORWARDED)
160                    == UIProvider.ConversationFlags.FORWARDED;
161            header.hasBeenRepliedTo =
162                    (conv.convFlags & UIProvider.ConversationFlags.REPLIED)
163                    == UIProvider.ConversationFlags.REPLIED;
164            header.isInvite =
165                    (conv.convFlags & UIProvider.ConversationFlags.CALENDAR_INVITE)
166                    == UIProvider.ConversationFlags.CALENDAR_INVITE;
167        }
168        return header;
169    }
170
171    /**
172     * Returns the view model for a conversation. If this is the first time
173     * call, a new view model will be returned. Note: this should only be called
174     * from the UI thread.
175     *
176     * @param account the account contains this conversation
177     * @param conversationId the Id of this conversation
178     * @param cursor the cursor to use in populating/ updating the model.
179     * @return the view model for this conversation
180     */
181    static ConversationItemViewModel forConversationId(String account, long conversationId) {
182        synchronized(sConversationHeaderMap) {
183            ConversationItemViewModel header =
184                    forConversationIdOrNull(account, conversationId);
185            if (header == null) {
186                final Pair<String, Long> key = new Pair<String, Long>(account, conversationId);
187                header = new ConversationItemViewModel();
188                sConversationHeaderMap.put(key, header);
189            }
190            return header;
191        }
192    }
193
194    public ConversationItemViewModel() {
195        senderFragments = Lists.newArrayList();
196    }
197
198    /**
199     * Adds a sender fragment.
200     *
201     * @param start the start position of this fragment
202     * @param end the start position of this fragment
203     * @param style the style of this fragment
204     * @param isFixed whether this fragment is fixed or not
205     */
206    void addSenderFragment(int start, int end, CharacterStyle style, boolean isFixed) {
207        SenderFragment senderFragment = new SenderFragment(start, end, sendersText, style, isFixed);
208        senderFragments.add(senderFragment);
209    }
210
211    /**
212     * Clears all the current sender fragments.
213     */
214    void clearSenderFragments() {
215        senderFragments.clear();
216    }
217
218    /**
219     * Returns the hashcode to compare if the data in the header is valid.
220     */
221    private static int getHashCode(Context context, String dateText, Object convInfo,
222            String rawFolders, boolean starred, boolean read, int priority) {
223        if (dateText == null) {
224            return -1;
225        }
226        if (TextUtils.isEmpty(rawFolders)) {
227            rawFolders = "";
228        }
229        return Objects.hashCode(convInfo, dateText, rawFolders, starred, read, priority);
230    }
231
232    /**
233     * Returns the layout hashcode to compare to see if the layout state has changed.
234     */
235    private int getLayoutHashCode() {
236        return Objects.hashCode(mDataHashCode, viewWidth, standardScaledDimen, checkboxVisible);
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, conversation.read,
251                conversation.priority);
252        mLayoutHashCode = getLayoutHashCode();
253    }
254
255    /**
256     * Returns if the data in this model is valid.
257     */
258    boolean isDataValid(Context context) {
259        return mDataHashCode == getHashCode(context, dateText, getConvInfo(),
260                conversation.getRawFoldersString(), conversation.starred, conversation.read,
261                conversation.priority);
262    }
263
264    /**
265     * Returns if the layout in this model is valid.
266     */
267    boolean isLayoutValid(Context context) {
268        return isDataValid(context) && mLayoutHashCode == getLayoutHashCode();
269    }
270
271    /**
272     * Describes the style of a Senders fragment.
273     */
274    static class SenderFragment {
275        // Indices that determine which substring of mSendersText we are
276        // displaying.
277        int start;
278        int end;
279
280        // The style to apply to the TextPaint object.
281        CharacterStyle style;
282
283        // Width of the fragment.
284        int width;
285
286        // Ellipsized text.
287        String ellipsizedText;
288
289        // Whether the fragment is fixed or not.
290        boolean isFixed;
291
292        // Should the fragment be displayed or not.
293        boolean shouldDisplay;
294
295        SenderFragment(int start, int end, CharSequence sendersText, CharacterStyle style,
296                boolean isFixed) {
297            this.start = start;
298            this.end = end;
299            this.style = style;
300            this.isFixed = isFixed;
301        }
302    }
303
304    /**
305     * Get conversation information to use for accessibility.
306     */
307    public CharSequence getContentDescription(Context context) {
308        return context.getString(R.string.content_description, conversation.subject,
309                conversation.getSnippet());
310    }
311
312    /**
313     * Clear cached header model objects when the folder changes.
314     */
315    public static void onFolderUpdated(Folder folder) {
316        Uri old = sCachedModelsFolder != null ? sCachedModelsFolder.uri : Uri.EMPTY;
317        Uri newUri = folder != null ? folder.uri : Uri.EMPTY;
318        if (!old.equals(newUri)) {
319            sCachedModelsFolder = folder;
320            sConversationHeaderMap.evictAll();
321        }
322    }
323}