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.ui; 19 20 import com.android.mail.browse.ConversationCursor; 21 import com.android.mail.providers.Conversation; 22 import com.android.mail.providers.Settings; 23 import com.android.mail.providers.UIProvider.AutoAdvance; 24 import com.android.mail.utils.LogTag; 25 import com.android.mail.utils.LogUtils; 26 import java.util.Collection; 27 28 /** 29 * An iterator over a conversation list that keeps track of the position of a conversation, and 30 * updates the position accordingly when the underlying list data changes and the conversation 31 * is in a different position. 32 */ 33 public class ConversationPositionTracker { 34 protected static final String LOG_TAG = LogTag.getLogTag(); 35 36 37 public interface Callbacks { getConversationListCursor()38 ConversationCursor getConversationListCursor(); 39 } 40 41 42 /** Did we recalculate positions after updating the cursor? */ 43 private boolean mCursorDirty = false; 44 /** The currently selected conversation */ 45 private Conversation mConversation; 46 47 private final Callbacks mCallbacks; 48 49 /** 50 * Constructs a position tracker that doesn't point to any specific conversation. 51 */ ConversationPositionTracker(Callbacks callbacks)52 public ConversationPositionTracker(Callbacks callbacks) { 53 mCallbacks = callbacks; 54 } 55 56 /** Move cursor to a specific position and return the conversation there */ conversationAtPosition(int position)57 private Conversation conversationAtPosition(int position){ 58 final ConversationCursor cursor = mCallbacks.getConversationListCursor(); 59 cursor.moveToPosition(position); 60 final Conversation conv = cursor.getConversation(); 61 conv.position = position; 62 return conv; 63 } 64 65 /** 66 * @return the total number of conversations in the list. 67 */ getCount()68 private int getCount() { 69 final ConversationCursor cursor = mCallbacks.getConversationListCursor(); 70 if (isDataLoaded(cursor)) { 71 return cursor.getCount(); 72 } else { 73 return 0; 74 } 75 } 76 77 /** 78 * @return the {@link Conversation} of the newer conversation by one position. If no such 79 * conversation exists, this method returns null. 80 */ getNewer(Collection<Conversation> victims)81 private Conversation getNewer(Collection<Conversation> victims) { 82 int pos = calculatePosition(); 83 if (!isDataLoaded() || pos < 0) { 84 return null; 85 } 86 // Walk backward from the existing position, trying to find a conversation that is not a 87 // victim. 88 pos--; 89 while (pos >= 0) { 90 final Conversation candidate = conversationAtPosition(pos); 91 if (!Conversation.contains(victims, candidate)) { 92 return candidate; 93 } 94 pos--; 95 } 96 return null; 97 } 98 99 /** 100 * @return the {@link Conversation} of the older conversation by one spot. If no such 101 * conversation exists, this method returns null. 102 */ getOlder(Collection<Conversation> victims)103 private Conversation getOlder(Collection<Conversation> victims) { 104 int pos = calculatePosition(); 105 if (!isDataLoaded() || pos < 0) { 106 return null; 107 } 108 // Walk forward from the existing position, trying to find a conversation that is not a 109 // victim. 110 pos++; 111 while (pos < getCount()) { 112 final Conversation candidate = conversationAtPosition(pos); 113 if (!Conversation.contains(victims, candidate)) { 114 return candidate; 115 } 116 pos++; 117 } 118 return null; 119 } 120 121 /** 122 * Initializes the tracker with initial conversation id and initial position. This invalidates 123 * the positions in the tracker. We need a valid cursor before we can bless the position as 124 * valid. This requires a call to 125 * {@link #onCursorUpdated()}. 126 * TODO(viki): Get rid of this method and the mConversation field entirely. 127 */ initialize(Conversation conversation)128 public void initialize(Conversation conversation) { 129 mConversation = conversation; 130 mCursorDirty = true; 131 calculatePosition(); // Return value discarded. Running for side effects. 132 } 133 134 /** @return whether or not we have a valid cursor to check the position of. */ isDataLoaded(ConversationCursor cursor)135 private static boolean isDataLoaded(ConversationCursor cursor) { 136 return cursor != null && !cursor.isClosed(); 137 } 138 isDataLoaded()139 private boolean isDataLoaded() { 140 final ConversationCursor cursor = mCallbacks.getConversationListCursor(); 141 return isDataLoaded(cursor); 142 } 143 144 /** 145 * Called when the conversation list changes. 146 */ onCursorUpdated()147 public void onCursorUpdated() { 148 mCursorDirty = true; 149 } 150 151 /** 152 * Recalculate the current position based on the cursor. This needs to be done once for 153 * each (Conversation, Cursor) pair. We could do this on every change of conversation or 154 * cursor, but that would be wasteful, since the recalculation of position is only required 155 * when transitioning to the next conversation. Transitions don't happen frequently, but 156 * changes in conversation and cursor do. So we defer this till it is actually needed. 157 * 158 * This method could change the current conversation if it cannot find the current conversation 159 * in the cursor. When this happens, this method sets the current conversation to some safe 160 * value and logs the reasons why it couldn't find the conversation. 161 * 162 * Calling this method repeatedly is safe: it returns early if it detects it has already been 163 * called. 164 * @return the position of the current conversation in the cursor. 165 */ calculatePosition()166 private int calculatePosition() { 167 final int invalidPosition = -1; 168 final ConversationCursor cursor = mCallbacks.getConversationListCursor(); 169 // If we have a valid position and nothing has changed, return that right away 170 if (!mCursorDirty) { 171 return mConversation.position; 172 } 173 // Ensure valid input data 174 if (cursor == null || mConversation == null) { 175 return invalidPosition; 176 } 177 mCursorDirty = false; 178 final int listSize = cursor.getCount(); 179 if (!isDataLoaded(cursor) || listSize == 0) { 180 return invalidPosition; 181 } 182 183 final int foundPosition = cursor.getConversationPosition(mConversation.id); 184 if (foundPosition >= 0) { 185 mConversation.position = foundPosition; 186 // Pre-emptively try to load the next cursor position so that the cursor window 187 // can be filled. The odd behavior of the ConversationCursor requires us to do 188 // this to ensure the adjacent conversation information is loaded for calls to 189 // hasNext. 190 cursor.moveToPosition(foundPosition + 1); 191 return foundPosition; 192 } 193 194 // If the conversation is no longer found in the list, try to save the same position if 195 // it is still a valid position. Otherwise, go back to a valid position until we can 196 // find a valid one. 197 final int newPosition; 198 if (foundPosition >= listSize) { 199 // Go to the last position since our expected position is past this somewhere. 200 newPosition = listSize - 1; 201 } else { 202 newPosition = foundPosition; 203 } 204 205 // Did not keep the current conversation, so let's try to load the conversation from the 206 // new position. 207 if (isDataLoaded(cursor) && newPosition >= 0){ 208 LogUtils.d(LOG_TAG, "ConversationPositionTracker: Could not find conversation %s" + 209 " in the cursor. Moving to position %d ", mConversation.toString(), 210 newPosition); 211 cursor.moveToPosition(newPosition); 212 mConversation = new Conversation(cursor); 213 mConversation.position = newPosition; 214 } 215 return newPosition; 216 } 217 218 /** 219 * Get the next conversation according to the AutoAdvance settings and the list of 220 * conversations available in the folder. If no next conversation can be found, this method 221 * returns null. 222 * @param autoAdvance the auto advance preference for the user as an 223 * {@link Settings#getAutoAdvanceSetting()} value. 224 * @param mTarget conversations to overlook while finding the next conversation. (These are 225 * usually the conversations to be deleted.) 226 * @return the next conversation to be shown, or null if no next conversation exists. 227 */ getNextConversation(int autoAdvance, Collection<Conversation> mTarget)228 public Conversation getNextConversation(int autoAdvance, Collection<Conversation> mTarget) { 229 final boolean getNewer = autoAdvance == AutoAdvance.NEWER; 230 final boolean getOlder = autoAdvance == AutoAdvance.OLDER; 231 final Conversation next = getNewer ? getNewer(mTarget) : 232 (getOlder ? getOlder(mTarget) : null); 233 LogUtils.d(LOG_TAG, "ConversationPositionTracker.getNextConversation: " + 234 "getNewer = %b, getOlder = %b, Next conversation is %s", 235 getNewer, getOlder, next); 236 return next; 237 } 238 239 }