Home | History | Annotate | Download | only in ui
      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 {
     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 }