Fix focus problems when using optimized fragment transactions.

Bug 33754255

Instead of making Views INVISIBLE immediately after adding them,
Views are set to alpha = 0 (or INVISIBLE pre-11) only after they
have been determined to be postponed.

Test: gradlew connectedCheck in fragments/

Change-Id: I474af8ca19f7544afef28067e8d11749ef3571b4
diff --git a/fragment/java/android/support/v4/app/Fragment.java b/fragment/java/android/support/v4/app/Fragment.java
index 0c7300e..881c2b4 100644
--- a/fragment/java/android/support/v4/app/Fragment.java
+++ b/fragment/java/android/support/v4/app/Fragment.java
@@ -321,6 +321,11 @@
     // True if mHidden has been changed and the animation should be scheduled.
     boolean mHiddenChanged;
 
+    // The alpha of the view when the view was added and then postponed. If the value is less
+    // than zero, this means that the view's add was canceled and should not participate in
+    // removal animations.
+    float mPostponedAlpha;
+
     /**
      * State information that has been retrieved from a fragment instance
      * through {@link FragmentManager#saveFragmentInstanceState(Fragment)
diff --git a/fragment/java/android/support/v4/app/FragmentManager.java b/fragment/java/android/support/v4/app/FragmentManager.java
index e7d13ff..587bda1 100644
--- a/fragment/java/android/support/v4/app/FragmentManager.java
+++ b/fragment/java/android/support/v4/app/FragmentManager.java
@@ -32,6 +32,7 @@
 import android.support.annotation.RestrictTo;
 import android.support.annotation.StringRes;
 import android.support.v4.os.BuildCompat;
+import android.support.v4.util.ArraySet;
 import android.support.v4.util.DebugUtils;
 import android.support.v4.util.LogWriter;
 import android.support.v4.util.Pair;
@@ -1382,10 +1383,12 @@
                         if (f.mView != null && f.mContainer != null) {
                             Animation anim = null;
                             if (mCurState > Fragment.INITIALIZING && !mDestroyed
-                                    && f.mView.getVisibility() == View.VISIBLE) {
+                                    && f.mView.getVisibility() == View.VISIBLE
+                                    && f.mPostponedAlpha >= 0) {
                                 anim = loadAnimation(f, transit, false,
                                         transitionStyle);
                             }
+                            f.mPostponedAlpha = 0;
                             if (anim != null) {
                                 final Fragment fragment = f;
                                 f.setAnimatingAway(f.mView);
@@ -1540,7 +1543,12 @@
             }
             if (f.mIsNewlyAdded && f.mContainer != null) {
                 // Make it visible and run the animations
-                f.mView.setVisibility(View.VISIBLE);
+                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+                    f.mView.setVisibility(View.VISIBLE);
+                } else if (f.mPostponedAlpha > 0f) {
+                    f.mView.setAlpha(f.mPostponedAlpha);
+                }
+                f.mPostponedAlpha = 0f;
                 f.mIsNewlyAdded = false;
                 // run animations:
                 Animation anim = loadAnimation(f, f.getNextTransition(), true,
@@ -2135,9 +2143,11 @@
 
         int postponeIndex = endIndex;
         if (allowOptimization) {
-            moveFragmentsToInvisible();
+            ArraySet<Fragment> addedFragments = new ArraySet<>();
+            addAddedFragments(addedFragments);
             postponeIndex = postponePostponableTransactions(records, isRecordPop,
-                    startIndex, endIndex);
+                    startIndex, endIndex, addedFragments);
+            makeRemovedFragmentsInvisible(addedFragments);
         }
 
         if (postponeIndex != startIndex && allowOptimization) {
@@ -2161,6 +2171,30 @@
     }
 
     /**
+     * Any fragments that were removed because they have been postponed should have their views
+     * made invisible by setting their alpha to 0 on API >= 11 or setting visibility to INVISIBLE
+     * on API < 11.
+     *
+     * @param fragments The fragments that were added during operation execution. Only the ones
+     *                  that are no longer added will have their alpha changed.
+     */
+    private void makeRemovedFragmentsInvisible(ArraySet<Fragment> fragments) {
+        final int numAdded = fragments.size();
+        for (int i = 0; i < numAdded; i++) {
+            final Fragment fragment = fragments.valueAt(i);
+            if (!fragment.mAdded) {
+                final View view = fragment.getView();
+                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+                    fragment.getView().setVisibility(View.INVISIBLE);
+                } else {
+                    fragment.mPostponedAlpha = view.getAlpha();
+                    view.setAlpha(0f);
+                }
+            }
+        }
+    }
+
+    /**
      * Examine all transactions and determine which ones are marked as postponed. Those will
      * have their operations rolled back and moved to the end of the record list (up to endIndex).
      * It will also add the postponed transaction to the queue.
@@ -2173,7 +2207,8 @@
      * postponed.
      */
     private int postponePostponableTransactions(ArrayList<BackStackRecord> records,
-            ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) {
+            ArrayList<Boolean> isRecordPop, int startIndex, int endIndex,
+            ArraySet<Fragment> added) {
         int postponeIndex = endIndex;
         for (int i = endIndex - 1; i >= startIndex; i--) {
             final BackStackRecord record = records.get(i);
@@ -2204,7 +2239,7 @@
                 }
 
                 // different views may be visible now
-                moveFragmentsToInvisible();
+                addAddedFragments(added);
             }
         }
         return postponeIndex;
@@ -2237,15 +2272,26 @@
         }
         if (moveToState) {
             moveToState(mCurState, true);
-        } else if (mActive != null) {
+        }
+
+        if (mActive != null) {
             final int numActive = mActive.size();
             for (int i = 0; i < numActive; i++) {
                 // Allow added fragments to be removed during the pop since we aren't going
                 // to move them to the final state with moveToState(mCurState).
                 Fragment fragment = mActive.get(i);
-                if (fragment.mView != null && fragment.mIsNewlyAdded
+                if (fragment != null && fragment.mView != null && fragment.mIsNewlyAdded
                         && record.interactsWith(fragment.mContainerId)) {
-                    fragment.mIsNewlyAdded = false;
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB
+                            && fragment.mPostponedAlpha > 0) {
+                        fragment.mView.setAlpha(fragment.mPostponedAlpha);
+                    }
+                    if (moveToState) {
+                        fragment.mPostponedAlpha = 0;
+                    } else {
+                        fragment.mPostponedAlpha = -1;
+                        fragment.mIsNewlyAdded = false;
+                    }
                 }
             }
         }
@@ -2309,10 +2355,11 @@
 
     /**
      * Ensure that fragments that are added are moved to at least the CREATED state.
-     * Any newly-added Views are made INVISIBLE so that the Transaction can be postponed
-     * with {@link Fragment#postponeEnterTransition()}.
+     * Any newly-added Views are inserted into {@code added} so that the Transaction can be
+     * postponed with {@link Fragment#postponeEnterTransition()}. They will later be made
+     * invisible (by setting their alpha to 0) if they have been removed when postponed.
      */
-    private void moveFragmentsToInvisible() {
+    private void addAddedFragments(ArraySet<Fragment> added) {
         if (mCurState < Fragment.CREATED) {
             return;
         }
@@ -2325,7 +2372,7 @@
                 moveToState(fragment, state, fragment.getNextAnim(), fragment.getNextTransition(),
                         false);
                 if (fragment.mView != null && !fragment.mHidden && fragment.mIsNewlyAdded) {
-                    fragment.mView.setVisibility(View.INVISIBLE);
+                    added.add(fragment);
                 }
             }
         }
diff --git a/fragment/java/android/support/v4/app/FragmentTransition.java b/fragment/java/android/support/v4/app/FragmentTransition.java
index aa64859..71ac889 100644
--- a/fragment/java/android/support/v4/app/FragmentTransition.java
+++ b/fragment/java/android/support/v4/app/FragmentTransition.java
@@ -1042,7 +1042,8 @@
             case BackStackRecord.OP_DETACH:
                 if (isOptimizedTransaction) {
                     setFirstOut = !fragment.mAdded && fragment.mView != null
-                            && fragment.mView.getVisibility() == View.VISIBLE;
+                            && fragment.mView.getVisibility() == View.VISIBLE
+                            && fragment.mPostponedAlpha >= 0;
                 } else {
                     setFirstOut = fragment.mAdded && !fragment.mHidden;
                 }
diff --git a/fragment/tests/java/android/support/v4/app/FragmentAnimationTest.java b/fragment/tests/java/android/support/v4/app/FragmentAnimationTest.java
index 873e36d..33e20d0 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentAnimationTest.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentAnimationTest.java
@@ -22,6 +22,7 @@
 import static org.junit.Assert.assertTrue;
 
 import android.app.Instrumentation;
+import android.os.Build;
 import android.os.Parcelable;
 import android.support.annotation.AnimRes;
 import android.support.fragment.test.R;
@@ -295,6 +296,9 @@
         assertPostponed(fragment2, 0);
         assertNotNull(fragment1.getView());
         assertEquals(View.VISIBLE, fragment1.getView().getVisibility());
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+            assertEquals(1f, fragment1.getView().getAlpha(), 0f);
+        }
         assertTrue(ViewCompat.isAttachedToWindow(fragment1.getView()));
 
         fragment2.startPostponedEnterTransition();
@@ -335,6 +339,9 @@
 
         assertNotNull(fragment1.getView());
         assertEquals(View.VISIBLE, fragment1.getView().getVisibility());
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+            assertEquals(1f, fragment1.getView().getAlpha(), 0f);
+        }
         assertTrue(ViewCompat.isAttachedToWindow(fragment1.getView()));
         assertTrue(fragment1.isAdded());
 
@@ -456,7 +463,12 @@
     private void assertPostponed(AnimatorFragment fragment, int expectedAnimators)
             throws InterruptedException {
         assertTrue(fragment.mOnCreateViewCalled);
-        assertEquals(View.INVISIBLE, fragment.getView().getVisibility());
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+            assertEquals(View.INVISIBLE, fragment.getView().getVisibility());
+        } else {
+            assertEquals(View.VISIBLE, fragment.getView().getVisibility());
+            assertEquals(0f, fragment.getView().getAlpha(), 0f);
+        }
         assertEquals(expectedAnimators, fragment.numAnimators);
     }
 
diff --git a/fragment/tests/java/android/support/v4/app/FragmentOptimizationTest.java b/fragment/tests/java/android/support/v4/app/FragmentOptimizationTest.java
index 08be463..06abd69 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentOptimizationTest.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentOptimizationTest.java
@@ -24,7 +24,9 @@
 import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v4.app.test.FragmentTestActivity;
+import android.view.View;
 import android.view.ViewGroup;
+import android.widget.EditText;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -644,4 +646,42 @@
         assertEquals(1, fragment1.onCreateViewCount);
     }
 
+    // Test that a fragment view that is created with focus has focus after the transaction
+    // completes.
+    @Test
+    public void focusedView() throws Throwable {
+        FragmentTestUtil.setContentView(mActivityRule, R.layout.double_container);
+        mContainer = (ViewGroup) mActivityRule.getActivity().findViewById(R.id.fragmentContainer1);
+        final EditText firstEditText = new EditText(mContainer.getContext());
+        mInstrumentation.runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mContainer.addView(firstEditText);
+                firstEditText.requestFocus();
+            }
+        });
+        assertTrue(firstEditText.isFocused());
+        final CountCallsFragment fragment1 = new CountCallsFragment();
+        final CountCallsFragment fragment2 = new CountCallsFragment();
+        fragment2.setLayoutId(R.layout.with_edit_text);
+        mInstrumentation.runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mFM.beginTransaction()
+                        .add(R.id.fragmentContainer2, fragment1)
+                        .addToBackStack(null)
+                        .setAllowOptimization(true)
+                        .commit();
+                mFM.beginTransaction()
+                        .replace(R.id.fragmentContainer2, fragment2)
+                        .addToBackStack(null)
+                        .setAllowOptimization(true)
+                        .commit();
+                mFM.executePendingTransactions();
+            }
+        });
+        final View editText = fragment2.getView().findViewById(R.id.editText);
+        assertTrue(editText.isFocused());
+        assertFalse(firstEditText.isFocused());
+    }
 }
diff --git a/fragment/tests/java/android/support/v4/app/PostponedTransitionTest.java b/fragment/tests/java/android/support/v4/app/PostponedTransitionTest.java
index fe41976..0f07b89 100644
--- a/fragment/tests/java/android/support/v4/app/PostponedTransitionTest.java
+++ b/fragment/tests/java/android/support/v4/app/PostponedTransitionTest.java
@@ -664,6 +664,9 @@
         assertNull(fragment.getView());
         assertNotNull(mBeginningFragment.getView());
         assertEquals(View.VISIBLE, mBeginningFragment.getView().getVisibility());
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+            assertEquals(1f, mBeginningFragment.getView().getAlpha(), 0f);
+        }
         assertTrue(mBeginningFragment.getView().isAttachedToWindow());
     }
 
@@ -695,6 +698,9 @@
         assertNotNull(fragment2);
         assertNotNull(fragment2.getView());
         assertEquals(View.VISIBLE, fragment2.getView().getVisibility());
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+            assertEquals(1f, fragment2.getView().getAlpha(), 0f);
+        }
         assertTrue(fragment2.isResumed());
         assertTrue(fragment2.isAdded());
         assertTrue(fragment2.getView().isAttachedToWindow());
@@ -726,7 +732,12 @@
         assertTrue(fromFragment.getView().isAttachedToWindow());
         assertTrue(toFragment.getView().isAttachedToWindow());
         assertEquals(View.VISIBLE, fromFragment.getView().getVisibility());
-        assertEquals(View.INVISIBLE, toFragment.getView().getVisibility());
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+            assertEquals(View.VISIBLE, toFragment.getView().getVisibility());
+            assertEquals(0f, toFragment.getView().getAlpha(), 0f);
+        } else {
+            assertEquals(View.INVISIBLE, toFragment.getView().getVisibility());
+        }
         assureNoTransition(fromFragment);
         assureNoTransition(toFragment);
         assertTrue(fromFragment.isResumed());
diff --git a/fragment/tests/res/layout/with_edit_text.xml b/fragment/tests/res/layout/with_edit_text.xml
new file mode 100644
index 0000000..7fd21a0
--- /dev/null
+++ b/fragment/tests/res/layout/with_edit_text.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:orientation="vertical">
+    <EditText android:layout_width="wrap_content"
+              android:layout_height="wrap_content"
+              android:id="@+id/editText"
+              android:text="@string/hello">
+        <requestFocus/>
+    </EditText>
+</LinearLayout>