allow the user to recover from stuck-cursor issues

Treat swipe-refresh as a trigger to clear stale adapter state that may
prevent normal cursor refresh from happening. While it's important to
delay cursor refresh until animations are complete (to prevent jank),
there was previously no way to get out of buggy states where the adapter
though an animation was happening but never did.

Bug: 14297883
Change-Id: I0d279770246e93c939b589cd0ac5eba76358b47e
diff --git a/src/com/android/mail/browse/ConversationCursor.java b/src/com/android/mail/browse/ConversationCursor.java
index 52b10d9..a5dbb5d 100644
--- a/src/com/android/mail/browse/ConversationCursor.java
+++ b/src/com/android/mail/browse/ConversationCursor.java
@@ -688,9 +688,11 @@
                             // cache entry
                             mDeletedCount--;
                             removed = true;
-                            LogUtils.d(LOG_TAG,
+                            LogUtils.i(LOG_TAG,
                                     "IN resetCursor, sDeletedCount decremented to: %d by %s",
-                                    mDeletedCount, key);
+                                    mDeletedCount,
+                                    (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) ? key
+                                            : "[redacted]");
                         }
                     }
                 } else {
@@ -2339,6 +2341,10 @@
         sb.append(mDeletedCount);
         sb.append(" mUnderlying=");
         sb.append(mUnderlyingCursor);
+        if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
+            sb.append(" mCacheMap=");
+            sb.append(mCacheMap);
+        }
         sb.append("}");
         return sb.toString();
     }
diff --git a/src/com/android/mail/ui/AbstractActivityController.java b/src/com/android/mail/ui/AbstractActivityController.java
index 19a0314..0393436 100644
--- a/src/com/android/mail/ui/AbstractActivityController.java
+++ b/src/com/android/mail/ui/AbstractActivityController.java
@@ -3039,7 +3039,10 @@
     @Override
     public final void onRefreshRequired() {
         if (isAnimating() || isDragging()) {
-            LogUtils.i(ConversationCursor.LOG_TAG, "onRefreshRequired: delay until animating done");
+            final ConversationListFragment f = getConversationListFragment();
+            LogUtils.w(ConversationCursor.LOG_TAG,
+                    "onRefreshRequired: delay until animating done. cursor=%s adapter=%s",
+                    mConversationListCursor, (f != null) ? f.getAnimatedAdapter() : null);
             return;
         }
         // Refresh the query in the background
@@ -3099,6 +3102,11 @@
         if (!isAnimating()) {
             // Swap cursors
             mConversationListCursor.sync();
+        } else {
+            // (CLF guaranteed to be non-null due to check in isAnimating)
+            LogUtils.w(LOG_TAG,
+                    "AAC.onRefreshReady suppressing sync() due to animation. cursor=%s aa=%s",
+                    mConversationListCursor, getConversationListFragment().getAnimatedAdapter());
         }
         mTracker.onCursorUpdated();
         perhapsShowFirstSearchResult();
@@ -3159,6 +3167,10 @@
 
     @Override
     public void onAnimationEnd(AnimatedAdapter animatedAdapter) {
+        if (animatedAdapter != null) {
+            LogUtils.i(LOG_TAG, "AAC.onAnimationEnd. cursor=%s adapter=%s", mConversationListCursor,
+                    animatedAdapter);
+        }
         if (mConversationListCursor == null) {
             LogUtils.e(LOG_TAG, "null ConversationCursor in onAnimationEnd");
             return;
@@ -3461,7 +3473,8 @@
                 return null;
             }
             return new ConversationCursorLoader(mActivity, account,
-                    folder.conversationListUri, folder.name, ignoreInitialConversationLimit);
+                    folder.conversationListUri, folder.getTypeDescription(),
+                    ignoreInitialConversationLimit);
         }
 
         @Override
diff --git a/src/com/android/mail/ui/AnimatedAdapter.java b/src/com/android/mail/ui/AnimatedAdapter.java
index 2bbc6ce..811e636 100644
--- a/src/com/android/mail/ui/AnimatedAdapter.java
+++ b/src/com/android/mail/ui/AnimatedAdapter.java
@@ -973,6 +973,25 @@
                 || !mSwipeDeletingItems.isEmpty();
     }
 
+    /**
+     * Forcibly clear any internal state that would cause {@link #isAnimating()} to return true.
+     * Call this in times of desperation, when you really, really want to trash state and just
+     * start over.
+     */
+    public void clearAnimationState() {
+        if (!isAnimating()) {
+            return;
+        }
+
+        mUndoingItems.clear();
+        mSwipeUndoingItems.clear();
+        mFadeLeaveBehindItems.clear();
+        mDeletingItems.clear();
+        mSwipeDeletingItems.clear();
+        mAnimatingViews.clear();
+        LogUtils.w(LOG_TAG, "AA.clearAnimationState forcibly cleared state, this=%s", this);
+    }
+
     @Override
     public String toString() {
         final StringBuilder sb = new StringBuilder("{");
@@ -991,6 +1010,10 @@
         sb.append(mFadeLeaveBehindItems);
         sb.append(" mLastDeletingItems=");
         sb.append(mLastDeletingItems);
+        sb.append(" mAnimatingViews=");
+        sb.append(mAnimatingViews);
+        sb.append(" mPendingDestruction=");
+        sb.append(mPendingDestruction);
         sb.append("}");
         return sb.toString();
     }
diff --git a/src/com/android/mail/ui/ConversationListFragment.java b/src/com/android/mail/ui/ConversationListFragment.java
index 6f057aa..8766c56 100644
--- a/src/com/android/mail/ui/ConversationListFragment.java
+++ b/src/com/android/mail/ui/ConversationListFragment.java
@@ -722,8 +722,14 @@
 
     public boolean isAnimating() {
         final AnimatedAdapter adapter = getAnimatedAdapter();
-        return (adapter != null && adapter.isAnimating()) ||
-                (mListView != null && mListView.isScrolling());
+        if (adapter != null && adapter.isAnimating()) {
+            return true;
+        }
+        final boolean isScrolling = (mListView != null && mListView.isScrolling());
+        if (isScrolling) {
+            LogUtils.i(LOG_TAG, "CLF.isAnimating=true due to scrolling");
+        }
+        return isScrolling;
     }
 
     private void clearChoicesAndActivated() {
@@ -1140,6 +1146,14 @@
 
         // This will call back to showSyncStatusBar():
         mActivity.getFolderController().requestFolderRefresh();
+
+        // Clear list adapter state out of an abundance of caution.
+        // There is a class of bugs where an animation that should have finished doesn't (maybe
+        // it didn't start, or it didn't finish), and the list gets stuck pretty much forever.
+        // Clearing the state here is in line with user expectation for 'refresh'.
+        getAnimatedAdapter().clearAnimationState();
+        // possibly act on the now-cleared state
+        mActivity.onAnimationEnd(mListAdapter);
     }
 
     /**