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 
18 package com.android.mail.browse;
19 
20 import android.database.Cursor;
21 import android.net.Uri;
22 import android.os.Bundle;
23 
24 import com.android.mail.content.ObjectCursor;
25 import com.android.mail.providers.Account;
26 import com.android.mail.providers.Attachment;
27 import com.android.mail.providers.Conversation;
28 import com.android.mail.providers.UIProvider.CursorExtraKeys;
29 import com.android.mail.providers.UIProvider.CursorStatus;
30 import com.android.mail.ui.ConversationUpdater;
31 
32 import com.google.common.collect.Lists;
33 
34 import java.util.List;
35 
36 /**
37  * MessageCursor contains the messages within a conversation; the public methods within should
38  * only be called by the UI thread, as cursor position isn't guaranteed to be maintained
39  */
40 public class MessageCursor extends ObjectCursor<ConversationMessage> {
41     /**
42      * The current controller that this cursor can use to reference the owning {@link Conversation},
43      * and a current {@link ConversationUpdater}. Since this cursor will survive a rotation, but
44      * the controller does not, whatever the new controller is MUST update this reference before
45      * using this cursor.
46      */
47     private ConversationController mController;
48 
49     private Integer mStatus;
50 
51     public interface ConversationController {
getConversation()52         Conversation getConversation();
getListController()53         ConversationUpdater getListController();
getMessageCursor()54         MessageCursor getMessageCursor();
getAccount()55         Account getAccount();
56     }
57 
MessageCursor(Cursor inner)58     public MessageCursor(Cursor inner) {
59         super(inner, ConversationMessage.FACTORY);
60     }
61 
setController(ConversationController controller)62     public void setController(ConversationController controller) {
63         mController = controller;
64     }
65 
getMessage()66     public ConversationMessage getMessage() {
67         final ConversationMessage m = getModel();
68         // ALWAYS set up each ConversationMessage with the latest controller.
69         // Rotation invalidates everything except this Cursor, its Loader and the cached Messages,
70         // so if we want to continue using them after rotate, we have to ensure their controller
71         // references always point to the current controller.
72         m.setController(mController);
73         return m;
74     }
75 
getConversation()76     public Conversation getConversation() {
77         return mController != null ? mController.getConversation() : null;
78     }
79 
80     // Is the conversation starred?
isConversationStarred()81     public boolean isConversationStarred() {
82         int pos = -1;
83         while (moveToPosition(++pos)) {
84             if (getMessage().starred) {
85                 return true;
86             }
87         }
88         return false;
89     }
90 
91 
isConversationRead()92     public boolean isConversationRead() {
93         int pos = -1;
94         while (moveToPosition(++pos)) {
95             if (!getMessage().read) {
96                 return false;
97             }
98         }
99         return true;
100     }
markMessagesRead()101     public void markMessagesRead() {
102         int pos = -1;
103         while (moveToPosition(++pos)) {
104             getMessage().read = true;
105         }
106     }
107 
getMessageForId(long id)108     public ConversationMessage getMessageForId(long id) {
109         if (isClosed()) {
110             return null;
111         }
112 
113         int pos = -1;
114         while (moveToPosition(++pos)) {
115             final ConversationMessage m = getMessage();
116             if (id == m.id) {
117                 return m;
118             }
119         }
120         return null;
121     }
122 
getStateHashCode()123     public int getStateHashCode() {
124         return getStateHashCode(0);
125     }
126 
127     /**
128      * Calculate a hash code that compactly summarizes the state of the messages in this cursor,
129      * with respect to the way the messages are displayed in conversation view. This is not a
130      * general-purpose hash code. When the state hash codes of a new cursor differs from the
131      * existing cursor's hash code, the conversation view will re-render from scratch.
132      *
133      * @param exceptLast optional number of messages to exclude iterating through at the end of the
134      * cursor. pass zero to iterate through all messages (or use {@link #getStateHashCode()}).
135      * @return state hash code of the selected messages in this cursor
136      */
getStateHashCode(int exceptLast)137     public int getStateHashCode(int exceptLast) {
138         int hashCode = 17;
139         int pos = -1;
140         final int stopAt = getCount() - exceptLast;
141         while (moveToPosition(++pos) && pos < stopAt) {
142             hashCode = 31 * hashCode + getMessage().getStateHashCode();
143         }
144         return hashCode;
145     }
146 
getStatus()147     public int getStatus() {
148         if (mStatus != null) {
149             return mStatus;
150         }
151 
152         mStatus = CursorStatus.LOADED;
153         final Bundle extras = getExtras();
154         if (extras != null && extras.containsKey(CursorExtraKeys.EXTRA_STATUS)) {
155             mStatus = extras.getInt(CursorExtraKeys.EXTRA_STATUS);
156         }
157         return mStatus;
158     }
159 
160     /**
161      * Returns true if the cursor is fully loaded. Returns false if the cursor is expected to get
162      * new messages.
163      * @return
164      */
isLoaded()165     public boolean isLoaded() {
166         return !CursorStatus.isWaitingForResults(getStatus());
167     }
168 
getDebugDump()169     public String getDebugDump() {
170         StringBuilder sb = new StringBuilder();
171         sb.append(String.format("conv='%s' status=%d messages:\n",
172                 mController.getConversation(), getStatus()));
173         int pos = -1;
174         while (moveToPosition(++pos)) {
175             final ConversationMessage m = getMessage();
176             final List<Uri> attUris = Lists.newArrayList();
177             for (Attachment a : m.getAttachments()) {
178                 attUris.add(a.uri);
179             }
180             sb.append(String.format(
181                     "[Message #%d hash=%s uri=%s id=%s serverId=%s from='%s' draftType=%d" +
182                     " sendingState=%s read=%s starred=%s attUris=%s]\n",
183                     pos, m.getStateHashCode(), m.uri, m.id, m.serverId, m.getFrom(), m.draftType,
184                     m.sendingState, m.read, m.starred, attUris));
185         }
186         return sb.toString();
187     }
188 
189 }