ConversationItemViewModel.java revision aa1f945612847bc4cf5c8909b8acfab4b5ecf54e
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.UIProvider;
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    public SpannableString[] styledSenders;
124
125    public SpannableStringBuilder styledSendersString;
126
127    /**
128     * Returns the view model for a conversation. If the model doesn't exist for this conversation
129     * null is returned. Note: this should only be called from the UI thread.
130     *
131     * @param account the account contains this conversation
132     * @param conversationId the Id of this conversation
133     * @return the view model for this conversation, or null
134     */
135    @VisibleForTesting
136    static ConversationItemViewModel forConversationIdOrNull(
137            String account, long conversationId) {
138        final Pair<String, Long> key = new Pair<String, Long>(account, conversationId);
139        synchronized(sConversationHeaderMap) {
140            return sConversationHeaderMap.get(key);
141        }
142    }
143
144    static ConversationItemViewModel forCursor(Cursor cursor) {
145        return forConversation(new Conversation(cursor));
146    }
147
148
149    static ConversationItemViewModel forConversation(Conversation conv) {
150        ConversationItemViewModel header = new ConversationItemViewModel();
151        if (conv != null) {
152            header.faded = false;
153            header.checkboxVisible = true;
154            header.conversation = conv;
155            header.starred = conv.starred;
156            header.unread = !conv.read;
157            header.rawFolders = conv.rawFolders;
158            header.personalLevel = conv.personalLevel;
159            header.priority = conv.priority;
160            header.hasBeenForwarded =
161                    (conv.convFlags & UIProvider.ConversationFlags.FORWARDED)
162                    == UIProvider.ConversationFlags.FORWARDED;
163            header.hasBeenRepliedTo =
164                    (conv.convFlags & UIProvider.ConversationFlags.REPLIED)
165                    == UIProvider.ConversationFlags.REPLIED;
166            header.isInvite =
167                    (conv.convFlags & UIProvider.ConversationFlags.CALENDAR_INVITE)
168                    == UIProvider.ConversationFlags.CALENDAR_INVITE;
169        }
170        return header;
171    }
172
173    /**
174     * Returns the view model for a conversation. If this is the first time
175     * call, a new view model will be returned. Note: this should only be called
176     * from the UI thread.
177     *
178     * @param account the account contains this conversation
179     * @param conversationId the Id of this conversation
180     * @param cursor the cursor to use in populating/ updating the model.
181     * @return the view model for this conversation
182     */
183    static ConversationItemViewModel forConversationId(String account, long conversationId) {
184        synchronized(sConversationHeaderMap) {
185            ConversationItemViewModel header =
186                    forConversationIdOrNull(account, conversationId);
187            if (header == null) {
188                final Pair<String, Long> key = new Pair<String, Long>(account, conversationId);
189                header = new ConversationItemViewModel();
190                sConversationHeaderMap.put(key, header);
191            }
192            return header;
193        }
194    }
195
196    public ConversationItemViewModel() {
197        senderFragments = Lists.newArrayList();
198    }
199
200    /**
201     * Adds a sender fragment.
202     *
203     * @param start the start position of this fragment
204     * @param end the start position of this fragment
205     * @param style the style of this fragment
206     * @param isFixed whether this fragment is fixed or not
207     */
208    void addSenderFragment(int start, int end, CharacterStyle style, boolean isFixed) {
209        SenderFragment senderFragment = new SenderFragment(start, end, sendersText, style, isFixed);
210        senderFragments.add(senderFragment);
211    }
212
213    /**
214     * Clears all the current sender fragments.
215     */
216    void clearSenderFragments() {
217        senderFragments.clear();
218    }
219
220    /**
221     * Returns the hashcode to compare if the data in the header is valid.
222     */
223    private static int getHashCode(Context context, String dateText, String fromSnippetInstructions) {
224        if (dateText == null) {
225            return -1;
226        }
227        if (TextUtils.isEmpty(fromSnippetInstructions)) {
228            fromSnippetInstructions = "fromSnippetInstructions";
229        }
230        return fromSnippetInstructions.hashCode() ^ dateText.hashCode();
231    }
232
233    /**
234     * Returns the layout hashcode to compare to see if thet layout state has changed.
235     */
236    private int getLayoutHashCode() {
237        return mDataHashCode ^ viewWidth ^ standardScaledDimen ^
238                Boolean.valueOf(checkboxVisible).hashCode();
239    }
240
241    /**
242     * Marks this header as having valid data and layout.
243     */
244    void validate(Context context) {
245        mDataHashCode = getHashCode(context, dateText, fromSnippetInstructions);
246        mLayoutHashCode = getLayoutHashCode();
247    }
248
249    /**
250     * Returns if the data in this model is valid.
251     */
252    boolean isDataValid(Context context) {
253        return mDataHashCode == getHashCode(context, dateText, fromSnippetInstructions);
254    }
255
256    /**
257     * Returns if the layout in this model is valid.
258     */
259    boolean isLayoutValid(Context context) {
260        return isDataValid(context) && mLayoutHashCode == getLayoutHashCode();
261    }
262
263    /**
264     * Describes the style of a Senders fragment.
265     */
266    static class SenderFragment {
267        // Indices that determine which substring of mSendersText we are
268        // displaying.
269        int start;
270        int end;
271
272        // The style to apply to the TextPaint object.
273        CharacterStyle style;
274
275        // Width of the fragment.
276        int width;
277
278        // Ellipsized text.
279        String ellipsizedText;
280
281        // Whether the fragment is fixed or not.
282        boolean isFixed;
283
284        // Should the fragment be displayed or not.
285        boolean shouldDisplay;
286
287        SenderFragment(int start, int end, CharSequence sendersText, CharacterStyle style,
288                boolean isFixed) {
289            this.start = start;
290            this.end = end;
291            this.style = style;
292            this.isFixed = isFixed;
293        }
294    }
295
296    /**
297     * Get conversation information to use for accessibility.
298     */
299    public CharSequence getContentDescription(Context context) {
300        return context.getString(R.string.content_description, conversation.subject,
301                conversation.getSnippet());
302    }
303}