Update pasting to handle more cases.

Fixes bug:5026774 pasting in email addresses changes

Change-Id: Id16e7470eb10a998fdb5efde06cb4449bb9d49e7
diff --git a/src/com/android/ex/chips/RecipientEditTextView.java b/src/com/android/ex/chips/RecipientEditTextView.java
index 29b3a1b..9ca2c38 100644
--- a/src/com/android/ex/chips/RecipientEditTextView.java
+++ b/src/com/android/ex/chips/RecipientEditTextView.java
@@ -18,6 +18,7 @@
 
 import android.app.Dialog;
 import android.content.ClipData;
+import android.content.ClipDescription;
 import android.content.ClipboardManager;
 import android.content.Context;
 import android.content.DialogInterface;
@@ -979,15 +980,20 @@
     }
 
     private boolean commitChip(int start, int end, Editable editable) {
-        if (getAdapter().getCount() > 0 && enoughToFilter()) {
+        ListAdapter adapter = getAdapter();
+        if (adapter != null && adapter.getCount() > 0 && enoughToFilter()
+                && end == getSelectionEnd()) {
             // choose the first entry.
             submitItemAtPosition(0);
             dismissDropDown();
             return true;
         } else {
             int tokenEnd = mTokenizer.findTokenEnd(editable, start);
-            if (editable.length() > tokenEnd && editable.charAt(tokenEnd) == ',') {
-                tokenEnd++;
+            if (editable.length() > tokenEnd + 1) {
+                char charAt = editable.charAt(tokenEnd + 1);
+                if (charAt == COMMIT_CHAR_COMMA || charAt == COMMIT_CHAR_SEMICOLON) {
+                    tokenEnd++;
+                }
             }
             String text = editable.toString().substring(start, tokenEnd).trim();
             clearComposingText();
@@ -1000,7 +1006,13 @@
                         editable.replace(start, end, chipText);
                     }
                 }
-                dismissDropDown();
+                // Only dismiss the dropdown if it is related to the text we
+                // just committed.
+                // For paste, it may not be as there are possibly multiple
+                // tokens being added.
+                if (end == getSelectionEnd()) {
+                    dismissDropDown();
+                }
                 sanitizeBetween();
                 return true;
             }
@@ -1113,7 +1125,7 @@
      */
     @Override
     protected void performFiltering(CharSequence text, int keyCode) {
-        if (enoughToFilter()) {
+        if (enoughToFilter() && !isCompletedToken(text)) {
             int end = getSelectionEnd();
             int start = mTokenizer.findTokenStart(text, end);
             // If this is a RecipientChip, don't filter
@@ -1127,6 +1139,22 @@
         super.performFiltering(text, keyCode);
     }
 
+    // Visible for testing.
+    /*package*/ boolean isCompletedToken(CharSequence text) {
+        if (TextUtils.isEmpty(text)) {
+            return false;
+        }
+        // Check to see if this is a completed token before filtering.
+        int end = text.length();
+        int start = mTokenizer.findTokenStart(text, end);
+        String token = text.toString().substring(start, end).trim();
+        if (!TextUtils.isEmpty(token)) {
+            char atEnd = token.charAt(token.length() - 1);
+            return atEnd == COMMIT_CHAR_COMMA || atEnd == COMMIT_CHAR_SEMICOLON;
+        }
+        return false;
+    }
+
     private void clearSelectedChip() {
         if (mSelectedChip != null) {
             unselectChip(mSelectedChip);
@@ -1909,6 +1937,92 @@
         }
     }
 
+    @Override
+    public boolean onTextContextMenuItem(int id) {
+        if (id == android.R.id.paste) {
+            removeTextChangedListener(mTextWatcher);
+            ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(
+                    Context.CLIPBOARD_SERVICE);
+            ClipData clip = clipboard.getPrimaryClip();
+            if (clip != null
+                    && clip.getDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
+                for (int i = 0; i < clip.getItemCount(); i++) {
+                    CharSequence paste = clip.getItemAt(i).getText();
+                    if (paste != null) {
+                        int start = getSelectionStart();
+                        int end = getSelectionEnd();
+                        Editable editable = getText();
+                        if (start >= 0 && end >= 0 && start != end) {
+                            editable.append(paste, start, end);
+                        } else {
+                            editable.insert(end, paste);
+                        }
+                        handlePaste();
+                    }
+                }
+            }
+            mHandler.post(mAddTextWatcher);
+            return true;
+        }
+        return super.onTextContextMenuItem(id);
+    }
+
+    // Visible for testing.
+    /* package */void handlePaste() {
+        String text = getText().toString();
+        int originalTokenStart = mTokenizer.findTokenStart(text, getSelectionEnd());
+        String lastAddress = text.substring(originalTokenStart);
+        int tokenStart = originalTokenStart;
+        int prevTokenStart = tokenStart;
+        RecipientChip findChip = null;
+        if (tokenStart != 0) {
+            // There are things before this!
+            while (tokenStart != 0 && findChip == null) {
+                prevTokenStart = tokenStart;
+                tokenStart = mTokenizer.findTokenStart(text, tokenStart);
+                findChip = findChip(tokenStart);
+            }
+            if (tokenStart != originalTokenStart) {
+                if (findChip != null) {
+                    tokenStart = prevTokenStart;
+                }
+                int tokenEnd;
+                RecipientChip createdChip;
+                while (tokenStart < originalTokenStart) {
+                    tokenEnd = movePastTerminators(mTokenizer.findTokenEnd(text, tokenStart));
+                    commitChip(tokenStart, tokenEnd, getText());
+                    createdChip = findChip(tokenStart);
+                    // +1 for the space at the end.
+                    tokenStart = getSpannable().getSpanEnd(createdChip) + 1;
+                }
+            }
+        }
+        // Take a look at the last token. If the token has been completed with a
+        // commit character, create a chip.
+        if (isCompletedToken(lastAddress)) {
+            Editable editable = getText();
+            commitChip(editable.toString().indexOf(lastAddress, originalTokenStart), editable
+                    .length(), editable);
+        }
+    }
+
+    // Visible for testing.
+    /* package */int movePastTerminators(int tokenEnd) {
+        if (tokenEnd >= length()) {
+            return tokenEnd;
+        }
+        char atEnd = getText().toString().charAt(tokenEnd);
+        if (atEnd == COMMIT_CHAR_COMMA || atEnd == COMMIT_CHAR_SEMICOLON) {
+            tokenEnd++;
+        }
+        // This token had not only an end token character, but also a space
+        // separating it from the next token.
+        if (tokenEnd < length() && getText().toString().charAt(tokenEnd) == ' ') {
+            tokenEnd++;
+        }
+        return tokenEnd;
+    }
+
     private class RecipientReplacementTask extends AsyncTask<Void, Void, Void> {
         private RecipientChip createFreeChip(RecipientEntry entry) {
             try {
diff --git a/tests/src/com/android/ex/chips/ChipsTest.java b/tests/src/com/android/ex/chips/ChipsTest.java
index eeeed0b..41b9eab 100644
--- a/tests/src/com/android/ex/chips/ChipsTest.java
+++ b/tests/src/com/android/ex/chips/ChipsTest.java
@@ -69,9 +69,20 @@
             return 48;
         }
 
+        @Override
         public Drawable getChipBackground(RecipientEntry contact) {
             return createChipBackground();
         }
+
+        @Override
+        public int length() {
+            return mEditable != null ? mEditable.length() : 0;
+        }
+
+        @Override
+        public String toString() {
+            return mEditable != null ? mEditable.toString() : "";
+        }
     }
 
     private MockRecipientEditTextView createViewForTesting() {
@@ -599,6 +610,180 @@
         assertEquals((String) spans[0].getDisplay(), "replacement");
     }
 
+    public void testHandlePaste() {
+        // Start with an empty edit field.
+        // Add an address; the text should be left as is.
+        MockRecipientEditTextView view = createViewForTesting();
+        view.setMoreItem(createTestMoreItem());
+        view.setChipBackground(createChipBackground());
+        view.setChipHeight(48);
+        mEditable = new SpannableStringBuilder();
+        mEditable.append("user@user.com");
+        view.setSelection(mEditable.length());
+        view.handlePaste();
+        assertEquals(mEditable.getSpans(0, mEditable.length(), RecipientChip.class).length, 0);
+        assertEquals(mEditable.toString(), "user@user.com");
+
+        // Test adding a single address to an empty chips field with a space at
+        // the end of it. The address should stay as text.
+        mEditable = new SpannableStringBuilder();
+        String tokenizedUser = "user@user.com" + " ";
+        mEditable.append(tokenizedUser);
+        view.setSelection(mEditable.length());
+        view.handlePaste();
+        assertEquals(mEditable.getSpans(0, mEditable.length(), RecipientChip.class).length, 0);
+        assertEquals(mEditable.toString(), tokenizedUser);
+
+        // Test adding a single address to an empty chips field with a semicolon at
+        // the end of it. The address should become a chip
+        mEditable = new SpannableStringBuilder();
+        tokenizedUser = "user@user.com;";
+        mEditable.append(tokenizedUser);
+        view.setSelection(mEditable.length());
+        view.handlePaste();
+        assertEquals(mEditable.getSpans(0, mEditable.length(), RecipientChip.class).length, 1);
+
+        // Test adding 2 address to an empty chips field. The second to last
+        // address should become a chip and the last address should stay as
+        // text.
+        mEditable = new SpannableStringBuilder();
+        mEditable.append("user1,user2@user.com");
+        view.setSelection(mEditable.length());
+        view.handlePaste();
+        assertEquals(mEditable.getSpans(0, mEditable.length(), RecipientChip.class).length, 1);
+        assertEquals(mEditable.getSpans(0, mEditable.toString().indexOf("user2@user.com"),
+                RecipientChip.class).length, 1);
+        assertEquals(mEditable.toString(), "<user1>, user2@user.com");
+
+        // Test adding a single address to the end of existing chips. The existing
+        // chips should remain, and the last address should stay as text.
+        populateMocks(3);
+        String first = (String) mTokenizer.terminateToken("FIRST");
+        String second = (String) mTokenizer.terminateToken("SECOND");
+        String third = (String) mTokenizer.terminateToken("THIRD");
+        mEditable = new SpannableStringBuilder();
+        mEditable.append(first + second + third);
+        view.setSelection(mEditable.length());
+        int firstStart = mEditable.toString().indexOf(first);
+        int firstEnd = firstStart + first.trim().length();
+        int secondStart = mEditable.toString().indexOf(second);
+        int secondEnd = secondStart + second.trim().length();
+        int thirdStart = mEditable.toString().indexOf(third);
+        int thirdEnd = thirdStart + third.trim().length();
+        mEditable.setSpan(mMockRecips[mMockRecips.length - 3], firstStart, firstEnd, 0);
+        mEditable.setSpan(mMockRecips[mMockRecips.length - 2], secondStart, secondEnd, 0);
+        mEditable.setSpan(mMockRecips[mMockRecips.length - 1], thirdStart, thirdEnd, 0);
+
+        mEditable.append("user@user.com");
+        view.setSelection(mEditable.length());
+        view.handlePaste();
+        assertEquals(mEditable.getSpans(0, mEditable.length(), RecipientChip.class).length,
+                mMockRecips.length);
+        assertEquals(mEditable.toString(), first + second + third + "user@user.com");
+
+        // Paste 2 addresses after existing chips. We expect the first address to be turned into
+        // a chip and the second to be left as text.
+        populateMocks(3);
+        mEditable = new SpannableStringBuilder();
+        mEditable.append(first + second + third);
+
+        mEditable.setSpan(mMockRecips[mMockRecips.length - 3], firstStart, firstEnd, 0);
+        mEditable.setSpan(mMockRecips[mMockRecips.length - 2], secondStart, secondEnd, 0);
+        mEditable.setSpan(mMockRecips[mMockRecips.length - 1], thirdStart, thirdEnd, 0);
+
+        mEditable.append("user1, user2@user.com");
+        view.setSelection(mEditable.length());
+        view.handlePaste();
+        assertEquals(mEditable.getSpans(0, mEditable.length(), RecipientChip.class).length,
+                mMockRecips.length + 1);
+        assertEquals(mEditable.getSpans(mEditable.toString().indexOf("<user1>"), mEditable
+                .toString().indexOf("user2@user.com") - 1, RecipientChip.class).length, 1);
+        assertEquals(mEditable.getSpans(mEditable.toString().indexOf("user2@user.com"), mEditable
+                .length(), RecipientChip.class).length, 0);
+        assertEquals(mEditable.toString(), first + second + third + "<user1>, user2@user.com");
+
+        // Paste 2 addresses after existing chips. We expect the first address to be turned into
+        // a chip and the second to be left as text. This removes the space seperator char between
+        // addresses.
+        populateMocks(3);
+        mEditable = new SpannableStringBuilder();
+        mEditable.append(first + second + third);
+
+        mEditable.setSpan(mMockRecips[mMockRecips.length - 3], firstStart, firstEnd, 0);
+        mEditable.setSpan(mMockRecips[mMockRecips.length - 2], secondStart, secondEnd, 0);
+        mEditable.setSpan(mMockRecips[mMockRecips.length - 1], thirdStart, thirdEnd, 0);
+
+        mEditable.append("user1,user2@user.com");
+        view.setSelection(mEditable.length());
+        view.handlePaste();
+        assertEquals(mEditable.getSpans(0, mEditable.length(), RecipientChip.class).length,
+                mMockRecips.length + 1);
+        assertEquals(mEditable.getSpans(mEditable.toString().indexOf("<user1>"), mEditable
+                .toString().indexOf("user2@user.com") - 1, RecipientChip.class).length, 1);
+        assertEquals(mEditable.getSpans(mEditable.toString().indexOf("user2@user.com"), mEditable
+                .length(), RecipientChip.class).length, 0);
+        assertEquals(mEditable.toString(), first + second + third + "<user1>, user2@user.com");
+
+        // Test a complete token pasted in at the end. It should be turned into a chip.
+        mEditable = new SpannableStringBuilder();
+        mEditable.append("user1, user2@user.com,");
+        view.setSelection(mEditable.length());
+        view.handlePaste();
+        assertEquals(mEditable.getSpans(0, mEditable.length(), RecipientChip.class).length, 2);
+        assertEquals(mEditable.getSpans(mEditable.toString().indexOf("<user1>"), mEditable
+                .toString().indexOf("user2@user.com") - 1, RecipientChip.class).length, 1);
+        assertEquals(mEditable.getSpans(mEditable.toString().indexOf("user2@user.com"), mEditable
+                .length(), RecipientChip.class).length, 1);
+        assertEquals(mEditable.toString(), "<user1>, <user2@user.com>, ");
+    }
+
+    public void testGetPastTerminators() {
+        MockRecipientEditTextView view = createViewForTesting();
+        view.setMoreItem(createTestMoreItem());
+        view.setChipBackground(createChipBackground());
+        view.setChipHeight(48);
+        String test = "test";
+        mEditable = new SpannableStringBuilder();
+        mEditable.append(test);
+        assertEquals(view.movePastTerminators(mTokenizer.findTokenEnd(mEditable.toString(), 0)),
+                test.length());
+
+        test = "test,";
+        mEditable = new SpannableStringBuilder();
+        mEditable.append(test);
+        assertEquals(view.movePastTerminators(mTokenizer.findTokenEnd(mEditable.toString(), 0)),
+                test.length());
+
+        test = "test, ";
+        mEditable = new SpannableStringBuilder();
+        mEditable.append(test);
+        assertEquals(view.movePastTerminators(mTokenizer.findTokenEnd(mEditable.toString(), 0)),
+                test.length());
+
+        test = "test;";
+        mEditable = new SpannableStringBuilder();
+        mEditable.append(test);
+        assertEquals(view.movePastTerminators(mTokenizer.findTokenEnd(mEditable.toString(), 0)),
+                test.length());
+
+        test = "test; ";
+        mEditable = new SpannableStringBuilder();
+        mEditable.append(test);
+        assertEquals(view.movePastTerminators(mTokenizer.findTokenEnd(mEditable.toString(), 0)),
+                test.length());
+    }
+
+    public void testIsCompletedToken() {
+        MockRecipientEditTextView view = createViewForTesting();
+        view.setMoreItem(createTestMoreItem());
+        view.setChipBackground(createChipBackground());
+        view.setChipHeight(48);
+        assertTrue(view.isCompletedToken("test;"));
+        assertTrue(view.isCompletedToken("test,"));
+        assertFalse(view.isCompletedToken("test"));
+        assertFalse(view.isCompletedToken("test "));
+    }
+
     private Drawable createChipBackground() {
         Bitmap drawable = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
         return new BitmapDrawable(getContext().getResources(), drawable);