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 { 52 Conversation getConversation(); 53 ConversationUpdater getListController(); 54 MessageCursor getMessageCursor(); 55 Account getAccount(); 56 } 57 58 public MessageCursor(Cursor inner) { 59 super(inner, ConversationMessage.FACTORY); 60 } 61 62 public void setController(ConversationController controller) { 63 mController = controller; 64 } 65 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 76 public Conversation getConversation() { 77 return mController != null ? mController.getConversation() : null; 78 } 79 80 // Is the conversation starred? 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 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 } 101 public void markMessagesRead() { 102 int pos = -1; 103 while (moveToPosition(++pos)) { 104 getMessage().read = true; 105 } 106 } 107 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 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 */ 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 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 */ 165 public boolean isLoaded() { 166 return !CursorStatus.isWaitingForResults(getStatus()); 167 } 168 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 }