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 com.android.mail.browse.ConversationCursor;
21import com.android.mail.providers.Conversation;
22import com.android.mail.providers.Settings;
23import com.android.mail.providers.UIProvider.AutoAdvance;
24import com.android.mail.utils.LogTag;
25import com.android.mail.utils.LogUtils;
26import 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 */
33public class ConversationPositionTracker {
34    protected static final String LOG_TAG = LogTag.getLogTag();
35
36
37    public interface Callbacks {
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     */
52    public ConversationPositionTracker(Callbacks callbacks) {
53        mCallbacks = callbacks;
54    }
55
56    /** Move cursor to a specific position and return the conversation there */
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     */
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     */
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     */
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     */
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. */
135    private static boolean isDataLoaded(ConversationCursor cursor) {
136        return cursor != null && !cursor.isClosed();
137    }
138
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     */
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     */
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     */
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}