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 }