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);