Add more descriptive UI for conflicts while renaming.

Test: updated existing rename conflict test and ran all
Bug: 31647077
Change-Id: I013bb9348e4691aea42f41f58af83e18e8ca106d
diff --git a/res/layout/dialog_file_name.xml b/res/layout/dialog_file_name.xml
index 3a95a13..6f8f9cf 100644
--- a/res/layout/dialog_file_name.xml
+++ b/res/layout/dialog_file_name.xml
@@ -19,11 +19,16 @@
     android:layout_height="wrap_content"
     android:fitsSystemWindows="true"
     android:padding="?android:attr/listPreferredItemPaddingEnd">
-
-    <EditText
-        android:id="@android:id/text1"
+    <android.support.design.widget.TextInputLayout
+        android:id="@+id/rename_input_wrapper"
+        android:orientation="vertical"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:inputType="text" />
-
+        android:layout_centerInParent="true" >
+        <EditText
+            android:id="@android:id/text1"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:inputType="text"/>
+    </android.support.design.widget.TextInputLayout>
 </FrameLayout>
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 0aaa12a..eae5187 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -817,7 +817,7 @@
 
         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
         List<DocumentInfo> docs = mModel.getDocuments(selected);
-        RenameDocumentFragment.show(getFragmentManager(), docs.get(0));
+        RenameDocumentFragment.show(getFragmentManager(), docs.get(0), mModel::hasFileWithName);
     }
 
     private boolean isDocumentEnabled(String mimeType, int flags) {
diff --git a/src/com/android/documentsui/dirlist/Model.java b/src/com/android/documentsui/dirlist/Model.java
index 7a76098..9ba8b5c 100644
--- a/src/com/android/documentsui/dirlist/Model.java
+++ b/src/com/android/documentsui/dirlist/Model.java
@@ -45,8 +45,10 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.Predicate;
 
 /**
@@ -82,12 +84,14 @@
 
     private static final String TAG = "Model";
 
+    /** Maps Model ID to cursor positions, for looking up items by Model ID. */
+    private final Map<String, Integer> mPositions = new HashMap<>();
+    private final Set<String> mFileNames = new HashSet<>();
+
     private boolean mIsLoading;
     private List<EventListener<Update>> mUpdateListeners = new ArrayList<>();
     @Nullable private Cursor mCursor;
     private int mCursorCount;
-    /** Maps Model ID to cursor positions, for looking up items by Model ID. */
-    private Map<String, Integer> mPositions = new HashMap<>();
     private String mIds[] = new String[0];
 
     @Nullable String info;
@@ -133,6 +137,7 @@
         error = null;
         doc = null;
         mIsLoading = false;
+        mFileNames.clear();
         notifyUpdateListeners();
     }
 
@@ -174,7 +179,7 @@
      */
     private void updateModelData() {
         mIds = new String[mCursorCount];
-
+        mFileNames.clear();
         mCursor.moveToPosition(-1);
         for (int pos = 0; pos < mCursorCount; ++pos) {
             if (!mCursor.moveToNext()) {
@@ -191,6 +196,7 @@
             } else {
                 mIds[pos] = getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
             }
+            mFileNames.add(getCursorString(mCursor, Document.COLUMN_DISPLAY_NAME));
         }
 
         // Populate the positions.
@@ -200,6 +206,10 @@
         }
     }
 
+    public boolean hasFileWithName(String name) {
+        return mFileNames.contains(name);
+    }
+
     public @Nullable Cursor getItem(String modelId) {
         Integer pos = mPositions.get(modelId);
         if (pos == null) {
diff --git a/src/com/android/documentsui/dirlist/RenameDocumentFragment.java b/src/com/android/documentsui/dirlist/RenameDocumentFragment.java
index ec91fe6..e2b87ea 100644
--- a/src/com/android/documentsui/dirlist/RenameDocumentFragment.java
+++ b/src/com/android/documentsui/dirlist/RenameDocumentFragment.java
@@ -33,11 +33,13 @@
 import android.provider.DocumentsContract;
 import android.support.annotation.Nullable;
 import android.support.design.widget.Snackbar;
+import android.support.design.widget.TextInputLayout;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.inputmethod.EditorInfo;
+import android.widget.Button;
 import android.widget.EditText;
 import android.widget.TextView;
 import android.widget.TextView.OnEditorActionListener;
@@ -50,6 +52,8 @@
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.ui.Snackbars;
 
+import java.util.function.Predicate;
+
 /**
  * Dialog to rename file or directory.
  */
@@ -57,10 +61,15 @@
     private static final String TAG_RENAME_DOCUMENT = "rename_document";
     private DocumentInfo mDocument;
     private EditText mEditText;
+    private TextInputLayout mRenameInputWrapper;
+    private Predicate<String> mHasFileNamed;
+    private @Nullable DialogInterface mDialog;
 
-    public static void show(FragmentManager fm, DocumentInfo document) {
+    public static void show(
+            FragmentManager fm, DocumentInfo document, Predicate<String> hasFileNamed) {
         final RenameDocumentFragment dialog = new RenameDocumentFragment();
         dialog.mDocument = document;
+        dialog.mHasFileNamed = hasFileNamed;
         dialog.show(fm, TAG_RENAME_DOCUMENT);
     }
 
@@ -77,22 +86,16 @@
         View view = dialogInflater.inflate(R.layout.dialog_file_name, null, false);
 
         mEditText = (EditText) view.findViewById(android.R.id.text1);
+        mRenameInputWrapper = (TextInputLayout) view.findViewById(R.id.rename_input_wrapper);
         builder.setTitle(R.string.menu_rename);
         builder.setView(view);
-
-        builder.setPositiveButton(
-                android.R.string.ok,
-                new OnClickListener() {
-                    @Override
-                    public void onClick(DialogInterface dialog, int which) {
-                        renameDocuments(mEditText.getText().toString());
-                    }
-                });
-
+        builder.setPositiveButton(android.R.string.ok, null);
         builder.setNegativeButton(android.R.string.cancel, null);
 
         final AlertDialog dialog = builder.create();
 
+        dialog.setOnShowListener(this::onShowDialog);
+
         // Workaround for the problem - virtual keyboard doesn't show on the phone.
         Shared.ensureKeyboardPresent(context, dialog);
 
@@ -105,8 +108,6 @@
                                 && event.getKeyCode() == KeyEvent.KEYCODE_ENTER
                                 && event.hasNoModifiers())) {
                             renameDocuments(mEditText.getText().toString());
-                            dialog.dismiss();
-                            return true;
                         }
                         return false;
                     }
@@ -114,6 +115,16 @@
         return dialog;
     }
 
+    private void onShowDialog(DialogInterface dialog){
+        mDialog = dialog;
+        Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
+        button.setOnClickListener(this::onClickDialog);
+    }
+
+    private void onClickDialog(View view) {
+        renameDocuments(mEditText.getText().toString());
+    }
+
     /**
      * Sets/Restores the data.
      * @param savedInstanceState
@@ -182,12 +193,16 @@
     private void renameDocuments(String newDisplayName) {
         BaseActivity activity = (BaseActivity) getActivity();
 
-        if (isValidDocumentName(newDisplayName)) {
-            new RenameDocumentsTask(activity, newDisplayName).execute(mDocument);
-        } else {
+        if (!isValidDocumentName(newDisplayName)) {
             Log.w(TAG, "Failed to rename file - invalid name:" + newDisplayName);
             Snackbars.makeSnackbar(getActivity(), R.string.rename_error,
                     Snackbar.LENGTH_SHORT).show();
+        } else if (mHasFileNamed.test(newDisplayName)){
+            mRenameInputWrapper.setError(getContext().getString(R.string.name_conflict));
+            selectFileName(mEditText);
+            Metrics.logRenameFileError(getContext());
+        } else {
+            new RenameDocumentsTask(activity, newDisplayName).execute(mDocument);
         }
 
     }
@@ -234,6 +249,9 @@
                 Snackbars.showRenameFailed(mActivity);
                 Metrics.logRenameFileError(getContext());
             }
+            if (mDialog != null) {
+                mDialog.dismiss();
+            }
             mActivity.setPending(false);
         }
     }
diff --git a/tests/common/com/android/documentsui/bots/UiBot.java b/tests/common/com/android/documentsui/bots/UiBot.java
index d73dacf..82f4822 100644
--- a/tests/common/com/android/documentsui/bots/UiBot.java
+++ b/tests/common/com/android/documentsui/bots/UiBot.java
@@ -119,6 +119,11 @@
                 .perform(ViewActions.replaceText(text));
     }
 
+    public void assertDialogText(String expected) throws UiObjectNotFoundException {
+        onView(TEXT_ENTRY)
+                .check(matches(withText(is(expected))));
+    }
+
     public boolean inFixedLayout() {
         TypedValue val = new TypedValue();
         // We alias files_activity to either fixed or drawer layouts based
@@ -192,6 +197,20 @@
         return title;
     }
 
+    public UiObject findFileRenameDialog() {
+        UiSelector selector = new UiSelector().text("Rename");
+        UiObject title = mDevice.findObject(selector);
+        title.waitForExists(mTimeout);
+        return title;
+    }
+
+    public UiObject findRenameErrorMessage() {
+        UiSelector selector = new UiSelector().text(mContext.getString(R.string.name_conflict));
+        UiObject title = mDevice.findObject(selector);
+        title.waitForExists(mTimeout);
+        return title;
+    }
+
     @SuppressWarnings("unchecked")
     public void assertDialogOkButtonFocused() {
         onView(withId(android.R.id.button1)).check(matches(hasFocus()));
diff --git a/tests/functional/com/android/documentsui/RenameDocumentUiTest.java b/tests/functional/com/android/documentsui/RenameDocumentUiTest.java
index 9848bd5..c761cad 100644
--- a/tests/functional/com/android/documentsui/RenameDocumentUiTest.java
+++ b/tests/functional/com/android/documentsui/RenameDocumentUiTest.java
@@ -138,20 +138,42 @@
     }
 
     public void testRename_NameExists() throws Exception {
+        renameWithConflict();
+
+        bots.main.clickDialogCancelButton();
+
+        bots.directory.assertDocumentsPresent(fileName1);
+        bots.directory.assertDocumentsPresent(fileName2);
+        bots.directory.assertDocumentsCount(4);
+    }
+
+    public void testRename_RecoverAfterConflict() throws Exception {
+        renameWithConflict();
+        device.waitForIdle();
+
+        bots.main.setDialogText(newName);
+
+        device.waitForIdle();
+        bots.main.clickDialogOkButton();
+
+        bots.directory.waitForDocument(newName);
+        bots.directory.assertDocumentsAbsent(fileName1);
+        bots.directory.assertDocumentsCount(4);
+    }
+
+    private void renameWithConflict() throws Exception {
         // Check that document with the new name exists
         bots.directory.assertDocumentsPresent(fileName2);
         bots.directory.selectDocument(fileName1, 1);
 
         clickRename();
 
-        bots.main.setDialogText(fileName2);
-
+        bots.main.assertDialogText(fileName1);
+        assertFalse(bots.main.findRenameErrorMessage().exists());
         bots.keyboard.pressEnter();
-
-        bots.directory.assertSnackbar(R.string.rename_error);
-        bots.directory.assertDocumentsPresent(fileName1);
-        bots.directory.assertDocumentsPresent(fileName2);
-        bots.directory.assertDocumentsCount(4);
+        assertTrue(bots.main.findRenameErrorMessage().exists());
+        bots.main.setDialogText(fileName2);
+        assertTrue(bots.main.findRenameErrorMessage().exists());
     }
 
     private void clickRename() throws UiObjectNotFoundException {