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