ConversationPositionTracker.java revision c7694221dfa5cec3f4ae290f2266b081b2639d80
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.ui; 19 20import android.database.Cursor; 21 22import com.android.mail.browse.ConversationCursor; 23import com.android.mail.providers.Conversation; 24import com.android.mail.providers.Settings; 25import com.android.mail.providers.UIProvider.AutoAdvance; 26import com.android.mail.utils.LogUtils; 27import com.google.common.annotations.VisibleForTesting; 28 29/** 30 * An iterator over a conversation list that keeps track of the position of a conversation, and 31 * updates the position accordingly when the underlying list data changes and the conversation 32 * is in a different position. 33 */ 34public class ConversationPositionTracker { 35 protected static final String LOG_TAG = new LogUtils().getLogTag(); 36 37 /** Cursor into the conversations */ 38 private ConversationCursor mCursor = null; 39 /** Did we recalculate positions after updating the cursor? */ 40 private boolean mCursorDirty = false; 41 /** The currently selected conversation */ 42 private Conversation mConversation; 43 /** The selected set */ 44 private final ConversationSelectionSet mSelectedSet; 45 46 /** 47 * This utility method returns the conversation ID at the current cursor position. 48 * @return the conversation id at the cursor. 49 */ 50 private static long getConversationId(Cursor cursor) { 51 final Conversation conversation = new Conversation(cursor); 52 return conversation.id; 53 } 54 55 /** 56 * Constructs a position tracker that doesn't point to any specific conversation. 57 */ 58 public ConversationPositionTracker(ConversationSelectionSet selectedSet) { 59 mSelectedSet = selectedSet; 60 } 61 62 /** 63 * Clears the current selected position. 64 */ 65 public void clearPosition() { 66 initialize(null); 67 } 68 69 /** Move cursor to a specific position and return the conversation there */ 70 private Conversation conversationAtPosition(int position){ 71 mCursor.moveToPosition(position); 72 final Conversation conv = new Conversation(mCursor); 73 conv.position = position; 74 return conv; 75 } 76 77 /** 78 * @return the total number of conversations in the list. 79 */ 80 public int getCount() { 81 if (isDataLoaded()) { 82 return mCursor.getCount(); 83 } else { 84 return 0; 85 } 86 } 87 88 /** 89 * @return the {@link Conversation} of the newer conversation by one position. If no such 90 * conversation exists, this method returns null. 91 */ 92 public Conversation getNewer() { 93 calculatePosition(); 94 if (!hasNewer()) { 95 return null; 96 } 97 return conversationAtPosition(mConversation.position - 1); 98 } 99 100 /** 101 * @return the {@link Conversation} of the next newer conversation not in the selection set. If 102 * no such conversation exists, this method returns null. 103 */ 104 public Conversation getNewerUnselected() { 105 calculatePosition(); 106 if (!isDataLoaded()) { 107 return null; 108 } 109 110 int pos = mConversation.position - 1; 111 while (pos >= 0) { 112 final Conversation conversation = conversationAtPosition(pos); 113 final long id = conversation.id; 114 if (!mSelectedSet.containsKey(id)) { 115 return conversation; 116 } 117 pos--; 118 } 119 return null; 120 } 121 122 /** 123 * @return the {@link Conversation} of the older conversation by one spot. If no such 124 * conversation exists, this method returns null. 125 */ 126 public Conversation getOlder() { 127 calculatePosition(); 128 if (!hasOlder()) { 129 return null; 130 } 131 return conversationAtPosition(mConversation.position + 1); 132 } 133 134 /** 135 * @return the {@link Conversation} of the next older conversation not in the selection set. 136 */ 137 public Conversation getOlderUnselected() { 138 calculatePosition(); 139 if (!isDataLoaded()) { 140 return null; 141 } 142 int pos = mConversation.position + 1; 143 while (pos < mCursor.getCount()) { 144 final Conversation conversation = conversationAtPosition(pos); 145 final long id = conversation.id; 146 if (!mSelectedSet.containsKey(id)) { 147 return conversation; 148 } 149 pos++; 150 } 151 return null; 152 } 153 154 /** 155 * @return the current conversation position in the list. 156 */ 157 public int getPosition() { 158 calculatePosition(); 159 return mConversation.position; 160 } 161 162 /** 163 * @return whether or not there is a newer conversation in the list. 164 */ 165 @VisibleForTesting 166 boolean hasNewer() { 167 calculatePosition(); 168 return isDataLoaded() && mCursor.moveToPosition(mConversation.position - 1); 169 } 170 171 /** 172 * @return whether or not there is an older conversation in the list. 173 */ 174 @VisibleForTesting 175 boolean hasOlder() { 176 calculatePosition(); 177 return isDataLoaded() && mCursor.moveToPosition(mConversation.position + 1); 178 } 179 180 /** 181 * Initializes the tracker with initial conversation id and initial position. This invalidates 182 * the positions in the tracker. We need a valid cursor before we can bless the position as 183 * valid. This requires a call to 184 * {@link #updateCursor(ConversationCursor)}. 185 */ 186 public void initialize(Conversation conversation) { 187 if (conversation.position < 0) { 188 LogUtils.wtf(LOG_TAG, "ConversationPositionTracker.initialize called with negative" 189 + " position. This is certainly wrong."); 190 throw new IllegalArgumentException(); 191 } 192 mConversation = conversation; 193 mCursorDirty = true; 194 } 195 196 /** @return whether or not we have a valid cursor to check the position of. */ 197 private boolean isDataLoaded() { 198 return mCursor != null && !mCursor.isClosed(); 199 } 200 201 /** 202 * Updates the underlying data when the conversation list changes. This class will try to find 203 * the existing conversation and update the position if the conversation is found. If the 204 * conversation that was pointed to by the existing position was not found, it will find the 205 * next valid possible conversation, though if none is found, it may become invalid. 206 * 207 * @return Whether or not the same conversation was found after the update and this position 208 * tracker is in a valid state. 209 */ 210 public void updateCursor(ConversationCursor cursor) { 211 mCursor = cursor; 212 // Now we should run applyCursor before proceeding. 213 mCursorDirty = true; 214 } 215 216 /** 217 * Recalculate the current position based on the cursor. This needs to be done once for 218 * each (Conversation, Cursor) pair. We could do this on every change of conversation or 219 * cursor, but that would be wasteful, since the recalculation of position is only required 220 * when transitioning to the next conversation. Transitions don't happen frequently, but 221 * changes in conversation and cursor do. So we defer this till it is actually needed. 222 * 223 * This method could change the current conversation if it cannot find the current conversation 224 * in the cursor. When this happens, this method sets the current conversation to some safe 225 * value and logs the reasons why it couldn't find the conversation. 226 * 227 * Calling this method repeatedly is safe: it returns early if it detects it has already been 228 * called. 229 */ 230 private void calculatePosition() { 231 // Run this method once for a mConversation, mCursor pair. 232 if (mCursor == null || !mCursorDirty) { 233 return; 234 } 235 mCursorDirty = false; 236 237 // If we don't have a valid position, exit early. 238 if (mConversation.position < 0) { 239 return; 240 } 241 242 final int listSize = (mCursor == null) ? 0 : mCursor.getCount(); 243 if (!isDataLoaded() || listSize == 0) { 244 return; 245 } 246 // Update the internal state for where the current conversation is in 247 // the list. Start from the beginning and find the current conversation in it. 248 int newPosition = 0; 249 while (mCursor.moveToPosition(newPosition)) { 250 if (getConversationId(mCursor) == mConversation.id) { 251 mConversation.position = newPosition; 252 final boolean changed = (mConversation.position != newPosition); 253 // Pre-emptively try to load the next cursor position so that the cursor window 254 // can be filled. The odd behavior of the ConversationCursor requires us to do this 255 // to ensure the adjacent conversation information is loaded for calls to hasNext. 256 mCursor.moveToPosition(newPosition + 1); 257 return; 258 } 259 newPosition++; 260 } 261 // If the conversation is no longer found in the list, try to save the same position if 262 // it is still a valid position. Otherwise, go back to a valid position until we can find 263 // a valid one. 264 if (mConversation.position >= listSize) { 265 // Go to the last position since our expected position is past this somewhere. 266 newPosition = mCursor.getCount() - 1; 267 } 268 269 // Did not keep the same conversation, but could still be a valid conversation. 270 if (isDataLoaded()){ 271 LogUtils.d(LOG_TAG, "ConversationPositionTracker: Could not find conversation %s" + 272 " in the cursor. Moving to position %d ", mConversation.toString(), 273 newPosition); 274 mCursor.moveToPosition(newPosition); 275 mConversation = new Conversation(mCursor); 276 } 277 return; 278 } 279 280 /** 281 * Get the next conversation according to the AutoAdvance settings and the list of 282 * conversations available in the folder. If no next conversation can be found, this method 283 * returns null. 284 * @param settings the settings associated with the account that contain the auto advance 285 * preference for the user. 286 * @return 287 */ 288 public Conversation getNextConversation(Settings settings) { 289 final int pref = Settings.getAutoAdvanceSetting(settings); 290 final boolean getNewer = (pref == AutoAdvance.NEWER && hasNewer()); 291 final boolean getOlder = (pref == AutoAdvance.OLDER && hasOlder()); 292 final Conversation next = getNewer ? getNewer() : 293 (getOlder ? getOlder() : null); 294 LogUtils.d(LOG_TAG, "ConversationPositionTracker.getNextConversation: " + 295 "getNewer = %b, getOlder = %b, Next conversation is %s", 296 getNewer, getOlder, next); 297 return next; 298 } 299}