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}