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}