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