Make hasTransientState be true only if user created selection is active.

Previously, hasTransientState returned true even when TextView has a
selection that hasn't been created by the user. This unnecessarily
prevents the TextView from being recycled.
This issue was introduced by Ib454b0fbbc2c2f8d198, which fixes that
setHasTransientState(true) is not always coupled with
setHasTransientState(false).

With this CL:
hasTransientState will get true when selection action mode is started.
hasTransientState will get false when selection is removed.
Note that hasTransientState intentionally continues to be true when
selection action mode is terminated with preserving selection.

Bug: 27913323
Change-Id: I960ddfd7221caeb676c23926af06a8a415dec288
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 59d857c..47b0348 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -3640,6 +3640,9 @@
             }
 
             if (menu.hasVisibleItems() || mode.getCustomView() != null) {
+                if (mHasSelection && !mTextView.hasTransientState()) {
+                    mTextView.setHasTransientState(true);
+                }
                 return true;
             } else {
                 return false;
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 3195097..cf3da7b 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -8294,14 +8294,12 @@
                     newSelEnd = Selection.getSelectionEnd(buf);
                 }
 
-                if (newSelStart == newSelEnd && hasTransientState()) {
-                    setHasTransientState(false);
-                } else if (newSelStart != newSelEnd && !hasTransientState()) {
-                    setHasTransientState(true);
-                }
-
                 if (mEditor != null) {
                     mEditor.refreshTextActionMode();
+                    if (!hasSelection() && mEditor.mTextActionMode == null && hasTransientState()) {
+                        // User generated selection has been removed.
+                        setHasTransientState(false);
+                    }
                 }
                 onSelectionChanged(newSelStart, newSelEnd);
             }
diff --git a/core/tests/coretests/src/android/widget/TextViewActivityTest.java b/core/tests/coretests/src/android/widget/TextViewActivityTest.java
index f779d6c..ecf88f1 100644
--- a/core/tests/coretests/src/android/widget/TextViewActivityTest.java
+++ b/core/tests/coretests/src/android/widget/TextViewActivityTest.java
@@ -675,4 +675,26 @@
         assertFloatingToolbarContainsItem(
                 getActivity().getString(com.android.internal.R.string.copy));
     }
+
+    @SmallTest
+    public void testTransientState() throws Exception {
+        final String text = "abc def";
+        onView(withId(R.id.textview)).perform(click());
+        onView(withId(R.id.textview)).perform(replaceText(text));
+
+        final TextView textView = (TextView) getActivity().findViewById(R.id.textview);
+        assertFalse(textView.hasTransientState());
+
+        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('b')));
+        // hasTransientState should return true when user generated selection is active.
+        assertTrue(textView.hasTransientState());
+        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.indexOf('d')));
+        // hasTransientState should return false as the selection has been cleared.
+        assertFalse(textView.hasTransientState());
+        textView.post(
+                () -> Selection.setSelection((Spannable) textView.getText(), 0, text.length()));
+        getInstrumentation().waitForIdleSync();
+        // hasTransientState should return false when selection is created by API.
+        assertFalse(textView.hasTransientState());
+    }
 }