Merge "Updated support for animator in MediaNowPlayingView pre-Lollipop" into nyc-mr1-dev
diff --git a/build.gradle b/build.gradle
index 9d02725..258d4d0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -30,8 +30,8 @@
     doclava project(':doclava')
 }
 
-ext.supportVersion = '25.0.0-SNAPSHOT'
-ext.extraVersion = 36
+ext.supportVersion = '25.0.0'
+ext.extraVersion = 39
 ext.supportRepoOut = ''
 ext.buildToolsVersion = '23.0.2'
 ext.buildNumber = Integer.toString(ext.extraVersion)
diff --git a/v7/appcompat/src/android/support/v7/widget/ForwardingListener.java b/v7/appcompat/src/android/support/v7/widget/ForwardingListener.java
index 0a6e158..9de02b2 100644
--- a/v7/appcompat/src/android/support/v7/widget/ForwardingListener.java
+++ b/v7/appcompat/src/android/support/v7/widget/ForwardingListener.java
@@ -20,6 +20,7 @@
 import android.os.SystemClock;
 import android.support.annotation.RestrictTo;
 import android.support.v4.view.MotionEventCompat;
+import android.support.v4.view.ViewCompat;
 import android.support.v7.view.menu.ShowableListMenu;
 import android.view.MotionEvent;
 import android.view.View;
@@ -99,12 +100,12 @@
 
     private void addDetachListenerBase(View src) {
         src.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
-            boolean mIsAttached = mSrc.isAttachedToWindow();
+            boolean mIsAttached = ViewCompat.isAttachedToWindow(mSrc);
 
             @Override
             public void onGlobalLayout() {
                 final boolean wasAttached = mIsAttached;
-                mIsAttached = mSrc.isAttachedToWindow();
+                mIsAttached = ViewCompat.isAttachedToWindow(mSrc);
                 if (wasAttached && !mIsAttached) {
                     onDetachedFromWindow();
                 }
diff --git a/v7/appcompat/tests/src/android/support/v7/app/AppCompatVectorDrawableIntegrationTest.java b/v7/appcompat/tests/src/android/support/v7/app/AppCompatVectorDrawableIntegrationTest.java
index b021ade..905521d 100644
--- a/v7/appcompat/tests/src/android/support/v7/app/AppCompatVectorDrawableIntegrationTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/AppCompatVectorDrawableIntegrationTest.java
@@ -73,8 +73,9 @@
         assertEquals("Left side should be white", Color.red(leftColor), 255);
         assertEquals("Right side should be black", Color.red(rightColor), 0);
 
-        if (Build.VERSION.SDK_INT >= 17) {
-            // setLayoutDirection is only available after API 17.
+        if (Build.VERSION.SDK_INT >= 19) {
+            // setLayoutDirection is only available after API 17. However, it correctly set its
+            // drawable's layout direction until API 19.
             view1.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
             vectorDrawable.draw(mCanvas);
 
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteButton.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteButton.java
index 20e83aa..25288c0 100644
--- a/v7/mediarouter/src/android/support/v7/app/MediaRouteButton.java
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteButton.java
@@ -513,6 +513,9 @@
                     }
                 }
             }
+
+            setEnabled(mRouter.isRouteAvailable(mSelector,
+                    MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE));
         }
     }
 
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index a70c5de..4eb81a1 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -5903,6 +5903,20 @@
             }
         }
 
+        boolean isPrefetchPositionAttached(int position) {
+            final int childCount = mChildHelper.getUnfilteredChildCount();
+            for (int i = 0; i < childCount; i++) {
+                View attachedView = mChildHelper.getUnfilteredChildAt(i);
+                ViewHolder holder = getChildViewHolderInt(attachedView);
+                // TODO: consider ignoring if holder isInvalid
+                // Note: can use mPosition here because adapter doesn't have pending updates
+                if (holder.mPosition == position) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
         void prefetch(int[] itemPrefetchArray, int viewCount) {
             if (viewCount == 0) return;
 
@@ -5911,11 +5925,18 @@
                 throw new IllegalArgumentException("Recycler requested to prefetch invalid view "
                         + childPosition);
             }
-            View prefetchView = getViewForPosition(childPosition);
+
+            View prefetchView = null;
+            if (!isPrefetchPositionAttached(childPosition)) {
+                // only prefetch if child not already attached
+                prefetchView = getViewForPosition(childPosition);
+            }
             if (viewCount > 1) {
                 prefetch(itemPrefetchArray, viewCount - 1);
             }
-            recycleView(prefetchView);
+            if (prefetchView != null) {
+                recycleView(prefetchView);
+            }
         }
     }
 
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java
index f0225d3..0d538ac 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java
@@ -16,6 +16,9 @@
 
 package android.support.v7.widget;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.Matchers.any;
@@ -62,6 +65,13 @@
         return InstrumentationRegistry.getContext();
     }
 
+    private void layout(int width, int height) {
+        mRecyclerView.measure(
+                View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
+                View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
+        mRecyclerView.layout(0, 0, width, height);
+    }
+
     @Test
     public void prefetchReusesCacheItems() {
         RecyclerView.LayoutManager prefetchingLayoutManager = new RecyclerView.LayoutManager() {
@@ -102,8 +112,7 @@
         when(mockAdapter.getItemCount()).thenReturn(10);
         mRecyclerView.setAdapter(mockAdapter);
 
-        mRecyclerView.measure(View.MeasureSpec.AT_MOST | 320, View.MeasureSpec.AT_MOST | 240);
-        mRecyclerView.layout(0, 0, 320, 320);
+        layout(320, 320);
 
         verify(mockAdapter, never()).onCreateViewHolder(any(ViewGroup.class), anyInt());
         verify(mockAdapter, never()).onBindViewHolder(
@@ -127,6 +136,13 @@
         }
     }
 
+    private void verifyCacheDoesNotContainPosition(int position) {
+        for (int i = 0; i < mRecycler.mCachedViews.size(); i++) {
+            assertNotEquals("Cache must not contain position " + position,
+                    position, mRecycler.mCachedViews.get(i).mPosition);
+        }
+    }
+
     private void verifyCacheContainsPosition(int position) {
         for (int i = 0; i < mRecycler.mCachedViews.size(); i++) {
             if (mRecycler.mCachedViews.get(i).mPosition == position) return;
@@ -150,43 +166,43 @@
                     @Override
                     public RecyclerView.ViewHolder answer(InvocationOnMock invocation)
                             throws Throwable {
-                        return new RecyclerView.ViewHolder(new View(getContext())) {};
+                        View view = new View(getContext());
+                        view.setMinimumWidth(100);
+                        view.setMinimumHeight(100);
+                        return new RecyclerView.ViewHolder(view) {};
                     }
                 });
         when(mockAdapter.getItemCount()).thenReturn(100);
         mRecyclerView.setAdapter(mockAdapter);
 
+        layout(300, 100);
 
-        mRecyclerView.measure(View.MeasureSpec.AT_MOST | 320, View.MeasureSpec.AT_MOST | 320);
-        mRecyclerView.layout(0, 0, 320, 320);
-
-        mViewPrefetcher.mItemPrefetchArray = new int[] { 0, 1, 2 };
+        mViewPrefetcher.mItemPrefetchArray = new int[] { 3, 4, 5 };
         mRecycler.prefetch(mViewPrefetcher.mItemPrefetchArray, 3);
-        verifyCacheContainsPositions(0, 1, 2);
+        verifyCacheContainsPositions(3, 4, 5);
 
         // further views recycled, as though from scrolling, shouldn't evict prefetched views:
         mRecycler.recycleView(mRecycler.getViewForPosition(10));
-        verifyCacheContainsPositions(0, 1, 2, 10);
+        verifyCacheContainsPositions(3, 4, 5, 10);
 
         mRecycler.recycleView(mRecycler.getViewForPosition(20));
-        verifyCacheContainsPositions(0, 1, 2, 10, 20);
+        verifyCacheContainsPositions(3, 4, 5, 10, 20);
 
         mRecycler.recycleView(mRecycler.getViewForPosition(30));
-        verifyCacheContainsPositions(0, 1, 2, 20, 30);
+        verifyCacheContainsPositions(3, 4, 5, 20, 30);
 
         mRecycler.recycleView(mRecycler.getViewForPosition(40));
-        verifyCacheContainsPositions(0, 1, 2, 30, 40);
+        verifyCacheContainsPositions(3, 4, 5, 30, 40);
 
 
         // After clearing the cache, the prefetch priorities should be cleared as well:
         mRecyclerView.mRecycler.recycleAndClearCachedViews();
-        for (int i : new int[] {0, 1, 2, 50, 60, 70, 80, 90}) {
+        for (int i : new int[] {3, 4, 5, 50, 60, 70, 80, 90}) {
             mRecycler.recycleView(mRecycler.getViewForPosition(i));
         }
 
         // cache only contains most recent positions, no priority for previous prefetches:
         verifyCacheContainsPositions(50, 60, 70, 80, 90);
-
     }
 
     @Test
@@ -212,8 +228,7 @@
         // NOTE: requested cache size must be smaller than span count so two rows cannot fit
         mRecyclerView.setItemViewCacheSize(2);
 
-        mRecyclerView.measure(View.MeasureSpec.AT_MOST | 300, View.MeasureSpec.AT_MOST | 200);
-        mRecyclerView.layout(0, 0, 300, 150);
+        layout(300, 150);
         mRecyclerView.scrollBy(0, 75);
         assertTrue(mRecycler.mCachedViews.isEmpty());
 
@@ -234,4 +249,67 @@
         verifyCacheContainsPositions(9, 10, 11);
         assertTrue(mRecycler.mCachedViews.size() == 5);
     }
+
+    @Test
+    public void prefetchItemsSkipAnimations() {
+        LinearLayoutManager llm = new LinearLayoutManager(getContext());
+        mRecyclerView.setLayoutManager(llm);
+        final int[] expandedPosition = new int[] {-1};
+
+        final RecyclerView.Adapter adapter = new RecyclerView.Adapter() {
+            @Override
+            public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+                return new RecyclerView.ViewHolder(new View(parent.getContext())) {};
+            }
+
+            @Override
+            public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+                int height = expandedPosition[0] == position ? 400 : 100;
+                holder.itemView.setLayoutParams(new RecyclerView.LayoutParams(200, height));
+            }
+
+            @Override
+            public int getItemCount() { return 10; }
+        };
+
+        // make move duration long enough to be able to see the effects
+        RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator();
+        itemAnimator.setMoveDuration(10000);
+        mRecyclerView.setAdapter(adapter);
+
+        layout(200, 400);
+
+        expandedPosition[0] = 1;
+        // insert payload to avoid creating a new view
+        adapter.notifyItemChanged(1, new Object());
+
+        layout(200, 400);
+        layout(200, 400);
+
+        assertTrue(itemAnimator.isRunning());
+        assertEquals(2, llm.getChildCount());
+        assertEquals(4, mRecyclerView.getChildCount());
+
+        // animating view should be observable as hidden, uncached...
+        verifyCacheDoesNotContainPosition(2);
+        assertNotNull("Animating view should be found, hidden",
+                mRecyclerView.mChildHelper.findHiddenNonRemovedView(2, RecyclerView.INVALID_TYPE));
+
+        // ...but must not be removed for prefetch
+        mViewPrefetcher.mItemPrefetchArray = new int[] {-1};
+        int viewCount = mRecyclerView.getLayoutManager().gatherPrefetchIndices(0, 1,
+                mRecyclerView.mState, mViewPrefetcher.mItemPrefetchArray);
+        mRecycler.prefetch(mViewPrefetcher.mItemPrefetchArray, viewCount);
+        int prefetchTarget = mViewPrefetcher.mItemPrefetchArray[0];
+        assertEquals("Prefetch must target view 2", 2, prefetchTarget);
+
+        // animating view still observable as hidden, uncached
+        verifyCacheDoesNotContainPosition(2);
+        assertNotNull("Animating view should be found, hidden",
+                mRecyclerView.mChildHelper.findHiddenNonRemovedView(2, RecyclerView.INVALID_TYPE));
+
+        assertTrue(itemAnimator.isRunning());
+        assertEquals(2, llm.getChildCount());
+        assertEquals(4, mRecyclerView.getChildCount());
+    }
 }
\ No newline at end of file