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